pax_global_header00006660000000000000000000000064145750540110014514gustar00rootroot0000000000000052 comment=63daad463aa1b85c4f6b92a0c53ae17c57df9a31 mistral-dashboard-19.0.0/000077500000000000000000000000001457505401100152035ustar00rootroot00000000000000mistral-dashboard-19.0.0/.gitignore000066400000000000000000000010241457505401100171700ustar00rootroot00000000000000*.py[cod] *.sqlite *.sw? # C extensions *.so # Packages *.egg *.egg-info dist build .venv eggs .eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox .stestr/ # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject .idea .DS_Store *.lock mistraldashboard/test/.secret_key_store # Files created by releasenotes build releasenotes/build # Files created by doc build doc/source/api # pbr generates these AUTHORS ChangeLog mistral-dashboard-19.0.0/.gitreview000066400000000000000000000001241457505401100172060ustar00rootroot00000000000000[gerrit] host=review.opendev.org port=29418 project=openstack/mistral-dashboard.git mistral-dashboard-19.0.0/.stestr.conf000066400000000000000000000000711457505401100174520ustar00rootroot00000000000000[DEFAULT] test_path=./mistraldashboard/tests top_dir=./ mistral-dashboard-19.0.0/.zuul.yaml000066400000000000000000000001741457505401100171460ustar00rootroot00000000000000- project: templates: - horizon-non-primary-django-jobs - check-requirements - openstack-python3-jobs mistral-dashboard-19.0.0/CONTRIBUTING.rst000066400000000000000000000011711457505401100176440ustar00rootroot00000000000000The source repository for this project can be found at: https://opendev.org/openstack/mistral-dashboard Pull requests submitted through GitHub are not monitored. To start contributing to OpenStack, follow the steps in the contribution guide to set up and use Gerrit: https://docs.openstack.org/contributors/code-and-documentation/quick-start.html Bugs should be filed on Launchpad: https://bugs.launchpad.net/mistral For more specific information about contributing to this repository, see the Mistral Dashboard contributor guide: https://docs.openstack.org/mistral/latest/developer/contributor/contributing.html mistral-dashboard-19.0.0/LICENSE000066400000000000000000000236361457505401100162220ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. mistral-dashboard-19.0.0/README.rst000066400000000000000000000042751457505401100167020ustar00rootroot00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/mistral-dashboard.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on ================= Mistral Dashboard ================= Horizon plugin for Mistral. Setup Instructions ================== This instruction assumes that Horizon is already installed and it's installation folder is . Detailed information on how to install Horizon can be found at https://docs.openstack.org/horizon/latest/contributor/quickstart.html#setup. The installation folder of Mistral Dashboard will be referred to as . The following should get you started. Clone the repository into your local OpenStack directory:: $ git clone https://opendev.org/openstack/mistral-dashboard.git Install mistral-dashboard $ sudo pip install -e Or if you're planning to run Horizon server in a virtual environment (see below): $ tox -evenv -- pip install -e ../mistral-dashboard/ and then $ cp -b /mistraldashboard/enabled/_50_mistral.py /openstack_dashboard/local/enabled/_50_mistral.py Since Mistral only supports Identity v3, you must ensure that the dashboard points the proper OPENSTACK_KEYSTONE_URL in /openstack_dashboard/local/local_settings.py file:: OPENSTACK_API_VERSIONS = { "identity": 3, } OPENSTACK_KEYSTONE_URL = "http://%s:5000/v3" % OPENSTACK_HOST Also, make sure you have changed OPENSTACK_HOST to point to your Keystone server and check all endpoints are accessible. You may want to change OPENSTACK_ENDPOINT_TYPE to "publicURL" if some of them are not. When you're ready, you would need to either restart your apache:: $ sudo service apache2 restart or run the development server (in case you have decided to use local horizon):: $ cd ../horizon/ $ tox -evenv -- python manage.py runserver Mistral-Dashboard Debug Instructions ------------------------------------ For debug instructions refer to `OpenStack Mistral Troubleshooting `_ mistral-dashboard-19.0.0/doc/000077500000000000000000000000001457505401100157505ustar00rootroot00000000000000mistral-dashboard-19.0.0/doc/requirements.txt000066400000000000000000000002151457505401100212320ustar00rootroot00000000000000sphinx>=2.0.0,!=2.1.0 # BSD oslosphinx>=4.7.0 # Apache-2.0 reno>=3.1.0 # Apache-2.0 docutils>=0.11 # OSI-Approved Open Source, Public Domain mistral-dashboard-19.0.0/doc/source/000077500000000000000000000000001457505401100172505ustar00rootroot00000000000000mistral-dashboard-19.0.0/doc/source/conf.py000066400000000000000000000042441457505401100205530ustar00rootroot00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'oslosphinx', ] # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'mistral-dashboard' copyright = '2014, OpenStack Foundation' # -- Options for openstackdocstheme ------------------------------------------- openstackdocs_repo_name = 'openstack/mistral-dashboard' openstackdocs_bug_project = 'mistral' openstackdocs_bug_tag = 'doc' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = [] # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project mistral-dashboard-19.0.0/doc/source/contributing.rst000066400000000000000000000001131457505401100225040ustar00rootroot00000000000000============ Contributing ============ .. include:: ../../CONTRIBUTING.rst mistral-dashboard-19.0.0/doc/source/index.rst000066400000000000000000000003741457505401100211150ustar00rootroot00000000000000 Welcome to Mistral Dashboard's documentation! ============================================ Contents: .. toctree:: :maxdepth: 1 readme contributing Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` mistral-dashboard-19.0.0/doc/source/readme.rst000066400000000000000000000000351457505401100212350ustar00rootroot00000000000000.. include:: ../../README.rstmistral-dashboard-19.0.0/manage.py000077500000000000000000000015101457505401100170050ustar00rootroot00000000000000#!/usr/bin/env python # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import sys from django.core.management import execute_from_command_line # noqa if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mistraldashboard.test.settings") execute_from_command_line(sys.argv) mistral-dashboard-19.0.0/mistraldashboard/000077500000000000000000000000001457505401100205265ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/__init__.py000066400000000000000000000000001457505401100226250ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/action_executions/000077500000000000000000000000001457505401100242515ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/action_executions/__init__.py000066400000000000000000000000001457505401100263500ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/action_executions/forms.py000066400000000000000000000067361457505401100257650ustar00rootroot00000000000000# Copyright 2016 - Nokia. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ from horizon import forms from mistraldashboard import api from mistraldashboard.handle_errors import handle_errors class UpdateForm(forms.SelfHandlingForm): action_execution_id = forms.CharField(label=_("Action Execution ID"), widget=forms.HiddenInput(), required=False) output_source = forms.ChoiceField( label=_('Output'), help_text=_('Content for output. ' 'Select either file, raw content or Null value.'), choices=[('null', _(' (sends empty value)')), ('file', _('File')), ('raw', _('Direct Input'))], widget=forms.Select( attrs={'class': 'switchable', 'data-slug': 'outputsource'} ), required=False ) output_upload = forms.FileField( label=_('Output File'), help_text=_('A local output to upload'), widget=forms.FileInput( attrs={'class': 'switched', 'data-switch-on': 'outputsource', 'data-outputsource-file': _('Output File')} ), required=False ) output_data = forms.CharField( label=_('Output Data'), help_text=_('The raw content for output'), widget=forms.widgets.Textarea( attrs={'class': 'switched', 'data-switch-on': 'outputsource', 'data-outputsource-raw': _('Output Data'), 'rows': 4} ), required=False ) state = forms.ChoiceField( label=_('State'), help_text=_('Select state to update'), choices=[('null', _(' (sends empty value)')), ('SUCCESS', _('Success')), ('ERROR', _('Error'))], widget=forms.Select( attrs={'class': 'switchable'} ), required=False ) def clean(self): cleaned_data = super(UpdateForm, self).clean() cleaned_data['output'] = None if cleaned_data.get('output_upload'): files = self.request.FILES cleaned_data['output'] = files['output_upload'].read() elif cleaned_data.get('output_data'): cleaned_data['output'] = cleaned_data['output_data'] elif cleaned_data.get('output_source') == 'null': cleaned_data['output'] = None del cleaned_data['output_upload'] del cleaned_data['output_data'] del cleaned_data['output_source'] if cleaned_data['state'] == 'null': cleaned_data['state'] = None return cleaned_data @handle_errors(_("Unable to update Action Execution"), []) def handle(self, request, data): return api.action_execution_update( request, data['action_execution_id'], data['state'], data['output'], ) mistral-dashboard-19.0.0/mistraldashboard/action_executions/panel.py000066400000000000000000000015111457505401100257200ustar00rootroot00000000000000# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ import horizon from mistraldashboard import dashboard class ActionExecutions(horizon.Panel): name = _("Action Executions") slug = 'action_executions' dashboard.MistralDashboard.register(ActionExecutions) mistral-dashboard-19.0.0/mistraldashboard/action_executions/tables.py000066400000000000000000000103061457505401100260750ustar00rootroot00000000000000# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from horizon import tables from mistraldashboard import api from mistraldashboard.default import smart_cell from mistraldashboard.default import utils smart_cell.init() class UpdateRow(tables.Row): ajax = True def get_data(self, request, id): instance = api.action_execution_get(request, id) return instance class DeleteActionExecution(tables.DeleteAction): @staticmethod def action_present(count): return ngettext_lazy( u"Delete Action Execution", u"Delete Action Executions", count ) @staticmethod def action_past(count): return ngettext_lazy( u"Deleted Action Execution", u"Deleted Action Executions", count ) def delete(self, request, action_execution_id): api.action_execution_delete(request, action_execution_id) class UpdateActionExecution(tables.LinkAction): name = "updateAE" verbose_name = _("Update") url = "horizon:mistral:action_executions:update" classes = ("ajax-modal",) class TaskExecutionIDColumn(tables.Column): def get_link_url(self, datum): task_url = "horizon:mistral:tasks:detail" obj_id = datum.task_execution_id return reverse(task_url, args=[obj_id]) class WorkflowNameColumn(tables.Column): def get_link_url(self, datum): workflow_url = "horizon:mistral:workflows:detail" obj_id = datum.workflow_name return reverse(workflow_url, args=[obj_id]) class ActionExecutionsTable(tables.DataTable): def getHoverHelp(data): if hasattr(data, 'state_info') and data.state_info: return {'title': data.state_info} STATE_STATUS_CHOICES = ( ("success", True), ("error", False), ("idle", None), ("running", None), ("canceled", None), ) id = tables.Column( "id", verbose_name=_("ID"), link="horizon:mistral:action_executions:detail" ) name = tables.Column( "name", verbose_name=_("Name") ) tags = tables.Column( "tags", verbose_name=_("Tags") ) workflow_name = WorkflowNameColumn( "workflow_name", verbose_name=_("Workflow Name"), link=True ) task_execution_id = TaskExecutionIDColumn( "task_execution_id", verbose_name=_("Task Execution ID"), link=True ) task_name = tables.Column( "task_name", verbose_name=_("Task name") ) description = tables.Column( "description", verbose_name=_("Description") ) created_at = tables.Column( "created_at", verbose_name=_("Created at"), filters=[utils.humantime] ) updated_at = tables.Column( "updated_at", verbose_name=_("Updated at"), filters=[utils.humantime] ) accepted = tables.Column( "accepted", verbose_name=_("Accepted"), filters=[utils.booleanfield], ) state = tables.Column( "state", status=True, status_choices=STATE_STATUS_CHOICES, verbose_name=_("State"), filters=[utils.label], cell_attributes_getter=getHoverHelp ) class Meta(object): name = "actionExecutions" verbose_name = _("Action Executions") status_columns = ["state"] row_class = UpdateRow table_actions = ( tables.FilterAction, DeleteActionExecution ) row_actions = (UpdateActionExecution, DeleteActionExecution) mistral-dashboard-19.0.0/mistraldashboard/action_executions/templates/000077500000000000000000000000001457505401100262475ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/action_executions/templates/action_executions/000077500000000000000000000000001457505401100317725ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/action_executions/templates/action_executions/_update.html000066400000000000000000000010221457505401100342740ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Enter new output and or state to update the corresponding Action Execution." %}

{% trans "For more info refer to:" %}
{% trans "Mistral documentation - Action Executions" %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/action_executions/templates/action_executions/detail.html000066400000000000000000000070631457505401100341300ustar00rootroot00000000000000 {% extends 'mistral/default/base.html' %} {% load i18n %} {% block title %}{% trans "Action Execution Details" %}{% endblock %} {% block page_header %}

{% trans "Action Execution Details" %}

{% endblock page_header %} {% block main %} {% load i18n sizeformat %}

{% trans "Overview" %}


{% trans "Name" %}
{{ action_execution.name }}
{% trans "ID" %}
{{ action_execution.id }}
{% if action_execution.description %}
{% trans "Description" %}
{{ action_execution.description }}
{% endif %}
{% trans "State" %}
{{ action_execution.state }}
{% if action_execution.state_info %}
{% trans "State Info" %}
{{ action_execution.state_info }}
{% endif %}
{% trans "Accepted" %}
{{ action_execution.accepted }}
{% trans "Tags" %}
{{ action_execution.tags }}

{% trans "Creation Date" %}
{{ action_execution.created_at|parse_isotime}}
{% trans "Time Since Created" %}
{{ action_execution.created_at|parse_isotime|timesince }}

{% trans "Update Date" %}
{{ action_execution.updated_at|parse_isotime}}
{% trans "Time Since Updated" %}
{{ action_execution.updated_at|parse_isotime|timesince }}

{% trans "Input" %}
{{ action_execution.input }}
{% trans "Output" %}
{{ action_execution.output }}
{% if action_execution.workflow_url %}

{% trans "Workflow" %}


{% trans "Name" %}
{{ action_execution.workflow_name }}
{% endif %} {% if action_execution.task_execution_url %}

{% trans "Task Execution" %}


{% trans "Name" %}
{{ action_execution.task_name }}
{% trans "ID" %}
{{ action_execution.task_execution_id }}
{% endif %} {% endblock %}
filtered.html000066400000000000000000000005221457505401100343760ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/action_executions/templates/action_executions{% extends 'mistral/default/table.html' %} {% load i18n %} {% block title %} {% trans "Action Executions" %} {{ task_id }} {% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Action Executions of Task ID:") %}

{{ task_id }}

{% endblock page_header %} mistral-dashboard-19.0.0/mistraldashboard/action_executions/templates/action_executions/index.html000066400000000000000000000004101457505401100337620ustar00rootroot00000000000000{% extends 'mistral/default/table.html' %} {% load i18n %} {% block title %} {% trans "Action Executions" %} {% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Action Executions")%} {% endblock page_header %} mistral-dashboard-19.0.0/mistraldashboard/action_executions/templates/action_executions/update.html000066400000000000000000000004051457505401100341410ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" %} {% endblock page_header %} {% block main %} {% include 'mistral/executions/_update.html' %} {% endblock %}mistral-dashboard-19.0.0/mistraldashboard/action_executions/tests.py000066400000000000000000000053111457505401100257650ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.urls import reverse from openstack_dashboard.test import helpers from mistraldashboard import api from mistraldashboard.test import helpers as test INDEX_URL = reverse('horizon:mistral:action_executions:index') class ActionExecutionsTest(test.TestCase): @helpers.create_mocks({api: ('action_executions_list',)}) def test_index(self): self.mock_action_executions_list.return_value =\ self.mistralclient_action_executions.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'mistral/action_executions/index.html') self.mock_action_executions_list.assert_called_once_with( helpers.IsHttpRequest()) @helpers.create_mocks({api: ('action_execution_update',)}) def test_update_post(self): action_execution = self.mistralclient_action_executions.first() self.mock_action_execution_update.return_value = action_execution form_data = {"action_execution_id": action_execution.id, "state": action_execution.state, "output_source": "raw", "output_data": action_execution.output} res = self.client.post( reverse('horizon:mistral:action_executions:update', args=(action_execution.id,)), form_data) self.assertNoFormErrors(res) self.mock_action_execution_update.assert_called_once_with( helpers.IsHttpRequest(), action_execution.id, action_execution.state, action_execution.output) @helpers.create_mocks({api: ('action_execution_get',)}) def test_detail(self): action_execution = self.mistralclient_action_executions.list()[0] self.mock_action_execution_get.return_value = action_execution url = reverse('horizon:mistral:action_executions:detail', args=[action_execution.id]) res = self.client.get(url) self.assertTemplateUsed(res, 'mistral/action_executions/detail.html') self.mock_action_execution_get.assert_called_once_with( helpers.IsHttpRequest(), action_execution.id) mistral-dashboard-19.0.0/mistraldashboard/action_executions/urls.py000066400000000000000000000025061457505401100256130ustar00rootroot00000000000000# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from mistraldashboard.action_executions import views ACTION_EXECUTIONS = r'^(?P[^/]+)/%s$' TASKS = r'^(?P[^/]+)/%s$' urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path(ACTION_EXECUTIONS % 'detail', views.OverviewView.as_view(), name='detail'), re_path(ACTION_EXECUTIONS % 'input', views.CodeView.as_view(), {'column': 'input'}, name='input'), re_path(ACTION_EXECUTIONS % 'output', views.CodeView.as_view(), {'column': 'output'}, name='output'), re_path(ACTION_EXECUTIONS % 'update', views.UpdateView.as_view(), name='update'), re_path(TASKS % 'task', views.FilteredByTaskView.as_view(), name='task') ] mistral-dashboard-19.0.0/mistraldashboard/action_executions/views.py000066400000000000000000000125551457505401100257700ustar00rootroot00000000000000# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views import generic from horizon import forms from horizon import tables from mistraldashboard.action_executions import forms as action_execution_forms from mistraldashboard.action_executions import tables as mistral_tables from mistraldashboard import api from mistraldashboard.default import utils from mistraldashboard import forms as mistral_forms def get_single_action_execution_data(request, **kwargs): action_execution_id = kwargs['action_execution_id'] action_execution = api.action_execution_get( request, action_execution_id ) return action_execution class OverviewView(generic.TemplateView): template_name = 'mistral/action_executions/detail.html' page_title = _("Action Execution Details") workflow_url = 'horizon:mistral:workflows:detail' task_execution_url = 'horizon:mistral:tasks:detail' def get_context_data(self, **kwargs): context = super(OverviewView, self).get_context_data(**kwargs) action_execution = get_single_action_execution_data( self.request, **kwargs ) if action_execution.workflow_name: action_execution.workflow_url = reverse( self.workflow_url, args=[action_execution.workflow_name]) if action_execution.task_execution_id: action_execution.task_execution_url = reverse( self.task_execution_url, args=[action_execution.task_execution_id] ) if action_execution.input: action_execution.input = utils.prettyprint(action_execution.input) if action_execution.output: action_execution.output = utils.prettyprint( action_execution.output ) if action_execution.state: action_execution.state = utils.label(action_execution.state) action_execution.accepted = utils.booleanfield( action_execution.accepted ) breadcrumb = [(action_execution.id, reverse( 'horizon:mistral:action_executions:detail', args=[action_execution.id] ))] context["custom_breadcrumb"] = breadcrumb context['action_execution'] = action_execution return context class CodeView(forms.ModalFormView): template_name = 'mistral/default/code.html' modal_header = _("Code view") form_id = "code_view" form_class = mistral_forms.EmptyForm cancel_label = "OK" cancel_url = reverse_lazy("horizon:mistral:action_executions:index") page_title = _("Code view") def get_context_data(self, **kwargs): context = super(CodeView, self).get_context_data(**kwargs) column = self.kwargs['column'] action_execution = get_single_action_execution_data( self.request, **self.kwargs ) io = {} if column == 'input': io['name'] = _('Input') io['value'] = utils.prettyprint(action_execution.input) elif column == 'output': io['name'] = _('Output') io['value'] = ( utils.prettyprint(action_execution.output) if action_execution.output else _("No available output yet") ) context['io'] = io return context class IndexView(tables.DataTableView): table_class = mistral_tables.ActionExecutionsTable template_name = 'mistral/action_executions/index.html' def get_data(self): return api.action_executions_list(self.request) class UpdateView(forms.ModalFormView): template_name = 'mistral/action_executions/update.html' modal_header = _("Update Action Execution") form_id = "update_action_execution" form_class = action_execution_forms.UpdateForm submit_label = _("Update") success_url = reverse_lazy("horizon:mistral:action_executions:index") submit_url = "horizon:mistral:action_executions:update" cancel_url = "horizon:mistral:action_executions:index" page_title = _("Update Action Execution") def get_initial(self): return {"action_execution_id": self.kwargs["action_execution_id"]} def get_context_data(self, **kwargs): context = super(UpdateView, self).get_context_data(**kwargs) context['submit_url'] = reverse( self.submit_url, args=[self.kwargs["action_execution_id"]] ) return context class FilteredByTaskView(tables.DataTableView): table_class = mistral_tables.ActionExecutionsTable template_name = 'mistral/action_executions/filtered.html' data = {} def get_data(self, **kwargs): task_id = self.kwargs['task_id'] data = api.action_executions_list(self.request, task_id) return data mistral-dashboard-19.0.0/mistraldashboard/actions/000077500000000000000000000000001457505401100221665ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/actions/__init__.py000066400000000000000000000000001457505401100242650ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/actions/forms.py000066400000000000000000000146761457505401100237040ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from django.urls import reverse from django.utils.translation import gettext_lazy as _ from horizon import exceptions from horizon import forms from horizon import messages from mistraldashboard import api class RunForm(forms.SelfHandlingForm): action_name = forms.CharField( label=_("Action"), required=True, widget=forms.TextInput(attrs={'readonly': 'readonly'}) ) input = forms.CharField( label=_("Input"), required=False, initial="{}", widget=forms.widgets.Textarea() ) save_result = forms.CharField( label=_("Save result to DB"), required=False, widget=forms.CheckboxInput() ) def handle(self, request, data): try: input = json.loads(data['input']) except Exception as e: msg = _('Action input is invalid JSON: %s') % str(e) messages.error(request, msg) return False try: params = {"save_result": data['save_result'] == 'True'} action = api.action_run( request, data['action_name'], input, params ) msg = _('Run action has been created with name ' '"%s".') % action.name messages.success(request, msg) return True except Exception as e: # In case of a failure, keep the dialog open and show the error msg = _('Failed to run action "%(action_name)s"' ' %(e)s:') % {'action_name': data['action_name'], 'e': str(e) } messages.error(request, msg) return False class CreateForm(forms.SelfHandlingForm): definition_source = forms.ChoiceField( label=_('Definition Source'), choices=[('file', _('File')), ('raw', _('Direct Input'))], widget=forms.Select( attrs={'class': 'switchable', 'data-slug': 'definitionsource'}) ) definition_upload = forms.FileField( label=_('Definition File'), help_text=_('A local definition to upload.'), widget=forms.FileInput( attrs={'class': 'switched', 'data-switch-on': 'definitionsource', 'data-required-when-shown': 'true', 'data-definitionsource-file': _('Definition File')} ), required=False ) definition_data = forms.CharField( label=_('Definition Data'), help_text=_('The raw contents of the definition.'), widget=forms.widgets.Textarea( attrs={'class': 'switched', 'data-switch-on': 'definitionsource', 'data-required-when-shown': 'true', 'data-definitionsource-raw': _('Definition Data'), 'rows': 4} ), required=False ) def clean(self): cleaned_data = super(CreateForm, self).clean() if cleaned_data.get('definition_upload'): files = self.request.FILES cleaned_data['definition'] = files['definition_upload'].read() elif cleaned_data.get('definition_data'): cleaned_data['definition'] = cleaned_data['definition_data'] else: raise forms.ValidationError( _('You must specify the definition source.')) return cleaned_data def handle(self, request, data): try: api.action_create(request, data['definition']) msg = _('Successfully created action.') messages.success(request, msg) return True except Exception: msg = _('Failed to create action.') redirect = reverse('horizon:mistral:actions:index') exceptions.handle(request, msg, redirect=redirect) class UpdateForm(forms.SelfHandlingForm): definition_source = forms.ChoiceField( label=_('Definition Source'), choices=[('file', _('File')), ('raw', _('Direct Input'))], widget=forms.Select( attrs={'class': 'switchable', 'data-slug': 'definitionsource'}) ) definition_upload = forms.FileField( label=_('Definition File'), help_text=_('A local definition to upload.'), widget=forms.FileInput( attrs={'class': 'switched', 'data-switch-on': 'definitionsource', 'data-definitionsource-file': _('Definition File')} ), required=False ) definition_data = forms.CharField( label=_('Definition Data'), help_text=_('The raw contents of the definition.'), widget=forms.widgets.Textarea( attrs={'class': 'switched', 'data-switch-on': 'definitionsource', 'data-definitionsource-raw': _('Definition Data'), 'rows': 4} ), required=False ) def clean(self): cleaned_data = super(UpdateForm, self).clean() if cleaned_data.get('definition_upload'): files = self.request.FILES cleaned_data['definition'] = files['definition_upload'].read() elif cleaned_data.get('definition_data'): cleaned_data['definition'] = cleaned_data['definition_data'] else: raise forms.ValidationError( _('You must specify the definition source.')) return cleaned_data def handle(self, request, data): try: api.action_update(request, data['definition']) msg = _('Successfully updated action.') messages.success(request, msg) return True except Exception: msg = _('Failed to update action.') redirect = reverse('horizon:mistral:actions:index') exceptions.handle(request, msg, redirect=redirect) mistral-dashboard-19.0.0/mistraldashboard/actions/panel.py000066400000000000000000000014711457505401100236420ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ import horizon from mistraldashboard import dashboard class Actions(horizon.Panel): name = _("Actions") slug = 'actions' dashboard.MistralDashboard.register(Actions) mistral-dashboard-19.0.0/mistraldashboard/actions/tables.py000066400000000000000000000070051457505401100240140ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from horizon import tables from horizon.utils import filters from mistraldashboard import api from mistraldashboard.default import smart_cell from mistraldashboard.default import utils smart_cell.init() class CreateAction(tables.LinkAction): name = "create" verbose_name = _("Create Action") url = "horizon:mistral:actions:create" classes = ("ajax-modal",) icon = "plus" class UpdateAction(tables.LinkAction): name = "update" verbose_name = _("Update Action") url = "horizon:mistral:actions:update" classes = ("ajax-modal",) icon = "pencil" class DeleteAction(tables.DeleteAction): @staticmethod def action_present(count): return ngettext_lazy( u"Delete Action", u"Delete Actions", count ) @staticmethod def action_past(count): return ngettext_lazy( u"Deleted Action", u"Deleted Actions", count ) def delete(self, request, action_name): api.action_delete(request, action_name) def allowed(self, request, action=None): if action: return not action.is_system else: return True def tags_to_string(action): return ', '.join(action.tags) if action.tags else None def cut(action, length=100): inputs = action.input if inputs and len(inputs) > length: return "%s..." % inputs[:length] else: return inputs class RunAction(tables.LinkAction): name = "run" verbose_name = _("Run") classes = ("ajax-modal",) def get_link_url(self, datum): obj_id = datum.name url = "horizon:mistral:actions:run" return reverse(url, args=[obj_id]) class ActionsTable(tables.DataTable): name = tables.Column( "name", verbose_name=_("Name"), link="horizon:mistral:actions:detail" ) is_system = tables.Column( "is_system", verbose_name=_("Is System"), filters=[utils.booleanfield] ) tags = tables.Column( tags_to_string, verbose_name=_("Tags") ) inputs = tables.Column( cut, verbose_name=_("Input") ) created = tables.Column( "created_at", verbose_name=_("Created"), filters=( filters.parse_isotime, filters.timesince_or_never ) ) updated = tables.Column( "updated_at", verbose_name=_("Updated"), filters=( filters.parse_isotime, filters.timesince_or_never ) ) class Meta(object): name = "actions" verbose_name = _("Actions") table_actions = ( CreateAction, UpdateAction, DeleteAction, tables.FilterAction, ) row_actions = (RunAction, DeleteAction) mistral-dashboard-19.0.0/mistraldashboard/actions/templates/000077500000000000000000000000001457505401100241645ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/actions/templates/actions/000077500000000000000000000000001457505401100256245ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/actions/templates/actions/_create.html000066400000000000000000000011401457505401100301100ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block form_attrs %}enctype="multipart/form-data"{% endblock %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Use one of the available definition source options to specify the definition to be used in creating this action." %}

{% trans "Refer"%} Mistral Workflow Language {% trans " documentation for syntax details." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/actions/templates/actions/_run.html000066400000000000000000000003171457505401100274560ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "From here you can run an action." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/actions/templates/actions/_update.html000066400000000000000000000011401457505401100301270ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block form_attrs %}enctype="multipart/form-data"{% endblock %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Use one of the available definition source options to specify the definition to be used in updating this action." %}

{% trans "Refer"%} Mistral Workflow Language {% trans " documentation for syntax details." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/actions/templates/actions/create.html000066400000000000000000000002651457505401100277600ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Create Action" %}{% endblock %} {% block main %} {% include 'mistral/actions/_create.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/actions/templates/actions/detail.html000066400000000000000000000025171457505401100277610ustar00rootroot00000000000000 {% extends 'mistral/default/base.html' %} {% load i18n %} {% block title %}{% trans "Action Definition" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Action Definition") %} {% endblock page_header %} {% block main %}

{% trans "Overview" %}


{% trans "Name" %}
{{ action.name }}
{% trans "ID" %}
{{ action.id }}
{% trans "Tags" %}
{{ action.tags }}
{% trans "Created at" %}
{{ action.created_at }}
{% trans "Is system" %}
{{ action.is_system }}
{% trans "Updated at" %}
{{ action.updated_at }}
{% trans "Scope" %}
{{ action.scope }}
{% trans "Input" %}
{{ action.input }}
{% trans "Description" %}
{{ action.description }}
{% trans "Definition" %}
{{ action.definition }}
{% endblock %}
mistral-dashboard-19.0.0/mistraldashboard/actions/templates/actions/index.html000066400000000000000000000003561457505401100276250ustar00rootroot00000000000000{% extends 'mistral/default/table.html' %} {% load i18n %} {% block title %}{% trans "Actions" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Actions") %} {% endblock page_header %}mistral-dashboard-19.0.0/mistraldashboard/actions/templates/actions/run.html000066400000000000000000000003761457505401100273240ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" %} {% endblock page_header %} {% block main %} {% include 'mistral/actions/_run.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/actions/templates/actions/update.html000066400000000000000000000002651457505401100277770ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Update Action" %}{% endblock %} {% block main %} {% include 'mistral/actions/_update.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/actions/tests.py000066400000000000000000000060421457505401100237040ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.urls import reverse from openstack_dashboard.test import helpers as horizon_test from mistraldashboard import api from mistraldashboard.test import helpers as test INDEX_URL = reverse('horizon:mistral:actions:index') class ActionsTest(test.TestCase): @horizon_test.create_mocks({api: ('pagination_list',)}) def test_index(self): self.mock_pagination_list.return_value =\ [self.mistralclient_actions.list(), False, False] res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'mistral/actions/index.html') self.mock_pagination_list.assert_called_once_with( entity="actions", request=horizon_test.IsHttpRequest(), marker=None, sort_keys='name', sort_dirs='desc', paginate=True, reversed_order=True) @horizon_test.create_mocks({api: ('action_create',)}) def test_create_post(self): action = self.mistralclient_actions.first() self.mock_action_create.return_value = action url = reverse("horizon:mistral:actions:create") form_data = { 'definition_source': 'raw', 'definition_data': action.definition } res = self.client.post(url, form_data) self.assertNoFormErrors(res) self.mock_action_create.assert_called_once_with( horizon_test.IsHttpRequest(), action.definition) @horizon_test.create_mocks({api: ('action_update',)}) def test_update_post(self): action = self.mistralclient_actions.first() self.mock_action_update.return_value = action url = reverse("horizon:mistral:actions:update") form_data = { 'definition_source': 'raw', 'definition_data': action.definition } res = self.client.post(url, form_data) self.assertNoFormErrors(res) self.mock_action_update.assert_called_once_with( horizon_test.IsHttpRequest(), action.definition) @horizon_test.create_mocks({api: ('action_get',)}) def test_detail(self): action = self.mistralclient_actions.list()[0] self.mock_action_get.return_value = action url = reverse('horizon:mistral:actions:detail', args=[action.id]) res = self.client.get(url) self.assertTemplateUsed(res, 'mistral/actions/detail.html') self.mock_action_get.assert_called_once_with( horizon_test.IsHttpRequest(), action.id) mistral-dashboard-19.0.0/mistraldashboard/actions/urls.py000066400000000000000000000020551457505401100235270ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from mistraldashboard.actions import views ACTIONS = r'^(?P[^/]+)/%s$' urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path(ACTIONS % 'detail', views.DetailView.as_view(), name='detail'), re_path(ACTIONS % 'run', views.RunView.as_view(), name='run'), re_path(r'^create$', views.CreateView.as_view(), name='create'), re_path(r'^update$', views.UpdateView.as_view(), name='update'), ] mistral-dashboard-19.0.0/mistraldashboard/actions/views.py000066400000000000000000000120231457505401100236730ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views import generic from horizon import exceptions from horizon import forms from horizon import tables from mistraldashboard.actions import forms as mistral_forms from mistraldashboard.actions import tables as mistral_tables from mistraldashboard import api from mistraldashboard.default import utils class CreateView(forms.ModalFormView): template_name = 'mistral/actions/create.html' modal_header = _("Create Action") form_id = "create_action" form_class = mistral_forms.CreateForm submit_label = _("Create") submit_url = reverse_lazy("horizon:mistral:actions:create") success_url = reverse_lazy('horizon:mistral:actions:index') page_title = _("Create Action") class UpdateView(forms.ModalFormView): template_name = 'mistral/actions/update.html' modal_header = _("Update Action") form_id = "update_action" form_class = mistral_forms.UpdateForm submit_label = _("Update") submit_url = reverse_lazy("horizon:mistral:actions:update") success_url = reverse_lazy('horizon:mistral:actions:index') page_title = _("Update Action") class IndexView(tables.DataTableView): table_id = "workflow_action" table_class = mistral_tables.ActionsTable template_name = 'mistral/actions/index.html' def has_prev_data(self, table): return self._prev def has_more_data(self, table): return self._more def get_data(self): actions = [] prev_marker = self.request.GET.get( mistral_tables.ActionsTable._meta.prev_pagination_param, None ) if prev_marker is not None: sort_dir = 'asc' marker = prev_marker else: sort_dir = 'desc' marker = self.request.GET.get( mistral_tables.ActionsTable._meta.pagination_param, None ) try: actions, self._more, self._prev = api.pagination_list( entity="actions", request=self.request, marker=marker, sort_keys='name', sort_dirs=sort_dir, paginate=True, reversed_order=True ) if prev_marker is not None: actions = sorted( actions, key=lambda action: getattr( action, 'name' ), reverse=False ) except Exception: self._prev = False self._more = False msg = _('Unable to retrieve actions list.') exceptions.handle(self.request, msg) return actions class DetailView(generic.TemplateView): template_name = 'mistral/actions/detail.html' page_title = _("Action Definition") def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) action = self.get_data(self.request, **kwargs) action.is_system = utils.booleanfield(action.is_system) breadcrumb = [(action.name, reverse( 'horizon:mistral:actions:detail', args=[action.id] ))] context["custom_breadcrumb"] = breadcrumb context['action'] = action return context def get_data(self, request, **kwargs): try: action_name = kwargs['action_name'] action = api.action_get(request, action_name) except Exception: msg = _('Unable to get action "%s".') % action_name redirect = reverse('horizon:mistral:actions:index') exceptions.handle(self.request, msg, redirect=redirect) return action class RunView(forms.ModalFormView): form_class = mistral_forms.RunForm template_name = 'mistral/actions/run.html' form_id = "run_action" success_url = reverse_lazy("horizon:mistral:actions:index") submit_label = _("Run") modal_header = _("Run Action") page_title = _("Run Action") submit_url = "horizon:mistral:actions:run" def get_initial(self, **kwargs): return {'action_name': self.kwargs['action_name']} def get_context_data(self, **kwargs): context = super(RunView, self).get_context_data(**kwargs) context['submit_url'] = reverse( self.submit_url, args=[self.kwargs["action_name"]] ) return context mistral-dashboard-19.0.0/mistraldashboard/api.py000066400000000000000000000277611457505401100216660ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import itertools from django.conf import settings from django.utils.translation import gettext_lazy as _ from horizon.utils import functions as utils from horizon.utils import memoized from mistralclient.api import client as mistral_client from mistraldashboard.handle_errors import handle_errors from openstack_dashboard.api import base SERVICE_TYPE = 'workflowv2' @memoized.memoized def mistralclient(request): return mistral_client.client( username=request.user.username, auth_token=request.user.token.id, project_id=request.user.tenant_id, # We can't use auth_url param in here if we config # and use keystone federation mistral_url=base.url_for(request, 'workflowv2'), # Todo: add SECONDARY_ENDPOINT_TYPE support endpoint_type=getattr( settings, 'OPENSTACK_ENDPOINT_TYPE', 'internalURL' ), service_type=SERVICE_TYPE, # We should not treat definition as file path or uri otherwise # we allow access to contents in internal servers enforce_raw_definition=False ) @handle_errors(_("Unable to retrieve list"), []) def pagination_list(entity, request, marker='', sort_keys='', sort_dirs='asc', paginate=False, reversed_order=False, selector=None): """Retrieve a listing of specific entity and handles pagination. :param entity: Requested entity (String) :param request: Request data :param marker: Pagination marker for large data sets: entity id :param sort_keys: Columns to sort results by :param sort_dirs: Sorting Directions (asc/desc). Default:asc :param paginate: If true will perform pagination based on settings. Default:False :param reversed_order: flag to reverse list. Default:False :param selector: additional selector to allow further server filtering """ limit = getattr(settings, 'API_RESULT_LIMIT', 1000) page_size = utils.get_page_size(request) if paginate: request_size = page_size + 1 else: request_size = limit if reversed_order: sort_dirs = 'desc' if sort_dirs == 'asc' else 'asc' api = mistralclient(request) entities_iter = ( getattr(api, entity).list( selector, marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs ) if selector else ( getattr(api, entity).list( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs ) ) ) has_prev_data = has_more_data = False if paginate: entities = list(itertools.islice(entities_iter, request_size)) # first and middle page condition if len(entities) > page_size: entities.pop(-1) has_more_data = True # middle page condition if marker is not None: has_prev_data = True # first page condition when reached via prev back elif reversed_order and marker is not None: has_more_data = True # last page condition elif marker is not None: has_prev_data = True # restore the original ordering here if reversed_order: entities = sorted(entities, key=lambda ent: (getattr(ent, sort_keys) or '').lower(), reverse=(sort_dirs == 'desc') ) else: entities = list(entities_iter) return entities, has_more_data, has_prev_data def execution_create(request, **data): """Creates new execution.""" return mistralclient(request).executions.create(**data) def execution_get(request, execution_id): """Get specific execution. :param execution_id: Execution ID """ return mistralclient(request).executions.get(execution_id) def execution_update(request, execution_id, field, value): """update specific execution field, either state or description. :param request: Request data :param execution_id: Execution ID :param field: flag - either Execution state or description :param value: new update value """ if field == "state": return mistralclient(request).\ executions.update(execution_id, value) elif field == "description": return mistralclient(request).\ executions.update(execution_id, None, value) def execution_delete(request, execution_name): """Delete execution. :param execution_name: Execution name """ return mistralclient(request).executions.delete(execution_name) @handle_errors(_("Unable to retrieve tasks."), []) def task_list(request, execution_id=None): """Returns all tasks. :param execution_id: Workflow execution ID associated with list of tasks """ return mistralclient(request).tasks.list(execution_id) def task_get(request, task_id=None): """Get specific task. :param task_id: Task ID """ return mistralclient(request).tasks.get(task_id) @handle_errors(_("Unable to retrieve workflows"), []) def workflow_list(request): """Returns all workflows.""" return mistralclient(request).workflows.list() def workflow_get(request, workflow_name): """Get specific workflow. :param workflow_name: Workflow name """ return mistralclient(request).workflows.get(workflow_name) def workflow_create(request, workflows_definition): """Create workflow. :param workflows_definition: Workflows definition """ return mistralclient(request).workflows.create(workflows_definition) def workflow_validate(request, workflow_definition): """Validate workflow. :param workflow_definition: Workflow definition """ return mistralclient(request).workflows.validate(workflow_definition) def workflow_delete(request, workflow_name): """Delete workflow. :param workflow_name: Workflow name """ return mistralclient(request).workflows.delete(workflow_name) def workflow_update(request, workflows_definition): """Update workflow. :param workflows_definition: Workflows definition """ return mistralclient(request).workflows.update(workflows_definition) @handle_errors(_("Unable to retrieve workbooks."), []) def workbook_list(request): """Returns all workbooks.""" return mistralclient(request).workbooks.list() def workbook_get(request, workbook_name): """Get specific workbook. :param workbook_name: Workbook name """ return mistralclient(request).workbooks.get(workbook_name) def workbook_create(request, workbook_definition): """Create workbook. :param workbook_definition: Workbook definition """ return mistralclient(request).workbooks.create(workbook_definition) def workbook_validate(request, workbook_definition): """Validate workbook. :param workbook_definition: Workbook definition """ return mistralclient(request).workbooks.validate(workbook_definition) def workbook_delete(request, workbook_name): """Delete workbook. :param workbook_name: Workbook name """ return mistralclient(request).workbooks.delete(workbook_name) def workbook_update(request, workbook_definition): """Update workbook. :param workbook_definition: Workbook definition """ return mistralclient(request).workbooks.update(workbook_definition) @handle_errors(_("Unable to retrieve actions."), []) def action_list(request): """Returns all actions.""" return mistralclient(request).actions.list() def action_get(request, action_name): """Get specific action. :param action_name: Action name """ return mistralclient(request).actions.get(action_name) def action_create(request, action_definition): """Create action. :param action_definition: Action definition """ return mistralclient(request).actions.create(action_definition) def action_update(request, action_definition): """Update action. :param action_definition: Action definition """ return mistralclient(request).actions.update(action_definition) def action_run(request, action_name, input, params): """Run specific action execution. :param action_name: Action name :param input: input :param params: params """ return mistralclient(request).action_executions.create( action_name, input, **params ) def action_delete(request, action_name): """Delete action. :param action_name: Action name """ return mistralclient(request).actions.delete(action_name) @handle_errors(_("Unable to retrieve action executions list"), []) def action_executions_list(request, task_execution_id=None): """Returns all actions executions. :param request: Request data :param task_execution_id: (Optional) Task Execution ID to filter by """ return mistralclient(request).action_executions.list(task_execution_id) @handle_errors(_("Unable to retrieve action execution"), []) def action_execution_get(request, action_execution_id): """Get specific action execution. :param action_execution_id: Action Execution ID """ return mistralclient(request).action_executions.get(action_execution_id) @handle_errors(_("Unable to delete action execution/s"), []) def action_execution_delete(request, action_execution_id): """Delete action execution. :param action_execution_id: Action execution ID """ return mistralclient(request).action_executions.delete(action_execution_id) def action_execution_update(request, id, state=None, output=None): """Update action execution output and or state. :param id: action execution id :param output: action execution output :param state: action execution state """ return mistralclient(request).action_executions.update(id, state, output) @handle_errors(_("Unable to retrieve cron trigger list"), []) def cron_trigger_list(request): """Returns all cron triggers. :param request: Request data """ return mistralclient(request).cron_triggers.list() @handle_errors(_("Unable to retrieve cron trigger"), []) def cron_trigger_get(request, cron_trigger_name): """Get specific cron trigger. :param request: Request data :param cron_trigger_name: Cron trigger name """ return mistralclient(request).cron_triggers.get(cron_trigger_name) @handle_errors(_("Unable to delete cron trigger/s"), []) def cron_trigger_delete(request, cron_trigger_name): """Delete Cron Trigger. :param request: Request data :param cron_trigger_name: Cron Trigger name """ return mistralclient(request).cron_triggers.delete(cron_trigger_name) def cron_trigger_create( request, cron_trigger_name, workflow_ID, workflow_input, workflow_params, pattern, first_time, count ): """Create Cron Trigger. :param request: Request data :param cron_trigger_name: Cron Trigger name :param workflow_ID: Workflow ID :param workflow_input: Workflow input :param workflow_params: Workflow params <* * * * *> :param pattern: <* * * * *> :param first_time: Date and time of the first execution :param count: Number of wanted executions """ return mistralclient(request).cron_triggers.create( cron_trigger_name, workflow_ID, workflow_input, workflow_params, pattern, first_time, count ) mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/000077500000000000000000000000001457505401100233755ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/__init__.py000066400000000000000000000000001457505401100254740ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/forms.py000066400000000000000000000160651457505401100251050ustar00rootroot00000000000000# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from django.utils.translation import gettext_lazy as _ from horizon import forms from horizon import messages from mistraldashboard import api from mistraldashboard.default.utils import convert_empty_string_to_none from mistraldashboard.handle_errors import handle_errors class CreateForm(forms.SelfHandlingForm): name = forms.CharField( max_length=255, label=_("Name"), help_text=_('Cron Trigger name.'), required=True ) workflow_id = forms.ChoiceField( label=_('Workflow ID'), help_text=_('Select Workflow ID.'), widget=forms.Select( attrs={'class': 'switchable', 'data-slug': 'workflow_select'} ) ) input_source = forms.ChoiceField( label=_('Input'), help_text=_('JSON of input values defined in the workflow. ' 'Select either file or raw content.'), choices=[('file', _('File')), ('raw', _('Direct Input'))], widget=forms.Select( attrs={'class': 'switchable', 'data-slug': 'inputsource'} ) ) input_upload = forms.FileField( label=_('Input File'), help_text=_('A local input to upload.'), widget=forms.FileInput( attrs={'class': 'switched', 'data-switch-on': 'inputsource', 'data-required-when-shown': 'true', 'data-inputsource-file': _('Input File')} ), required=False ) input_data = forms.CharField( label=_('Input Data'), help_text=_('The raw contents of the input.'), widget=forms.widgets.Textarea( attrs={'class': 'switched', 'data-switch-on': 'inputsource', 'data-required-when-shown': 'true', 'data-inputsource-raw': _('Input Data'), 'rows': 4} ), required=False ) params_source = forms.ChoiceField( label=_('Params'), help_text=_('JSON of params values defined in the workflow. ' 'Select either file or raw content.'), choices=[('file', _('File')), ('raw', _('Direct Input'))], widget=forms.Select( attrs={'class': 'switchable', 'data-slug': 'paramssource'} ) ) params_upload = forms.FileField( label=_('Params File'), help_text=_('A local input to upload.'), widget=forms.FileInput( attrs={'class': 'switched', 'data-switch-on': 'paramssource', 'data-required-when-shown': 'true', 'data-paramssource-file': _('Params File')} ), required=False ) params_data = forms.CharField( label=_('Params Data'), help_text=_('The raw contents of the params.'), widget=forms.widgets.Textarea( attrs={'class': 'switched', 'data-switch-on': 'paramssource', 'data-required-when-shown': 'true', 'data-paramssource-raw': _('Params Data'), 'rows': 4} ), required=False ) first_time = forms.CharField( label=_('First Time (YYYY-MM-DD HH:MM)'), help_text=_('Date and time of the first execution.'), widget=forms.widgets.TextInput(), required=False ) schedule_count = forms.CharField( label=_('Count'), help_text=_('Number of desired executions.'), widget=forms.widgets.TextInput(), required=False ) schedule_pattern = forms.CharField( label=_('Pattern (* * * * *)'), help_text=_('Cron Trigger pattern, mind the space between each char.'), widget=forms.widgets.TextInput(), required=False ) def __init__(self, request, *args, **kwargs): super(CreateForm, self).__init__(request, *args, **kwargs) workflow_list = api.workflow_list(request) workflow_id_list = [] for wf in workflow_list: workflow_id_list.append( (wf.id, "{id} ({name})".format(id=wf.id, name=wf.name)) ) self.fields['workflow_id'].choices = workflow_id_list def clean(self): cleaned_data = super(CreateForm, self).clean() cleaned_data['input'] = "" cleaned_data['params'] = "" if cleaned_data.get('input_upload'): files = self.request.FILES cleaned_data['input'] = files['input_upload'].read() elif cleaned_data.get('input_data'): cleaned_data['input'] = cleaned_data['input_data'] del(cleaned_data['input_upload']) del(cleaned_data['input_data']) if len(cleaned_data['input']) > 0: try: cleaned_data['input'] = json.loads(cleaned_data['input']) except Exception as e: msg = _('Input is invalid JSON: %s') % str(e) raise forms.ValidationError(msg) if cleaned_data.get('params_upload'): files = self.request.FILES cleaned_data['params'] = files['params_upload'].read() elif cleaned_data.get('params_data'): cleaned_data['params'] = cleaned_data['params_data'] del(cleaned_data['params_upload']) del(cleaned_data['params_data']) if len(cleaned_data['params']) > 0: try: cleaned_data['params'] = json.loads(cleaned_data['params']) except Exception as e: msg = _('Params is invalid JSON: %s') % str(e) raise forms.ValidationError(msg) return cleaned_data @handle_errors(_("Unable to create Cron Trigger"), []) def handle(self, request, data): data['input'] = convert_empty_string_to_none(data['input']) data['params'] = convert_empty_string_to_none(data['params']) data['schedule_pattern'] = convert_empty_string_to_none( data['schedule_pattern'] ) data['first_time'] = convert_empty_string_to_none(data['first_time']) data['schedule_count'] = convert_empty_string_to_none( data['schedule_count'] ) api.cron_trigger_create( request, data['name'], data['workflow_id'], data['input'], data['params'], data['schedule_pattern'], data['first_time'], data['schedule_count'], ) msg = _('Successfully created Cron Trigger.') messages.success(request, msg) return True mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/panel.py000066400000000000000000000014711457505401100250510ustar00rootroot00000000000000# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ import horizon from mistraldashboard import dashboard class CronTriggers(horizon.Panel): name = _("Cron Triggers") slug = 'cron_triggers' dashboard.MistralDashboard.register(CronTriggers) mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/tables.py000066400000000000000000000061731457505401100252300ustar00rootroot00000000000000# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from horizon import tables from mistraldashboard import api from mistraldashboard.default.utils import humantime class CreateCronTrigger(tables.LinkAction): name = "create" verbose_name = _("Create Cron Trigger") url = "horizon:mistral:cron_triggers:create" classes = ("ajax-modal",) icon = "plus" class DeleteCronTrigger(tables.DeleteAction): @staticmethod def action_present(count): return ngettext_lazy( u"Delete Cron Trigger", u"Delete Cron Triggers", count ) @staticmethod def action_past(count): return ngettext_lazy( u"Deleted Cron Trigger", u"Deleted Cron Triggers", count ) def delete(self, request, cron_trigger_name): api.cron_trigger_delete(request, cron_trigger_name) class WorkflowColumn(tables.Column): def get_link_url(self, datum): workflow_url = "horizon:mistral:workflows:detail" obj_id = datum.workflow_name return reverse(workflow_url, args=[obj_id]) class CronTriggersTable(tables.DataTable): id = tables.Column( "id", verbose_name=_("ID"), link="horizon:mistral:cron_triggers:detail" ) name = tables.Column( "name", verbose_name=_("Name") ) workflow_name = WorkflowColumn( "workflow_name", verbose_name=_("Workflow"), link=True ) pattern = tables.Column( "pattern", verbose_name=_("Pattern"), ) next_execution_time = tables.Column( "next_execution_time", verbose_name=_("Next Execution Time"), ) remaining_executions = tables.Column( "remaining_executions", verbose_name=_("Remaining Executions"), ) first_execution_time = tables.Column( "first_execution_time", verbose_name=_("First Execution Time"), ) created_at = tables.Column( "created_at", verbose_name=_("Created at"), filters=[humantime] ) updated_at = tables.Column( "updated_at", verbose_name=_("Updated at"), filters=[humantime] ) def get_object_id(self, datum): return datum.name class Meta(object): name = "cron trigger" verbose_name = _("Cron Trigger") table_actions = ( tables.FilterAction, CreateCronTrigger, DeleteCronTrigger ) row_actions = (DeleteCronTrigger,) mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/templates/000077500000000000000000000000001457505401100253735ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/templates/cron_triggers/000077500000000000000000000000001457505401100302425ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/templates/cron_triggers/_create.html000066400000000000000000000027121457505401100325340ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block form_attrs %}enctype="multipart/form-data"{% endblock %} {% block modal-body-right %}

{% trans "Description" %}:

{% blocktrans %} Cron Trigger is an object allowing to run workflow on a schedule. {% endblocktrans %}

{% blocktrans %} Using Cron Triggers it is possible to run workflows according to specific rules: periodically setting a pattern or on external events like Ceilometer alarm. {% endblocktrans %}

{% trans "For more info" %}:


{% trans "Please note" %}:
{% blocktrans %} Name, Workflow ID and Pattern are mandatory fields. {% endblocktrans %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/templates/cron_triggers/create.html000066400000000000000000000002241457505401100323710ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% load static %} {% block main %} {% include 'mistral/cron_triggers/_create.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/templates/cron_triggers/detail.html000066400000000000000000000065041457505401100323770ustar00rootroot00000000000000 {% extends 'mistral/default/base.html' %} {% load i18n %} {% block title %}{% trans "Cron Trigger Details" %}{% endblock %} {% block page_header %}

{% trans "Cron Trigger Details" %}

{% endblock page_header %} {% block main %} {% load i18n sizeformat %}

{% trans "Overview" %}


{% trans "Name" %}
{{ cron_trigger.name }}
{% trans "ID" %}
{{ cron_trigger.id }}
{% trans "Pattern" %}
{{ cron_trigger.pattern }}

{% trans "Creation Date" %}
{{ cron_trigger.created_at|parse_isotime }}
{% trans "Time Since Created" %}
{{ cron_trigger.created_at|parse_isotime|timesince }}

{% trans "First Execution" %}
{% with time=cron_trigger.first_execution_time %} {{ time|parse_isotime|default:"Empty" }} {% endwith %}
{% trans "Time Since first Execution" %}
{% with time=cron_trigger.first_execution_time %} {{ time|parse_isotime|timesince|default:"Empty" }} {% endwith %}

{% trans "Update Date" %}
{{ cron_trigger.updated_at|parse_isotime|default:"Empty" }}
{% trans "Time Since Updated" %}
{% with time=cron_trigger.updated_at %} {{ time|parse_isotime|timesince|default:"Empty" }} {% endwith %}

{% trans "Next Execution Time" %}
{% with time=cron_trigger.next_execution_time %} {{ time|parse_isotime|default:"Empty" }} {% endwith %}
{% trans "Time Until Next Execution" %}
{% with time=cron_trigger.next_execution_time %} {{ time|parse_isotime|timeuntil|default:"Empty" }} {% endwith %}
{% trans "Remaining Executions" %}
{{ cron_trigger.remaining_executions|default:"0"}}

{% trans "Workflow" %}


{% trans "Workflow Name" %}
{{ cron_trigger.workflow_name }}
{% endblock %}
mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/templates/cron_triggers/index.html000066400000000000000000000004001457505401100322310ustar00rootroot00000000000000{% extends 'mistral/default/table.html' %} {% load i18n %} {% block title %} {% trans "Cron Triggers" %} {% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Cron Triggers")%} {% endblock page_header %} mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/tests.py000066400000000000000000000060751457505401100251210ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.urls import reverse from openstack_dashboard.test import helpers from mistraldashboard import api from mistraldashboard.test import helpers as test INDEX_URL = reverse('horizon:mistral:cron_triggers:index') class CronTriggersTest(test.TestCase): @helpers.create_mocks({api: ('cron_trigger_list',)}) def test_index(self): self.mock_cron_trigger_list.return_value =\ self.mistralclient_cron_triggers.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'mistral/cron_triggers/index.html') self.mock_cron_trigger_list.assert_called_once_with( helpers.IsHttpRequest()) @helpers.create_mocks({api: ('cron_trigger_create', 'workflow_list')}) def test_create_post(self): cron_trigger = self.mistralclient_cron_triggers.first() workflows = self.mistralclient_workflows.list() self.mock_cron_trigger_create.return_value = cron_trigger self.mock_workflow_list.return_value = workflows url = reverse("horizon:mistral:cron_triggers:create") form_data = { 'name': cron_trigger.name, 'workflow_id': '1', 'input_source': 'raw', 'input_data': '{"a":"b"}', 'params_source': 'raw', 'params_data': '{"a":"b"}', 'schedule_pattern': cron_trigger.pattern, 'first_time': cron_trigger.first_execution_time, 'schedule_count': '1' } res = self.client.post(url, form_data) self.assertNoFormErrors(res) self.mock_cron_trigger_create.assert_called_once_with( helpers.IsHttpRequest(), cron_trigger.name, form_data["workflow_id"], {u'a': u'b'}, {u'a': u'b'}, None, None, form_data["schedule_count"] ) self.mock_workflow_list.assert_called_once_with( helpers.IsHttpRequest()) @helpers.create_mocks({api: ('cron_trigger_get',)}) def test_detail(self): cron_trigger = self.mistralclient_cron_triggers.list()[0] self.mock_cron_trigger_get.return_value = cron_trigger url = reverse('horizon:mistral:cron_triggers:detail', args=[cron_trigger.id]) res = self.client.get(url) self.assertTemplateUsed(res, 'mistral/cron_triggers/detail.html') self.mock_cron_trigger_get.assert_called_once_with( helpers.IsHttpRequest(), cron_trigger.id) mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/urls.py000066400000000000000000000016661457505401100247450ustar00rootroot00000000000000# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from mistraldashboard.cron_triggers import views CRON_TRIGGERS = r'^(?P[^/]+)/%s$' urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path(CRON_TRIGGERS % 'detail', views.OverviewView.as_view(), name='detail'), re_path(r'^create$', views.CreateView.as_view(), name='create'), ] mistral-dashboard-19.0.0/mistraldashboard/cron_triggers/views.py000066400000000000000000000052241457505401100251070ustar00rootroot00000000000000# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views import generic from horizon import forms from horizon import tables from mistraldashboard import api from mistraldashboard.cron_triggers import forms as mistral_forms from mistraldashboard.cron_triggers import tables as mistral_tables class OverviewView(generic.TemplateView): template_name = 'mistral/cron_triggers/detail.html' page_title = _("Cron Trigger Details") workflow_url = 'horizon:mistral:workflows:detail' list_url = 'horizon:mistral:cron_triggers:index' def get_context_data(self, **kwargs): context = super(OverviewView, self).get_context_data(**kwargs) cron_trigger = {} cron_trigger = api.cron_trigger_get( self.request, kwargs['cron_trigger_name'] ) cron_trigger.workflow_url = reverse( self.workflow_url, args=[cron_trigger.workflow_name] ) cron_trigger.list_url = reverse_lazy(self.list_url) breadcrumb = [(cron_trigger.name, reverse( 'horizon:mistral:cron_triggers:detail', args=[cron_trigger.name] ))] context["custom_breadcrumb"] = breadcrumb context['cron_trigger'] = cron_trigger return context class CreateView(forms.ModalFormView): template_name = 'mistral/cron_triggers/create.html' modal_header = _("Create Cron Trigger") form_id = "create_cron_trigger" form_class = mistral_forms.CreateForm submit_label = _("Create Cron Trigger") submit_url = reverse_lazy("horizon:mistral:cron_triggers:create") success_url = reverse_lazy('horizon:mistral:cron_triggers:index') page_title = _("Create Cron Trigger") def get_form_kwargs(self): kwargs = super(CreateView, self).get_form_kwargs() return kwargs class IndexView(tables.DataTableView): table_class = mistral_tables.CronTriggersTable template_name = 'mistral/cron_triggers/index.html' def get_data(self): return api.cron_trigger_list(self.request) mistral-dashboard-19.0.0/mistraldashboard/dashboard.py000066400000000000000000000021261457505401100230300ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ import horizon from mistraldashboard.default import panel class MistralDashboard(horizon.Dashboard): name = _("Workflow") slug = "mistral" panels = ( 'default', 'workbooks', 'workflows', 'actions', 'executions', 'tasks', 'action_executions', 'cron_triggers', ) default_panel = 'default' roles = ('admin',) horizon.register(MistralDashboard) MistralDashboard.register(panel.Default) mistral-dashboard-19.0.0/mistraldashboard/default/000077500000000000000000000000001457505401100221525ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/default/__init__.py000066400000000000000000000000001457505401100242510ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/default/panel.py000066400000000000000000000014241457505401100236240ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ import horizon class Default(horizon.Panel): name = _("Default") slug = 'default' urls = 'mistraldashboard.workbooks.urls' nav = False mistral-dashboard-19.0.0/mistraldashboard/default/smart_cell.py000066400000000000000000000060471457505401100246600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright © 2015 - Alcatel-Lucent # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """File Overrides OpenStack Horizon Cell method to return the whole row data using cell_attributes_getter""" from django import forms from django import template from horizon.tables import base def get_data(self, datum, column, row): """Fetches the data to be displayed in this cell.""" table = row.table if column.auto == "multi_select": data = "" if row.can_be_selected(datum): widget = forms.CheckboxInput(check_test=lambda value: False) # Convert value to string to avoid accidental type conversion data = widget.render('object_ids', str(table.get_object_id(datum)), {'class': 'table-row-multi-select'}) table._data_cache[column][table.get_object_id(datum)] = data elif column.auto == "form_field": widget = column.form_field if issubclass(widget.__class__, forms.Field): widget = widget.widget widget_name = "%s__%s" % \ (column.name, str(table.get_object_id(datum))) # Create local copy of attributes, so it don't change column # class form_field_attributes form_field_attributes = {} form_field_attributes.update(column.form_field_attributes) # Adding id of the input so it pairs with label correctly form_field_attributes['id'] = widget_name if (template.defaultfilters.urlize in column.filters or template.defaultfilters.yesno in column.filters): data = widget.render(widget_name, column.get_raw_data(datum), form_field_attributes) else: data = widget.render(widget_name, column.get_data(datum), form_field_attributes) table._data_cache[column][table.get_object_id(datum)] = data elif column.auto == "actions": data = table.render_row_actions(datum) table._data_cache[column][table.get_object_id(datum)] = data else: data = column.get_data(datum) if column.cell_attributes_getter: # Following line is the change: cell_attributes_getter called with # "datum" instead of "data" cell_attributes = column.cell_attributes_getter(datum) or {} self.attrs.update(cell_attributes) return data def init(): base.Cell.get_data = get_data mistral-dashboard-19.0.0/mistraldashboard/default/templates/000077500000000000000000000000001457505401100241505ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/default/templates/default/000077500000000000000000000000001457505401100255745ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/default/templates/default/_booleanfield.html000066400000000000000000000001701457505401100312420ustar00rootroot00000000000000 {{ bool }} mistral-dashboard-19.0.0/mistraldashboard/default/templates/default/_code.html000066400000000000000000000006051457505401100275340ustar00rootroot00000000000000{% extends "horizon/common/_modal.html" %} {% block modal-header %} {{ io.name }} {% endblock %} {% block modal-body %}
{{ io.value }}
{% endblock %} {% block modal-footer %} {{ cancel_label }} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/default/templates/default/_humantime.html000066400000000000000000000001371457505401100306110ustar00rootroot00000000000000{% load humanize %} mistral-dashboard-19.0.0/mistraldashboard/default/templates/default/_label.html000066400000000000000000000000621457505401100276760ustar00rootroot00000000000000{{ label }} mistral-dashboard-19.0.0/mistraldashboard/default/templates/default/_preprint.html000066400000000000000000000000331457505401100304600ustar00rootroot00000000000000
    {{ pre }}
mistral-dashboard-19.0.0/mistraldashboard/default/templates/default/_prettyprint.html000066400000000000000000000001371457505401100312260ustar00rootroot00000000000000
{{ full }}
mistral-dashboard-19.0.0/mistraldashboard/default/templates/default/base.html000066400000000000000000000003371457505401100273770ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block css %} {% include "_stylesheets.html" %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/default/templates/default/table.html000066400000000000000000000002171457505401100275510ustar00rootroot00000000000000{% extends 'mistral/default/base.html' %} {% block main %}
{{ table.render }}
{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/default/utils.py000066400000000000000000000045121457505401100236660ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.template.loader import render_to_string import iso8601 import json TYPES = { 'SUCCESS': 'label-success', 'ERROR': 'label-danger', 'DELAYED': 'label-default', 'PAUSED': 'label-primary', 'RUNNING': 'label-info', } BOOLEAN_FIELD = { 'True': { 'color': 'green', 'icon': 'fa fa-check' }, 'False': { 'color': 'red', 'icon': 'fa fa-remove' } } def label(x): return render_to_string("mistral/default/_label.html", {"label": x, "type": TYPES.get(x)}) def booleanfield(x): # todo: check undefined instead of the if blocks in view # todo: check the red version return render_to_string("mistral/default/_booleanfield.html", {"bool": str(x), "type": BOOLEAN_FIELD.get(str(x))}) def humantime(x): return render_to_string("mistral/default/_humantime.html", {"datetime": iso8601.parse_date(x)}) def prettyprint(x): short = None full = json.dumps(json.loads(x), indent=4, ensure_ascii=False) lines = full.split('\n') if (len(lines) > 5): short = '\n'.join(lines[:5] + ['...']) return render_to_string("mistral/default/_prettyprint.html", {"full": full, "short": short}) def htmlpre(pre): return render_to_string("mistral/default/_preprint.html", {"pre": pre}) def convert_empty_string_to_none(str): """Returns None if given string is empty. Empty string is default for Django form empty HTML input. python-mistral-client does not handle empty strings, only "None" type. :param str: string variable """ return str if len(str) != 0 else None mistral-dashboard-19.0.0/mistraldashboard/enabled/000077500000000000000000000000001457505401100221205ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/enabled/_50_mistral.py000066400000000000000000000015301457505401100246070ustar00rootroot00000000000000# Copyright (c) 2012 OpenStack Foundation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from mistraldashboard import exceptions DASHBOARD = 'mistral' ADD_INSTALLED_APPS = ['mistraldashboard'] DEFAULT = False ADD_EXCEPTIONS = { 'recoverable': exceptions.RECOVERABLE, 'not_found': exceptions.NOT_FOUND, 'unauthorized': exceptions.UNAUTHORIZED, } mistral-dashboard-19.0.0/mistraldashboard/enabled/__init__.py000066400000000000000000000000001457505401100242170ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/exceptions.py000066400000000000000000000015011457505401100232560ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from mistralclient.api import base from openstack_dashboard import exceptions NOT_FOUND = exceptions.NOT_FOUND RECOVERABLE = exceptions.RECOVERABLE + (base.APIException,) UNAUTHORIZED = exceptions.UNAUTHORIZED mistral-dashboard-19.0.0/mistraldashboard/executions/000077500000000000000000000000001457505401100227145ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/executions/__init__.py000066400000000000000000000000001457505401100250130ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/executions/forms.py000066400000000000000000000032031457505401100244120ustar00rootroot00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.utils.translation import gettext_lazy as _ from horizon import exceptions from horizon import forms from horizon import messages from mistraldashboard import api class UpdateDescriptionForm(forms.SelfHandlingForm): execution_id = forms.CharField(label=_("Execution ID"), widget=forms.HiddenInput(), required=False) description = forms.CharField(max_length=255, label=_("Execution description")) def handle(self, request, data): try: api.execution_update( request, data["execution_id"], "description", data["description"]) msg = _('Successfully updated execution description.') messages.success(request, msg) return True except Exception: msg = _('Failed to update execution description.') redirect = reverse('horizon:mistral:executions:index') exceptions.handle(request, msg, redirect=redirect) mistral-dashboard-19.0.0/mistraldashboard/executions/panel.py000066400000000000000000000015021457505401100243630ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ import horizon from mistraldashboard import dashboard class Executions(horizon.Panel): name = _("Workflow Executions") slug = 'executions' dashboard.MistralDashboard.register(Executions) mistral-dashboard-19.0.0/mistraldashboard/executions/tables.py000066400000000000000000000140761457505401100245500ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from horizon import exceptions from horizon import tables from mistraldashboard import api from mistraldashboard.default import smart_cell from mistraldashboard.default.utils import humantime from mistraldashboard.default.utils import label smart_cell.init() class DeleteExecution(tables.DeleteAction): @staticmethod def action_present(count): return ngettext_lazy( u"Delete Execution", u"Delete Executions", count ) @staticmethod def action_past(count): return ngettext_lazy( u"Deleted Execution", u"Deleted Executions", count ) def delete(self, request, execution_name): api.execution_delete(request, execution_name) class CancelExecution(tables.BatchAction): name = "cancel execution" classes = ("btn-danger",) @staticmethod def action_present(count): return ngettext_lazy( u"Cancel Execution", u"Cancel Executions", count ) @staticmethod def action_past(count): return ngettext_lazy( u"Canceled Execution", u"Canceled Executions", count ) def allowed(self, request, instance): if instance.state == "RUNNING": return True return False def action(self, request, obj_id): api.execution_update(request, obj_id, "state", "ERROR") class PauseExecution(tables.BatchAction): name = "pause execution" @staticmethod def action_present(count): return ngettext_lazy( u"Pause Execution", u"Pause Executions", count ) @staticmethod def action_past(count): return ngettext_lazy( u"Paused Execution", u"Paused Executions", count ) def allowed(self, request, instance): if instance.state == "RUNNING": return True return False def action(self, request, obj_id): api.execution_update(request, obj_id, "state", "PAUSED") class ResumeExecution(tables.BatchAction): name = "resume execution" @staticmethod def action_present(count): return ngettext_lazy( u"Resume Execution", u"Resume Executions", count ) @staticmethod def action_past(count): return ngettext_lazy( u"Resumed Execution", u"Resumed Executions", count ) def allowed(self, request, instance): if instance.state == "PAUSED": return True return False def action(self, request, obj_id): api.execution_update(request, obj_id, "state", "RUNNING") class UpdateDescription(tables.LinkAction): name = "updateDescription" verbose_name = _("Update Description") url = "horizon:mistral:executions:update_description" classes = ("ajax-modal",) class UpdateRow(tables.Row): ajax = True def get_data(self, request, id): try: instance = api.execution_get(request, id) except Exception: msg = _('Unable to get execution by ID "%s".') % id exceptions.handle(request, msg) return instance class ExecutionsTable(tables.DataTable): def getHoverHelp(data): if hasattr(data, 'state_info') and data.state_info: return {'title': data.state_info} STATE_STATUS_CHOICES = ( ("success", True), ("error", False), ("paused", False), ("delayed", None), ("running", None), ) STATUS_DISPLAY_CHOICES = ( ("success", _("Success")), ("error", _("Error")), ("paused", _("Paused")), ("delayed", _("Delayed")), ("running", _("Running")), ) id = tables.Column( "id", verbose_name=_("ID"), link="horizon:mistral:executions:detail" ) workflow_name = tables.Column( "workflow_name", verbose_name=_("Workflow") ) task = tables.Column( "task", verbose_name=_("Tasks"), empty_value=_("View"), link="horizon:mistral:tasks:execution" ) input = tables.Column( "", verbose_name=_("Input"), empty_value=_("View"), link="horizon:mistral:executions:input", link_classes=("ajax-modal",) ) output = tables.Column( "", verbose_name=_("Output"), empty_value=_("View"), link="horizon:mistral:executions:output", link_classes=("ajax-modal",) ) created_at = tables.Column( "created_at", verbose_name=_("Created at"), filters=[humantime] ) updated_at = tables.Column( "updated_at", verbose_name=_("Updated at"), filters=[humantime] ) state = tables.Column( "state", verbose_name=_("State"), filters=[label], status=True, status_choices=STATE_STATUS_CHOICES, display_choices=STATUS_DISPLAY_CHOICES, cell_attributes_getter=getHoverHelp ) class Meta(object): name = "executions" verbose_name = _("Executions") status_columns = ["state"] row_class = UpdateRow table_actions = (DeleteExecution, tables.FilterAction) row_actions = (DeleteExecution, UpdateDescription, PauseExecution, CancelExecution, ResumeExecution, DeleteExecution) mistral-dashboard-19.0.0/mistraldashboard/executions/templates/000077500000000000000000000000001457505401100247125ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/executions/templates/executions/000077500000000000000000000000001457505401100271005ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/executions/templates/executions/_update_description.html000066400000000000000000000003121457505401100340060ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Enter execution description." %}

{% endblock %}mistral-dashboard-19.0.0/mistraldashboard/executions/templates/executions/detail.html000066400000000000000000000046411457505401100312350ustar00rootroot00000000000000 {% extends 'mistral/default/base.html' %} {% load i18n %} {% block title %}{% trans "Execution Details" %}{% endblock %} {% block page_header %}

{% trans "Execution Details" %}

{% endblock page_header %} {% block main %} {% load i18n sizeformat %}

{% trans "Overview" %}


{% trans "ID" %}
{{ execution.id|default:_("None") }}
{% if execution.description %}
{% trans "Description" %}
{{ execution.description }}
{% endif %}
{% trans "State" %}
{{ execution.state }}
{% if execution.state_info %}
{% trans "State Info" %}
{{ execution.state_info }}
{% endif %}
{% trans "Worflow Name" %}
{{ execution.workflow_name }}
{% trans "Tasks" %}
{% trans "view corresponding tasks" %}

{% trans "Creation Date" %}
{{ execution.created_at|parse_isotime}}
{% trans "Time Since Created" %}
{{ execution.created_at|parse_isotime|timesince }}

{% trans "Update Date" %}
{{ execution.updated_at|parse_isotime}}
{% trans "Time Since Updated" %}
{{ execution.updated_at|parse_isotime|timesince }}

{% trans "Params" %}
{{ execution.params }}
{% trans "Input" %}
{{ execution.input }}
{% trans "Output" %}
{{ execution.output }}
{% endblock %}
mistral-dashboard-19.0.0/mistraldashboard/executions/templates/executions/index.html000066400000000000000000000004071457505401100310760ustar00rootroot00000000000000{% extends 'mistral/default/table.html' %} {% load i18n %} {% block title %}{% trans "Workflow Executions" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Workflow Executions") %} {% endblock page_header %} mistral-dashboard-19.0.0/mistraldashboard/executions/templates/executions/index_filtered_task.html000066400000000000000000000004061457505401100337750ustar00rootroot00000000000000{% extends 'mistral/default/table.html' %} {% load i18n %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Workflow Executions of Task ID:") %}

{{ task_execution_id }}

{% endblock page_header %} mistral-dashboard-19.0.0/mistraldashboard/executions/templates/executions/update_description.html000066400000000000000000000004211457505401100336500ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" %} {% endblock page_header %} {% block main %} {% include 'mistral/executions/_update_description.html' %} {% endblock %}mistral-dashboard-19.0.0/mistraldashboard/executions/tests.py000066400000000000000000000055621457505401100244400ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.urls import reverse from openstack_dashboard.test import helpers from mistraldashboard import api from mistraldashboard.test import helpers as test INDEX_URL = reverse('horizon:mistral:executions:index') class ExecutionsTest(test.TestCase): @helpers.create_mocks({api: ('pagination_list',)}) def test_index(self): self.mock_pagination_list.return_value =\ [self.mistralclient_executions.list(), False, False] res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'mistral/executions/index.html') self.assertCountEqual(res.context['table'].data, self.mistralclient_executions.list()) self.mock_pagination_list.assert_called_once_with( entity="executions", request=helpers.IsHttpRequest(), marker=None, sort_dirs='desc', paginate=True) @helpers.create_mocks({api: ('execution_update',)}) def test_update_post(self): execution = self.mistralclient_executions.first() self.mock_execution_update.return_value = execution form_data = { "execution_id": execution.id, "description": "description"} res = self.client.post( reverse('horizon:mistral:executions:update_description', args=(execution.id,)), form_data) self.assertNoFormErrors(res) self.mock_execution_update.assert_called_once_with( helpers.IsHttpRequest(), execution.id, "description", "description") @helpers.create_mocks({api: ('execution_get', 'task_list')}) def test_detail(self): execution = self.mistralclient_executions.list()[0] tasks = self.mistralclient_tasks.list() self.mock_execution_get.return_value = execution self.mock_task_list.return_value = tasks url = reverse('horizon:mistral:executions:detail', args=[execution.id]) res = self.client.get(url) self.assertTemplateUsed(res, 'mistral/executions/detail.html') self.mock_execution_get.assert_called_once_with( helpers.IsHttpRequest(), execution.id) self.mock_task_list.assert_called_once_with( helpers.IsHttpRequest(), execution.id) mistral-dashboard-19.0.0/mistraldashboard/executions/urls.py000066400000000000000000000026671457505401100242660ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from mistraldashboard.executions import views EXECUTIONS = r'^(?P[^/]+)/%s$' TASKS = r'^(?P[^/]+)/%s$' urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path(EXECUTIONS % 'detail', views.DetailView.as_view(), name='detail'), re_path(TASKS % 'tasks', views.TasksView.as_view(), name='tasks'), re_path(EXECUTIONS % 'detail_task_id', views.DetailView.as_view(), {'caller': 'task'}, name='detail_task_id'), re_path(EXECUTIONS % 'output', views.CodeView.as_view(), {'column': 'output'}, name='output'), re_path(EXECUTIONS % 'input', views.CodeView.as_view(), {'column': 'input'}, name='input'), re_path(EXECUTIONS % 'update_description', views.UpdateDescriptionView.as_view(), name='update_description'), ] mistral-dashboard-19.0.0/mistraldashboard/executions/views.py000066400000000000000000000220231457505401100244220ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.views import generic from django.urls import reverse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from horizon import exceptions from horizon import forms from horizon import tables from mistraldashboard import api from mistraldashboard.default import utils from mistraldashboard.executions import forms as m_forms from mistraldashboard.executions import tables as mistral_tables from mistraldashboard import forms as mistral_forms def get_single_data(request, id, type="execution"): """Get Execution or Task data by ID. :param request: Request data :param id: Entity ID :param type: Request dispatch flag, Default: Execution """ if type == "execution": try: execution = api.execution_get(request, id) except Exception: msg = _('Unable to get execution by its ID"%s".') % id redirect = reverse('horizon:mistral:executions:index') exceptions.handle(request, msg, redirect=redirect) return execution elif type == "task": try: task = api.task_get(request, id) except Exception: msg = _('Unable to get task by its ID "%s".') % id redirect = reverse('horizon:mistral:tasks:index') exceptions.handle(request, msg, redirect=redirect) return task elif type == "task_by_execution": try: task = api.task_list(request, id)[0] except Exception: msg = _('Unable to get task by Execution ID "%s".') % id redirect = reverse('horizon:mistral:executions:index') exceptions.handle(request, msg, redirect=redirect) return task class IndexView(tables.DataTableView): table_class = mistral_tables.ExecutionsTable template_name = 'mistral/executions/index.html' def has_prev_data(self, table): return self._prev def has_more_data(self, table): return self._more def get_data(self): executions = [] prev_marker = self.request.GET.get( mistral_tables.ExecutionsTable._meta.prev_pagination_param, None ) if prev_marker is not None: sort_dir = 'asc' marker = prev_marker else: sort_dir = 'desc' marker = self.request.GET.get( mistral_tables.ExecutionsTable._meta.pagination_param, None ) try: executions, self._more, self._prev = api.pagination_list( entity="executions", request=self.request, marker=marker, sort_dirs=sort_dir, paginate=True ) if prev_marker is not None: executions = sorted( executions, key=lambda execution: getattr( execution, 'created_at' ), reverse=True ) except Exception: self._prev = False self._more = False msg = _('Unable to retrieve executions list.') exceptions.handle(self.request, msg) return executions class TasksView(tables.DataTableView): table_class = mistral_tables.ExecutionsTable template_name = 'mistral/executions/index_filtered_task.html' def has_prev_data(self, table): return self._prev def has_more_data(self, table): return self._more def get_data(self): executions = [] prev_marker = self.request.GET.get( mistral_tables.ExecutionsTable._meta.prev_pagination_param, None ) if prev_marker is not None: sort_dir = 'asc' marker = prev_marker else: sort_dir = 'desc' marker = self.request.GET.get( mistral_tables.ExecutionsTable._meta.pagination_param, None ) try: executions, self._more, self._prev = api.pagination_list( entity="executions", request=self.request, marker=marker, sort_dirs=sort_dir, paginate=True, selector=self.kwargs['task_execution_id'] ) if prev_marker is not None: executions = sorted( executions, key=lambda execution: getattr( execution, 'created_at' ), reverse=True ) except Exception: self._prev = False self._more = False msg = _('Unable to retrieve executions list of ' 'the requested task.') exceptions.handle(self.request, msg) return executions class DetailView(generic.TemplateView): template_name = 'mistral/executions/detail.html' page_title = _("Execution Overview") workflow_url = 'horizon:mistral:workflows:detail' task_url = 'horizon:mistral:tasks:execution' def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) task = {} execution = {} if 'caller' in kwargs: if kwargs['caller'] == 'task': kwargs['task_id'] = kwargs['execution_id'] del kwargs['execution_id'] task = get_single_data( self.request, kwargs['task_id'], "task" ) execution = get_single_data( self.request, task.workflow_execution_id, ) else: execution = get_single_data( self.request, kwargs['execution_id'], ) task = get_single_data( self.request, self.kwargs['execution_id'], "task_by_execution" ) execution.workflow_url = reverse(self.workflow_url, args=[execution.workflow_name]) execution.input = utils.prettyprint(execution.input) execution.output = utils.prettyprint(execution.output) execution.params = utils.prettyprint(execution.params) execution.state = utils.label(execution.state) task.url = reverse(self.task_url, args=[execution.id]) breadcrumb = [(execution.id, reverse( 'horizon:mistral:executions:detail', args=[execution.id] ))] context["custom_breadcrumb"] = breadcrumb context['execution'] = execution context['task'] = task return context class CodeView(forms.ModalFormView): template_name = 'mistral/default/code.html' modal_header = _("Code view") form_id = "code_view" form_class = mistral_forms.EmptyForm cancel_label = "OK" cancel_url = reverse_lazy("horizon:mistral:executions:index") page_title = _("Code view") def get_context_data(self, **kwargs): context = super(CodeView, self).get_context_data(**kwargs) execution = get_single_data( self.request, self.kwargs['execution_id'], ) column = self.kwargs['column'] io = {} if column == 'input': io['name'] = _('Input') io['value'] = execution.input = utils.prettyprint(execution.input) elif column == 'output': io['name'] = _('Output') io['value'] = execution.output = utils.prettyprint( execution.output ) context['io'] = io return context class UpdateDescriptionView(forms.ModalFormView): template_name = 'mistral/executions/update_description.html' modal_header = _("Update Execution Description") form_id = "update_execution_description" form_class = m_forms.UpdateDescriptionForm submit_label = _("Update") success_url = reverse_lazy("horizon:mistral:executions:index") submit_url = "horizon:mistral:executions:update_description" cancel_url = "horizon:mistral:executions:index" page_title = _("Update Execution Description") def get_initial(self): return {"execution_id": self.kwargs["execution_id"]} def get_context_data(self, **kwargs): context = super(UpdateDescriptionView, self).get_context_data(**kwargs) context['submit_url'] = reverse( self.submit_url, args=[self.kwargs["execution_id"]] ) return context mistral-dashboard-19.0.0/mistraldashboard/forms.py000066400000000000000000000012261457505401100222270ustar00rootroot00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from horizon import forms class EmptyForm(forms.SelfHandlingForm): def handle(self, request, data): pass mistral-dashboard-19.0.0/mistraldashboard/handle_errors.py000066400000000000000000000056401457505401100237340ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import functools import inspect import horizon.exceptions def handle_errors(error_message, error_default=None, request_arg=None): """A decorator for adding default error handling to API calls. It wraps the original method in a try-except block, with horizon's error handling added. Note: it should only be used on functions or methods that take request as their argument (it has to be named "request", or ``request_arg`` has to be provided, indicating which argument is the request). The decorated method accepts a number of additional parameters: :param _error_handle: whether to handle the errors in this call :param _error_message: override the error message :param _error_default: override the default value returned on error :param _error_redirect: specify a redirect url for errors :param _error_ignore: ignore known errors """ def decorator(func): if request_arg is None: _request_arg = 'request' if _request_arg not in inspect.getfullargspec(func).args: raise RuntimeError( "The handle_errors decorator requires 'request' as " "an argument of the function or method being decorated") else: _request_arg = request_arg @functools.wraps(func) def wrapper(*args, **kwargs): _error_handle = kwargs.pop('_error_handle', True) _error_message = kwargs.pop('_error_message', error_message) _error_default = kwargs.pop('_error_default', error_default) _error_redirect = kwargs.pop('_error_redirect', None) _error_ignore = kwargs.pop('_error_ignore', False) if not _error_handle: return func(*args, **kwargs) try: return func(*args, **kwargs) except Exception as e: callargs = inspect.getcallargs(func, *args, **kwargs) request = callargs[_request_arg] _error_message += ': ' + str(e) horizon.exceptions.handle(request, _error_message, ignore=_error_ignore, redirect=_error_redirect) return _error_default wrapper.wrapped = func return wrapper return decorator mistral-dashboard-19.0.0/mistraldashboard/static/000077500000000000000000000000001457505401100220155ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/static/mistraldashboard/000077500000000000000000000000001457505401100253405ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/static/mistraldashboard/css/000077500000000000000000000000001457505401100261305ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/static/mistraldashboard/css/style.css000066400000000000000000000012021457505401100277750ustar00rootroot00000000000000.mistral-wrapper.list{ list-style: inherit; } .mistral-wrapper #actions{ width:100%; } .mistral-wrapper #actions a.btn{ width:initial; } .mistral-wrapper.detail-screen .page-breadcrumb ol li{ max-width: inherit; } .mistral-wrapper.detail-screen .page-breadcrumb li:last-child{ display:none; } .mistral-wrapper .navbar-brand{ padding: 6px 10px; } .boolfield{ font-style: italic; } .boolfield i{ padding-right: .2em; } .boolfield i.green{ color: green; } .boolfield i.red{ color: red; } .line-space{ margin: .3em 0; } .line-space dd{ display:inline-block; margin-left: 1.5em; } mistral-dashboard-19.0.0/mistraldashboard/tasks/000077500000000000000000000000001457505401100216535ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/tasks/__init__.py000066400000000000000000000000001457505401100237520ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/tasks/panel.py000066400000000000000000000014571457505401100233330ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ import horizon from mistraldashboard import dashboard class Tasks(horizon.Panel): name = _("Task Executions") slug = 'tasks' dashboard.MistralDashboard.register(Tasks) mistral-dashboard-19.0.0/mistraldashboard/tasks/tables.py000066400000000000000000000070261457505401100235040ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.template.defaultfilters import title from django.urls import reverse from django.utils.translation import gettext_lazy as _ from horizon import exceptions from horizon import tables from mistraldashboard import api from mistraldashboard.default import smart_cell from mistraldashboard.default.utils import humantime from mistraldashboard.default.utils import label smart_cell.init() class UpdateRow(tables.Row): ajax = True def get_data(self, request, id): try: instance = api.task_get(request, id) except Exception: msg = _('Unable to get task by ID "%s".') % id exceptions.handle(request, msg) return instance class TypeColumn(tables.Column): def get_link_url(self, datum): obj_id = datum.id url = "" action_execution_url = "horizon:mistral:action_executions:task" workflow_execution_url = "horizon:mistral:executions:tasks" if datum.type == "ACTION": url = action_execution_url elif datum.type == "WORKFLOW": url = workflow_execution_url return reverse(url, args=[obj_id]) class TaskTable(tables.DataTable): def getHoverHelp(data): if hasattr(data, 'state_info') and data.state_info: return {'title': data.state_info} STATE_STATUS_CHOICES = ( ("success", True), ("error", False), ("idle", None), ("running", None), ) id = tables.Column( "id", verbose_name=_("ID"), link="horizon:mistral:tasks:detail" ) name = tables.Column( "name", verbose_name=_("Name") ) workflow_execution_id = tables.Column( "workflow_execution_id", verbose_name=_("Workflow Execution ID"), link="horizon:mistral:executions:detail_task_id" ) type = TypeColumn( "type", verbose_name=_("Type"), filters=[title], link=True ) result = tables.Column( "", verbose_name=_("Result"), empty_value=_("View"), link="horizon:mistral:tasks:result", link_classes=("ajax-modal",) ) published = tables.Column( "", verbose_name=_("Published"), empty_value=_("View"), link="horizon:mistral:tasks:published", link_classes=("ajax-modal",) ) created_at = tables.Column( "created_at", verbose_name=_("Created at"), filters=[humantime] ) updated_at = tables.Column( "updated_at", verbose_name=_("Updated at"), filters=[humantime] ) state = tables.Column( "state", status=True, status_choices=STATE_STATUS_CHOICES, verbose_name=_("State"), filters=[label], cell_attributes_getter=getHoverHelp ) class Meta(object): name = "tasks" verbose_name = _("Tasks") table_actions = (tables.FilterAction,) status_columns = ["state"] row_class = UpdateRow mistral-dashboard-19.0.0/mistraldashboard/tasks/templates/000077500000000000000000000000001457505401100236515ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/tasks/templates/tasks/000077500000000000000000000000001457505401100247765ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/tasks/templates/tasks/detail.html000066400000000000000000000051071457505401100271310ustar00rootroot00000000000000 {% extends 'mistral/default/base.html' %} {% load i18n %} {% block title %}{% trans "Task Execution Details" %}{% endblock %} {% block page_header %}

{% trans "Task Execution Details" %}

{% endblock page_header %} {% block main %}

{% trans "Overview" %}


{% trans "Name" %}
{{ task.name }}
{% trans "ID" %}
{{ task.id }}
{% trans "Type" %}
{{ task.type |title }}
{% if task.state_info %}
{% trans "State Info" %}
{{ task.state_info }}
{% endif %}
{% trans "State" %}
{{ task.state }}

{% trans "Creation Date" %}
{{ task.created_at|parse_isotime }}
{% trans "Time Since Created" %}
{{ task.created_at|parse_isotime|timesince }}

{% trans "Update Date" %}
{{ task.updated_at|parse_isotime }}
{% trans "Time Since Updated" %}
{{ task.updated_at|parse_isotime|timesince }}

{% trans "Result" %}
{{ task.result }}
{% trans "Published" %}
{{ task.published }}

{% trans "Workflow" %}


{% trans "Workflow Name" %}
{{ task.workflow_name }}
{% trans "Workflow Execution ID" %}
{{ task.workflow_execution_id }}
{% endblock %}
mistral-dashboard-19.0.0/mistraldashboard/tasks/templates/tasks/filtered.html000066400000000000000000000005151457505401100274630ustar00rootroot00000000000000{% extends 'mistral/default/table.html' %} {% load i18n %} {% block title %} {% trans "Tasks" %} {{ task_id }} {% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Tasks of Workflow Execution ID:") %}

{{ execution_id }}

{% endblock page_header %} mistral-dashboard-19.0.0/mistraldashboard/tasks/templates/tasks/index.html000066400000000000000000000004051457505401100267720ustar00rootroot00000000000000{% extends 'mistral/default/table.html' %} {% load i18n %} {% block title %} {% trans "Task Executions" %} {% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Task Executions") %} {% endblock page_header %} mistral-dashboard-19.0.0/mistraldashboard/tasks/tests.py000066400000000000000000000034121457505401100233670ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.urls import reverse from openstack_dashboard.test import helpers from mistraldashboard import api from mistraldashboard.test import helpers as test INDEX_URL = reverse('horizon:mistral:tasks:index') class TasksTest(test.TestCase): @helpers.create_mocks({api: ('task_list',)}) def test_index(self): self.mock_task_list.return_value =\ self.mistralclient_tasks.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'mistral/tasks/index.html') self.assertCountEqual(res.context['table'].data, self.mistralclient_tasks.list()) self.mock_task_list.assert_called_once_with(helpers.IsHttpRequest()) @helpers.create_mocks({api: ('task_get',)}) def test_detail(self): task = self.mistralclient_tasks.list()[0] self.mock_task_get.return_value = task url = reverse('horizon:mistral:tasks:detail', args=[task.id]) res = self.client.get(url) self.assertTemplateUsed(res, 'mistral/tasks/detail.html') self.mock_task_get.assert_called_once_with( helpers.IsHttpRequest(), task.id) mistral-dashboard-19.0.0/mistraldashboard/tasks/urls.py000066400000000000000000000023071457505401100232140ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from mistraldashboard.tasks import views TASKS = r'^(?P[^/]+)/%s$' EXECUTIONS = r'^(?P[^/]+)/%s$' urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path(TASKS % 'detail', views.OverviewView.as_view(), name='detail'), re_path(EXECUTIONS % 'execution', views.ExecutionView.as_view(), name='execution'), re_path(TASKS % 'result', views.CodeView.as_view(), {'column': 'result'}, name='result'), re_path(TASKS % 'published', views.CodeView.as_view(), {'column': 'published'}, name='published'), ] mistral-dashboard-19.0.0/mistraldashboard/tasks/views.py000066400000000000000000000107031457505401100233630ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views import generic from horizon import exceptions from horizon import forms from horizon import tables from mistraldashboard import api from mistraldashboard.default import utils from mistraldashboard import forms as mistral_forms from mistraldashboard.tasks import tables as mistral_tables def get_single_task_data(request, **kwargs): try: task_id = kwargs['task_id'] task = api.task_get(request, task_id) except Exception: msg = _('Unable to get task "%s".') % task_id redirect = reverse('horizon:mistral:tasks:index') exceptions.handle(request, msg, redirect=redirect) return task class ExecutionView(tables.DataTableView): table_class = mistral_tables.TaskTable template_name = 'mistral/tasks/filtered.html' def get_data(self, **kwargs): try: execution_id = self.kwargs['execution_id'] tasks = api.task_list(self.request, execution_id) except Exception: msg = _('Unable to get task by execution id "%s".') % execution_id redirect = reverse('horizon:mistral:executions:index') exceptions.handle(self.request, msg, redirect=redirect) return tasks class OverviewView(generic.TemplateView): template_name = 'mistral/tasks/detail.html' page_title = _("Task Details") workflow_detail_url = 'horizon:mistral:workflows:detail' workflow_execution_tasks_url = 'horizon:mistral:executions:tasks' execution_url = 'horizon:mistral:executions:detail' action_execution_url = 'horizon:mistral:action_executions:task' def get_context_data(self, **kwargs): context = super(OverviewView, self).get_context_data(**kwargs) task = get_single_task_data(self.request, **kwargs) task.workflow_detail_url = reverse(self.workflow_detail_url, args=[task.workflow_name]) task.execution_url = reverse(self.execution_url, args=[task.workflow_execution_id]) task.result = utils.prettyprint(task.result) task.published = utils.prettyprint(task.published) task.state = utils.label(task.state) if task.type == "ACTION": task.type_url = reverse( self.action_execution_url, args=[task.id] ) elif task.type == "WORKFLOW": task.type_url = reverse( self.workflow_execution_tasks_url, args=[task.id] ) breadcrumb = [(task.id, reverse( 'horizon:mistral:tasks:detail', args=[task.id] ))] context["custom_breadcrumb"] = breadcrumb context['task'] = task return context class CodeView(forms.ModalFormView): template_name = 'mistral/default/code.html' modal_header = _("Code view") form_id = "code_view" form_class = mistral_forms.EmptyForm cancel_label = "OK" cancel_url = reverse_lazy("horizon:mistral:tasks:index") page_title = _("Code view") def get_context_data(self, **kwargs): context = super(CodeView, self).get_context_data(**kwargs) column = self.kwargs['column'] task = get_single_task_data(self.request, **self.kwargs) io = {} if column == 'result': io['name'] = _('Result') io['value'] = task.result = utils.prettyprint(task.result) elif column == 'published': io['name'] = _('Published') io['value'] = task.published = utils.prettyprint(task.published) context['io'] = io return context class IndexView(tables.DataTableView): table_class = mistral_tables.TaskTable template_name = 'mistral/tasks/index.html' def get_data(self): return api.task_list(self.request) mistral-dashboard-19.0.0/mistraldashboard/test/000077500000000000000000000000001457505401100215055ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/test/__init__.py000066400000000000000000000000001457505401100236040ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/test/helpers.py000066400000000000000000000017641457505401100235310ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from openstack_dashboard.test import helpers from mistraldashboard.test.test_data import utils class MistralTestsMixin(object): def _setup_test_data(self): super(MistralTestsMixin, self)._setup_test_data() utils.load_test_data(self) class TestCase(MistralTestsMixin, helpers.TestCase): pass class APITestCase(MistralTestsMixin, helpers.APITestCase): pass mistral-dashboard-19.0.0/mistraldashboard/test/settings.py000066400000000000000000000015471457505401100237260ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # Copyright 2016 NEC Corporation. All rights reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from horizon.test.settings import * # noqa from openstack_dashboard.test.settings import * # noqa INSTALLED_APPS = list(INSTALLED_APPS) # noqa: F405 INSTALLED_APPS.append('mistraldashboard') mistral-dashboard-19.0.0/mistraldashboard/test/test_data/000077500000000000000000000000001457505401100234555ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/test/test_data/__init__.py000066400000000000000000000000001457505401100255540ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/test/test_data/mistral_data.py000066400000000000000000000114231457505401100264740ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from mistralclient.api.v2 import action_executions from mistralclient.api.v2 import actions from mistralclient.api.v2 import cron_triggers from mistralclient.api.v2 import executions from mistralclient.api.v2 import tasks from mistralclient.api.v2 import workbooks from mistralclient.api.v2 import workflows from openstack_dashboard.test.test_data import utils as test_data_utils # Workbooks WB_DEF = """ --- version: 2.0 name: wb workflows: wf1: type: direct input: - param1 - param2 tasks: task1: action: std.http url="localhost:8989" on-success: - test_subsequent test_subsequent: action: std.http url="http://some_url" server_id=1 """.strip() # Workflows WF_DEF = """ version: '2.0' flow: tasks: task1: action: nova.servers_get server="1" """.strip() def data(TEST): # MistralActions TEST.mistralclient_actions = test_data_utils.TestDataContainer() action_1 = actions.Action( actions.ActionManager(None), {'name': 'a', 'id': '1', 'is_system': True, 'input': 'param1', 'description': 'my cool action', 'tags': ['test'], 'definition': '1', 'created_at': '1', 'updated_at': '1' } ) TEST.mistralclient_actions.add(action_1) # MistralExecutions TEST.mistralclient_executions = test_data_utils.TestDataContainer() execution_1 = executions.Execution( executions.ExecutionManager(None), {'id': '123', 'workflow_name': 'my_wf', 'description': '', 'state': 'RUNNING', 'input': '{"person": {"first_name": "John", "last_name": "Doe"}}', 'output': "1", 'params': "1", } ) TEST.mistralclient_executions.add(execution_1) # Tasks TEST.mistralclient_tasks = test_data_utils.TestDataContainer() task_1 = tasks.Task( tasks.TaskManager(None), {'id': '1', 'workflow_execution_id': '123', 'name': 'my_task', 'workflow_name': 'my_wf', 'state': 'RUNNING', 'type': 'ACTION', 'tags': ['deployment', 'demo'], 'result': "1", 'published': '{"a":"1"}'}) TEST.mistralclient_tasks.add(task_1) # Workbooks TEST.mistralclient_workbooks = test_data_utils.TestDataContainer() workbook_1 = workbooks.Workbook( workbooks.WorkbookManager(None), {'name': 'a', 'tags': ['a', 'b'], 'created_at': '1', 'updated_at': '1', 'definition': WB_DEF} ) TEST.mistralclient_workbooks.add(workbook_1) # Workflows TEST.mistralclient_workflows = test_data_utils.TestDataContainer() workflow_1 = workflows.Workflow( workflows.WorkflowManager(None), {'name': 'a', 'id': '1', 'tags': ['a', 'b'], 'input': 'param', 'created_at': '1', 'updated_at': '1', 'definition': WF_DEF} ) TEST.mistralclient_workflows.add(workflow_1) # MistralActionsExecutions TEST.mistralclient_action_executions = test_data_utils.TestDataContainer() action_executions_1 = action_executions.ActionExecution( action_executions.ActionExecutionManager(None), {'id': '1', 'name': 'a', 'tags': ['a', 'b'], 'workflow_name': 'my work flow', 'task_execution_id': '1', 'task_name': 'b', 'input': "1", 'output': "1", 'description': '', 'created_at': '1', 'updated_at': '1', 'accepted': True, 'state': 'SUCCESS' } ) TEST.mistralclient_action_executions.add(action_executions_1) # MistralCronTriggers TEST.mistralclient_cron_triggers = test_data_utils.TestDataContainer() cron_triggers_1 = cron_triggers.CronTrigger( cron_triggers.CronTriggerManager(None), {'id': '1', 'name': 'a', 'workflow_name': 'my work flow', 'pattern': '', 'next_execution_time': '', 'remaining_executions': '', 'first_execution_time': '', 'created_at': '1', 'updated_at': '1' }) TEST.mistralclient_cron_triggers.add(cron_triggers_1) mistral-dashboard-19.0.0/mistraldashboard/test/test_data/utils.py000066400000000000000000000021601457505401100251660ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from openstack_dashboard.test.test_data import utils def load_test_data(load_onto=None): from mistraldashboard.test.test_data import mistral_data from openstack_dashboard.test.test_data import exceptions # The order of these loaders matters, some depend on others. loaders = ( exceptions.data, mistral_data.data, ) if load_onto: for data_func in loaders: data_func(load_onto) return load_onto else: return utils.TestData(*loaders) mistral-dashboard-19.0.0/mistraldashboard/test/urls.py000066400000000000000000000013111457505401100230400ustar00rootroot00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.conf import urls import openstack_dashboard.urls urlpatterns = [ urls.url(r'', urls.include(openstack_dashboard.urls)) ] mistral-dashboard-19.0.0/mistraldashboard/workbooks/000077500000000000000000000000001457505401100225465ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/workbooks/__init__.py000066400000000000000000000000001457505401100246450ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/workbooks/forms.py000066400000000000000000000103761457505401100242550ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.utils.translation import gettext_lazy as _ from horizon import exceptions from horizon import forms from horizon import messages from mistraldashboard import api class DefinitionForm(forms.SelfHandlingForm): definition_source = forms.ChoiceField( label=_('Definition Source'), choices=[('file', _('File')), ('raw', _('Direct Input'))], widget=forms.Select( attrs={'class': 'switchable', 'data-slug': 'definitionsource'}) ) definition_upload = forms.FileField( label=_('Definition File'), help_text=_('A local definition to upload.'), widget=forms.FileInput( attrs={'class': 'switched', 'data-switch-on': 'definitionsource', 'data-required-when-shown': 'true', 'data-definitionsource-file': _('Definition File')} ), required=False ) definition_data = forms.CharField( label=_('Definition Data'), help_text=_('The raw contents of the definition.'), widget=forms.widgets.Textarea( attrs={'class': 'switched', 'data-switch-on': 'definitionsource', 'data-required-when-shown': 'true', 'data-definitionsource-raw': _('Definition Data'), 'rows': 4} ), required=False ) def __init__(self, *args, **kwargs): self.next_view = kwargs.pop('next_view') super(DefinitionForm, self).__init__(*args, **kwargs) def clean(self): cleaned_data = super(DefinitionForm, self).clean() if cleaned_data.get('definition_upload'): files = self.request.FILES cleaned_data['definition'] = files['definition_upload'].read() elif cleaned_data.get('definition_data'): cleaned_data['definition'] = cleaned_data['definition_data'] else: raise forms.ValidationError( _('You must specify the definition source.')) try: validated = api.workbook_validate( self.request, cleaned_data['definition'] ) except Exception as e: raise forms.ValidationError(str(e)) if not validated.get('valid'): raise forms.ValidationError( validated.get('error', _('Validated failed'))) return cleaned_data def handle(self, request, data): kwargs = {'definition': data['definition']} request.method = 'GET' return self.next_view.as_view()(request, **kwargs) class CreateForm(forms.SelfHandlingForm): definition = forms.CharField( widget=forms.widgets.Textarea( attrs={'rows': 12} ), required=False ) def handle(self, request, data): try: api.workbook_create(request, data['definition']) msg = _('Successfully created workbook.') messages.success(request, msg) return True except Exception: msg = _('Failed to create workbook.') redirect = reverse('horizon:mistral:workbooks:index') exceptions.handle(request, msg, redirect=redirect) class UpdateForm(CreateForm): def handle(self, request, data): try: api.workbook_update(request, data['definition']) msg = _('Successfully updated workbook.') messages.success(request, msg) return True except Exception: msg = _('Failed to update workbook.') redirect = reverse('horizon:mistral:workbooks:index') exceptions.handle(request, msg, redirect=redirect) mistral-dashboard-19.0.0/mistraldashboard/workbooks/panel.py000066400000000000000000000014661457505401100242260ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ import horizon from mistraldashboard import dashboard class Workbooks(horizon.Panel): name = _("Workbooks") slug = 'workbooks' dashboard.MistralDashboard.register(Workbooks) mistral-dashboard-19.0.0/mistraldashboard/workbooks/tables.py000066400000000000000000000052741457505401100244020ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from horizon import tables from horizon.utils import filters from mistraldashboard import api class CreateWorkbook(tables.LinkAction): name = "create" verbose_name = _("Create Workbook") url = "horizon:mistral:workbooks:select_definition" classes = ("ajax-modal",) icon = "plus" class UpdateWorkbook(tables.LinkAction): name = "update" verbose_name = _("Update Workbook") url = "horizon:mistral:workbooks:change_definition" classes = ("ajax-modal",) icon = "pencil" class DeleteWorkbook(tables.DeleteAction): @staticmethod def action_present(count): return ngettext_lazy( u"Delete Workbook", u"Delete Workbooks", count ) @staticmethod def action_past(count): return ngettext_lazy( u"Deleted Workbook", u"Deleted Workbooks", count ) def delete(self, request, workbook_name): api.workbook_delete(request, workbook_name) def tags_to_string(workbook): return ', '.join(workbook.tags) if workbook.tags else None class WorkbooksTable(tables.DataTable): name = tables.Column( "name", verbose_name=_("Name"), link="horizon:mistral:workbooks:detail" ) tags = tables.Column(tags_to_string, verbose_name=_("Tags")) created = tables.Column( "created_at", verbose_name=_("Created"), filters=( filters.parse_isotime, filters.timesince_or_never ) ) updated = tables.Column( "updated_at", verbose_name=_("Updated"), filters=( filters.parse_isotime, filters.timesince_or_never ) ) def get_object_id(self, datum): return datum.name class Meta(object): name = "workbooks" verbose_name = _("Workbooks") table_actions = ( CreateWorkbook, UpdateWorkbook, DeleteWorkbook, tables.FilterAction ) row_actions = (DeleteWorkbook,) mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/000077500000000000000000000000001457505401100245445ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/workbooks/000077500000000000000000000000001457505401100265645ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/workbooks/_create.html000066400000000000000000000003351457505401100310550ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Create a new workbook with the provided definition." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/workbooks/_select_definition.html000066400000000000000000000011441457505401100333000ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block form_attrs %}enctype="multipart/form-data"{% endblock %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Use one of the available definition source options to specify the definition to be used in creating this workbook." %}

{% trans "Refer"%} Mistral Workflow Language {% trans " documentation for syntax details." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/workbooks/_update.html000066400000000000000000000003301457505401100310670ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Update workbooks with the provided definition." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/workbooks/create.html000066400000000000000000000002711457505401100307150ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Create Workbook" %}{% endblock %} {% block main %} {% include 'mistral/workbooks/_create.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/workbooks/detail.html000066400000000000000000000006341457505401100307170ustar00rootroot00000000000000 {% extends 'mistral/default/base.html' %} {% load i18n %} {% block title %}{% trans "Workbook Definition" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Workbook Definition") %} {% endblock page_header %} {% block main %}
{{ definition }}
{% endblock %}
mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/workbooks/index.html000066400000000000000000000003631457505401100305630ustar00rootroot00000000000000{% extends 'mistral/default/table.html' %} {% load i18n %} {% block title %}{% trans "Workbooks" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Workbooks") %} {% endblock page_header %} mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/workbooks/select_definition.html000066400000000000000000000003061457505401100331400ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Select Definition" %}{% endblock %} {% block main %} {% include 'mistral/workbooks/_select_definition.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workbooks/templates/workbooks/update.html000066400000000000000000000002711457505401100307340ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Update Workbook" %}{% endblock %} {% block main %} {% include 'mistral/workbooks/_update.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workbooks/tests.py000066400000000000000000000130341457505401100242630ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.urls import reverse from openstack_dashboard.test import helpers from mistraldashboard import api from mistraldashboard.test import helpers as test INDEX_URL = reverse('horizon:mistral:workbooks:index') CREATE_URL = reverse('horizon:mistral:workbooks:create') UPDATE_URL = reverse('horizon:mistral:workbooks:update') class WorkflowsTest(test.TestCase): @helpers.create_mocks({api: ('workbook_list',)}) def test_index(self): self.mock_workbook_list.return_value =\ self.mistralclient_workbooks.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'mistral/workbooks/index.html') self.assertCountEqual(res.context['table'].data, self.mistralclient_workbooks.list()) self.mock_workbook_list.\ assert_called_once_with(helpers.IsHttpRequest()) def test_create_get(self): res = self.client.get(CREATE_URL) self.assertTemplateUsed(res, 'mistral/workbooks/create.html') @helpers.create_mocks({api: ('workbook_validate', 'workbook_create')}) def test_create_post(self): self.mock_workbook_validate.return_value = {'valid': True} workbook = self.mistralclient_workbooks.first() url = reverse('horizon:mistral:workbooks:select_definition') res = self.client.get(url) self.assertTemplateUsed( res, 'mistral/workbooks/select_definition.html' ) form_data = { 'definition_source': 'raw', 'definition_data': workbook.definition } res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'mistral/workbooks/create.html') self.mock_workbook_validate.assert_called_once_with( helpers.IsHttpRequest(), workbook.definition) form_data = { 'definition': workbook.definition } self.mock_workbook_create.return_value = workbook res = self.client.post(CREATE_URL, form_data) self.assertNoFormErrors(res) self.assertEqual(res.status_code, 302) self.assertRedirectsNoFollow(res, INDEX_URL) self.mock_workbook_create.assert_called_once_with( helpers.IsHttpRequest(), workbook.definition) def test_update_get(self): res = self.client.get(UPDATE_URL) self.assertTemplateUsed(res, 'mistral/workbooks/update.html') @helpers.create_mocks({api: ('workbook_validate', 'workbook_update')}) def test_update_post(self): workbook = self.mistralclient_workbooks.first() self.mock_workbook_validate.return_value = {'valid': True} url = reverse('horizon:mistral:workbooks:change_definition') res = self.client.get(url) self.assertTemplateUsed( res, 'mistral/workbooks/select_definition.html' ) form_data = { 'definition_source': 'raw', 'definition_data': workbook.definition } res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'mistral/workbooks/update.html') self.mock_workbook_validate.assert_called_once_with( helpers.IsHttpRequest(), workbook.definition) form_data = { 'definition': workbook.definition } self.mock_workbook_update.return_value = workbook res = self.client.post(UPDATE_URL, form_data) self.assertNoFormErrors(res) self.assertEqual(res.status_code, 302) self.assertRedirectsNoFollow(res, INDEX_URL) self.mock_workbook_update.assert_called_once_with( helpers.IsHttpRequest(), workbook.definition) @helpers.create_mocks({api: ('workbook_list', 'workbook_delete')}) def test_delete_ok(self): workbooks = self.mistralclient_workbooks.list() self.mock_workbook_list.return_value = workbooks self.mock_workbook_delete.return_value = None data = {'action': 'workbooks__delete', 'object_ids': [workbooks[0].name]} res = self.client.post(INDEX_URL, data) self.mock_workbook_delete.assert_called_once_with( helpers.IsHttpRequest(), workbooks[0].name) self.mock_workbook_list.assert_called_once_with( helpers.IsHttpRequest()) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) @helpers.create_mocks({api: ('workbook_get',)}) def test_detail(self): workbook = self.mistralclient_workbooks.list()[0] self.mock_workbook_get.return_value = workbook url = reverse('horizon:mistral:workbooks:detail', args=[workbook.name]) res = self.client.get(url) self.assertTemplateUsed(res, 'mistral/workbooks/detail.html') self.mock_workbook_get.assert_called_once_with( helpers.IsHttpRequest(), workbook.name) mistral-dashboard-19.0.0/mistraldashboard/workbooks/urls.py000066400000000000000000000023411457505401100241050ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from mistraldashboard.workbooks import views WORKBOOKS = r'^(?P[^/]+)/%s$' urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path(r'^select_definition$', views.SelectDefinitionView.as_view(), name='select_definition'), re_path(r'^change_definition$', views.ChangeDefinitionView.as_view(), name='change_definition'), re_path(r'^create$', views.CreateView.as_view(), name='create'), re_path(r'^update$', views.UpdateView.as_view(), name='update'), re_path(WORKBOOKS % 'detail', views.DetailView.as_view(), name='detail'), ] mistral-dashboard-19.0.0/mistraldashboard/workbooks/views.py000066400000000000000000000101051457505401100242520ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views import generic from horizon import exceptions from horizon import forms from horizon import tables from mistraldashboard import api from mistraldashboard.workbooks import forms as mistral_forms from mistraldashboard.workbooks import tables as mistral_tables class IndexView(tables.DataTableView): table_class = mistral_tables.WorkbooksTable template_name = 'mistral/workbooks/index.html' def get_data(self): return api.workbook_list(self.request) class DetailView(generic.TemplateView): template_name = 'mistral/workbooks/detail.html' page_title = _("Workbook Definition") def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) workbook = self.get_data(self.request, **kwargs) breadcrumb = [(workbook.name, reverse( 'horizon:mistral:workbooks:detail', args=[workbook.name] ))] context["custom_breadcrumb"] = breadcrumb context['definition'] = workbook.definition return context def get_data(self, request, **kwargs): try: workbook_name = kwargs['workbook_name'] workbook = api.workbook_get(request, workbook_name) except Exception: msg = _('Unable to get workbook "%s".') % workbook_name redirect = reverse('horizon:mistral:workbooks:index') exceptions.handle(self.request, msg, redirect=redirect) return workbook class SelectDefinitionView(forms.ModalFormView): template_name = 'mistral/workbooks/select_definition.html' modal_header = _("Create Workbook") form_id = "select_definition" form_class = mistral_forms.DefinitionForm submit_label = _("Validate") submit_url = reverse_lazy("horizon:mistral:workbooks:select_definition") success_url = reverse_lazy('horizon:mistral:workbooks:create') page_title = _("Select Definition") def get_form_kwargs(self): kwargs = super(SelectDefinitionView, self).get_form_kwargs() kwargs['next_view'] = CreateView return kwargs class ChangeDefinitionView(SelectDefinitionView): modal_header = _("Update Workbook") submit_url = reverse_lazy("horizon:mistral:workbooks:change_definition") success_url = reverse_lazy('horizon:mistral:workbooks:update') page_title = _("Update Definition") def get_form_kwargs(self): kwargs = super(ChangeDefinitionView, self).get_form_kwargs() kwargs['next_view'] = UpdateView return kwargs class CreateView(forms.ModalFormView): template_name = 'mistral/workbooks/create.html' modal_header = _("Create Workbook") form_id = "create_workbook" form_class = mistral_forms.CreateForm submit_label = _("Create") submit_url = reverse_lazy("horizon:mistral:workbooks:create") success_url = reverse_lazy('horizon:mistral:workbooks:index') page_title = _("Create Workbook") def get_initial(self): initial = {} if 'definition' in self.kwargs: initial['definition'] = self.kwargs['definition'] return initial class UpdateView(CreateView): template_name = 'mistral/workbooks/update.html' modal_header = _("Update Workbook") form_id = "update_workbook" form_class = mistral_forms.UpdateForm submit_label = _("Update") submit_url = reverse_lazy("horizon:mistral:workbooks:update") page_title = _("Update Workbook") mistral-dashboard-19.0.0/mistraldashboard/workflows/000077500000000000000000000000001457505401100225635ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/workflows/__init__.py000066400000000000000000000000001457505401100246620ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/workflows/forms.py000066400000000000000000000142121457505401100242630ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.utils.translation import gettext_lazy as _ from horizon import exceptions from horizon import forms from horizon import messages from mistraldashboard import api class ExecuteForm(forms.SelfHandlingForm): workflow_name = forms.CharField( label=_("Workflow"), required=True, widget=forms.TextInput(attrs={'readonly': 'readonly'}) ) task_name = forms.CharField( label=_("Task name"), required=False, widget=forms.TextInput() ) def __init__(self, *args, **kwargs): super(ExecuteForm, self).__init__(*args, **kwargs) self._generate_parameter_fields(kwargs["initial"]["parameter_list"]) def _generate_parameter_fields(self, list): self.workflow_parameters = [] for entry in list.split(","): label, _, default = entry.partition("=") label = label.strip() if label != '': self.workflow_parameters.append(label) if default == "None": default = None required = False else: required = True self.fields[label] = forms.CharField(label=label, required=required, initial=default) def handle(self, request, data): try: data['wf_identifier'] = data.pop('workflow_name') data['workflow_input'] = {} for param in self.workflow_parameters: value = data.pop(param) if value == "": value = None data['workflow_input'][param] = value ex = api.execution_create(request, **data) msg = _('Execution has been created with id "%s".') % ex.id messages.success(request, msg) return True except Exception: msg = _('Failed to execute workflow.') redirect = reverse('horizon:mistral:workflows:index') exceptions.handle(request, msg, redirect=redirect) class DefinitionForm(forms.SelfHandlingForm): definition_source = forms.ChoiceField( label=_('Definition Source'), choices=[('file', _('File')), ('raw', _('Direct Input'))], widget=forms.Select( attrs={'class': 'switchable', 'data-slug': 'definitionsource'}) ) definition_upload = forms.FileField( label=_('Definition File'), help_text=_('A local definition to upload.'), widget=forms.FileInput( attrs={'class': 'switched', 'data-switch-on': 'definitionsource', 'data-required-when-shown': 'true', 'data-definitionsource-file': _('Definition File')} ), required=False ) definition_data = forms.CharField( label=_('Definition Data'), help_text=_('The raw contents of the definition.'), widget=forms.widgets.Textarea( attrs={'class': 'switched', 'data-switch-on': 'definitionsource', 'data-required-when-shown': 'true', 'data-definitionsource-raw': _('Definition Data'), 'rows': 4} ), required=False ) def __init__(self, *args, **kwargs): self.next_view = kwargs.pop('next_view') super(DefinitionForm, self).__init__(*args, **kwargs) def clean(self): cleaned_data = super(DefinitionForm, self).clean() if cleaned_data.get('definition_upload'): files = self.request.FILES cleaned_data['definition'] = files['definition_upload'].read() elif cleaned_data.get('definition_data'): cleaned_data['definition'] = cleaned_data['definition_data'] else: raise forms.ValidationError( _('You must specify the definition source.')) try: validated = api.workflow_validate( self.request, cleaned_data['definition'] ) except Exception as e: raise forms.ValidationError(str(e)) if not validated.get('valid'): raise forms.ValidationError( validated.get('error', _('Validated failed'))) return cleaned_data def handle(self, request, data): kwargs = {'definition': data['definition']} request.method = 'GET' return self.next_view.as_view()(request, **kwargs) class CreateForm(forms.SelfHandlingForm): definition = forms.CharField( widget=forms.widgets.Textarea( attrs={'rows': 12} ), required=False ) def handle(self, request, data): try: api.workflow_create(request, data['definition']) msg = _('Successfully created workflow.') messages.success(request, msg) return True except Exception: msg = _('Failed to create workflow.') redirect = reverse('horizon:mistral:workflows:index') exceptions.handle(request, msg, redirect=redirect) class UpdateForm(CreateForm): def handle(self, request, data): try: api.workflow_update(request, data['definition']) msg = _('Successfully updated workflow.') messages.success(request, msg) return True except Exception: msg = _('Failed to update workflow.') redirect = reverse('horizon:mistral:workflows:index') exceptions.handle(request, msg, redirect=redirect) mistral-dashboard-19.0.0/mistraldashboard/workflows/panel.py000066400000000000000000000014661457505401100242430ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ import horizon from mistraldashboard import dashboard class Workflows(horizon.Panel): name = _("Workflows") slug = 'workflows' dashboard.MistralDashboard.register(Workflows) mistral-dashboard-19.0.0/mistraldashboard/workflows/tables.py000066400000000000000000000072461457505401100244200ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.template.defaultfilters import title from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from horizon import tables from horizon.utils import filters from mistraldashboard import api class CreateWorkflow(tables.LinkAction): name = "create" verbose_name = _("Create Workflow") url = "horizon:mistral:workflows:select_definition" classes = ("ajax-modal",) icon = "plus" class UpdateWorkflow(tables.LinkAction): name = "update" verbose_name = _("Update Workflow") url = "horizon:mistral:workflows:change_definition" classes = ("ajax-modal",) icon = "pencil" class DeleteWorkflow(tables.DeleteAction): @staticmethod def action_present(count): return ngettext_lazy( u"Delete Workflow", u"Delete Workflows", count ) @staticmethod def action_past(count): return ngettext_lazy( u"Deleted Workflow", u"Deleted Workflows", count ) def delete(self, request, workflow_name): api.workflow_delete(request, workflow_name) class ExecuteWorkflow(tables.LinkAction): name = "execute" verbose_name = _("Execute") url = "horizon:mistral:workflows:execute" classes = ("ajax-modal", "btn-edit") def tags_to_string(workflow): return ', '.join(workflow.tags) if workflow.tags else None def cut(workflow, length=50): inputs = workflow.input if inputs and len(inputs) > length: return "%s..." % inputs[:length] else: return inputs class WorkflowsTable(tables.DataTable): name = tables.Column( "name", verbose_name=_("Name"), link="horizon:mistral:workflows:detail" ) id = tables.Column( "id", verbose_name=_("ID"), ) scope = tables.Column( "scope", verbose_name=_("Scope"), filters=[title], ) definition = tables.Column( "", verbose_name=_("Definition"), empty_value=_("View"), link="horizon:mistral:workflows:definition", link_classes=("ajax-modal",) ) tags = tables.Column( tags_to_string, verbose_name=_("Tags") ) inputs = tables.Column( cut, verbose_name=_("Input"), link="horizon:mistral:workflows:input", link_classes=("ajax-modal",) ) created = tables.Column( "created_at", verbose_name=_("Created"), filters=( filters.parse_isotime, filters.timesince_or_never ) ) updated = tables.Column( "updated_at", verbose_name=_("Updated"), filters=( filters.parse_isotime, filters.timesince_or_never ) ) def get_object_id(self, datum): return datum.name class Meta(object): name = "workflows" verbose_name = _("Workflows") table_actions = ( CreateWorkflow, UpdateWorkflow, DeleteWorkflow, tables.FilterAction ) row_actions = (ExecuteWorkflow, DeleteWorkflow) mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/000077500000000000000000000000001457505401100245615ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/000077500000000000000000000000001457505401100266165ustar00rootroot00000000000000mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/_create.html000066400000000000000000000003351457505401100311070ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Create a new workflow with the provided definition." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/_execute.html000066400000000000000000000010441457505401100313040ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block form_id %}execute_form{% endblock %} {% block form_action %}{% url 'horizon:mistral:workflows:execute' workflow_name %}{% endblock %} {% block modal-header %}{% trans "Execute" %}{% endblock %} {% block modal-body %}
{% include "horizon/common/_form_fields.html" %}

{% trans "Description:" %}

{% trans "From here you can execute a workflow." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/_select_definition.html000066400000000000000000000011341457505401100333310ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block form_attrs %}enctype="multipart/form-data"{% endblock %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Use one of the available definition source options to specify the definition to be used in creating this workflow." %}

{% trans "Refer "%} Mistral Workflow Language {% trans " documentation for syntax details." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/_update.html000066400000000000000000000003301457505401100311210ustar00rootroot00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Update workflows with the provided definition." %}

{% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/create.html000066400000000000000000000002711457505401100307470ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Create Workflow" %}{% endblock %} {% block main %} {% include 'mistral/workflows/_create.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/detail.html000066400000000000000000000006331457505401100307500ustar00rootroot00000000000000 {% extends 'mistral/default/base.html' %} {% load i18n %} {% block title %}{% trans "Workflow Definition" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Workflow Definition") %} {% endblock page_header %} {% block main %}
{{ definition }}
{% endblock %}
mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/execute.html000066400000000000000000000005231457505401100311460ustar00rootroot00000000000000{% extends 'mistral/default/base.html' %} {% load i18n %} {% block title %}{% trans "Execute workflow" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Execute workflow") %} {% endblock page_header %} {% block main %} {% include 'mistral/workflows/_execute.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/index.html000066400000000000000000000003631457505401100306150ustar00rootroot00000000000000{% extends 'mistral/default/table.html' %} {% load i18n %} {% block title %}{% trans "Workflows" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Workflows") %} {% endblock page_header %} mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/select_definition.html000066400000000000000000000003061457505401100331720ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Select Definition" %}{% endblock %} {% block main %} {% include 'mistral/workflows/_select_definition.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workflows/templates/workflows/update.html000066400000000000000000000002711457505401100307660ustar00rootroot00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Update Workflow" %}{% endblock %} {% block main %} {% include 'mistral/workflows/_update.html' %} {% endblock %} mistral-dashboard-19.0.0/mistraldashboard/workflows/tests.py000066400000000000000000000131411457505401100242770ustar00rootroot00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.urls import reverse from openstack_dashboard.test import helpers from mistraldashboard import api from mistraldashboard.test import helpers as test INDEX_URL = reverse('horizon:mistral:workflows:index') CREATE_URL = reverse('horizon:mistral:workflows:create') UPDATE_URL = reverse('horizon:mistral:workflows:update') class WorkflowsTest(test.TestCase): @helpers.create_mocks({api: ('workflow_list',)}) def test_index(self): self.mock_workflow_list.return_value =\ self.mistralclient_workflows.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'mistral/workflows/index.html') self.assertCountEqual(res.context['table'].data, self.mistralclient_workflows.list()) self.mock_workflow_list.assert_called_once_with( helpers.IsHttpRequest()) def test_create_get(self): res = self.client.get(CREATE_URL) self.assertTemplateUsed(res, 'mistral/workflows/create.html') @helpers.create_mocks({api: ('workflow_validate', 'workflow_create')}) def test_create_post(self): workflow = self.mistralclient_workflows.first() self.mock_workflow_validate.return_value = {'valid': True} self.mock_workflow_create.return_value = workflow url = reverse('horizon:mistral:workflows:select_definition') res = self.client.get(url) self.assertTemplateUsed( res, 'mistral/workflows/select_definition.html' ) form_data = { 'definition_source': 'raw', 'definition_data': workflow.definition } res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'mistral/workflows/create.html') self.mock_workflow_validate.assert_called_once_with( helpers.IsHttpRequest(), workflow.definition ) form_data = { 'definition': workflow.definition } res = self.client.post(CREATE_URL, form_data) self.assertNoFormErrors(res) self.assertEqual(res.status_code, 302) self.assertRedirectsNoFollow(res, INDEX_URL) self.mock_workflow_create.assert_called_once_with( helpers.IsHttpRequest(), workflow.definition ) def test_update_get(self): res = self.client.get(UPDATE_URL) self.assertTemplateUsed(res, 'mistral/workflows/update.html') @helpers.create_mocks({api: ('workflow_validate', 'workflow_update')}) def test_update_post(self): workflow = self.mistralclient_workflows.first() self.mock_workflow_validate.return_value = {'valid': True} self.mock_workflow_update.return_value = workflow url = reverse('horizon:mistral:workflows:change_definition') res = self.client.get(url) self.assertTemplateUsed( res, 'mistral/workflows/select_definition.html' ) form_data = { 'definition_source': 'raw', 'definition_data': workflow.definition } res = self.client.post(url, form_data) self.assertTemplateUsed(res, 'mistral/workflows/update.html') self.mock_workflow_validate.assert_called_once_with( helpers.IsHttpRequest(), workflow.definition ) form_data = { 'definition': workflow.definition } res = self.client.post(UPDATE_URL, form_data) self.assertNoFormErrors(res) self.assertEqual(res.status_code, 302) self.assertRedirectsNoFollow(res, INDEX_URL) self.mock_workflow_update.assert_called_once_with( helpers.IsHttpRequest(), workflow.definition ) @helpers.create_mocks({api: ('workflow_list', 'workflow_delete')}) def test_delete_ok(self): workflows = self.mistralclient_workflows.list() self.mock_workflow_list.return_value = workflows self.mock_workflow_delete.return_value = None data = {'action': 'workflows__delete', 'object_ids': [workflows[0].name]} res = self.client.post(INDEX_URL, data) self.mock_workflow_delete.assert_called_once_with( helpers.IsHttpRequest(), workflows[0].name ) self.mock_workflow_list.assert_called_once_with( helpers.IsHttpRequest()) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) @helpers.create_mocks({api: ('workflow_get',)}) def test_detail(self): workflow = self.mistralclient_workflows.list()[0] self.mock_workflow_get.return_value = workflow url = reverse('horizon:mistral:workflows:detail', args=[workflow.name]) res = self.client.get(url) self.assertTemplateUsed(res, 'mistral/workflows/detail.html') self.mock_workflow_get.assert_called_once_with( helpers.IsHttpRequest(), workflow.name) mistral-dashboard-19.0.0/mistraldashboard/workflows/urls.py000066400000000000000000000030431457505401100241220ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from mistraldashboard.workflows import views WORKFLOWS = r'^(?P[^/]+)/%s$' urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path(r'^select_definition$', views.SelectDefinitionView.as_view(), name='select_definition'), re_path(r'^change_definition$', views.ChangeDefinitionView.as_view(), name='change_definition'), re_path(r'^create$', views.CreateView.as_view(), name='create'), re_path(r'^update$', views.UpdateView.as_view(), name='update'), re_path(WORKFLOWS % 'execute', views.ExecuteView.as_view(), name='execute'), re_path(WORKFLOWS % 'detail', views.DetailView.as_view(), name='detail'), re_path(WORKFLOWS % 'definition', views.CodeView.as_view(), {'column': 'definition'}, name='definition'), re_path(WORKFLOWS % 'input', views.CodeView.as_view(), {'column': 'input'}, name='input'), ] mistral-dashboard-19.0.0/mistraldashboard/workflows/views.py000066400000000000000000000134521457505401100242770ustar00rootroot00000000000000# Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import reverse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views import generic from horizon import exceptions from horizon import forms from horizon import tables from mistraldashboard import api from mistraldashboard.default import utils from mistraldashboard import forms as mistral_forms from mistraldashboard.workflows import forms as workflow_forms from mistraldashboard.workflows import tables as workflows_tables def get_single_data(request, workflow_name): try: workflow = api.workflow_get(request, workflow_name) except Exception: msg = _('Unable to get workflow "%s".') % workflow_name redirect = reverse('horizon:mistral:workflows:index') exceptions.handle(request, msg, redirect=redirect) return workflow class IndexView(tables.DataTableView): table_class = workflows_tables.WorkflowsTable template_name = 'mistral/workflows/index.html' def get_data(self): return api.workflow_list(self.request) class DetailView(generic.TemplateView): template_name = 'mistral/workflows/detail.html' page_title = _("Workflow Definition") def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) workflow = get_single_data(self.request, kwargs['workflow_name']) breadcrumb = [(workflow.name, reverse( 'horizon:mistral:workflows:detail', args=[workflow.name] ))] context["custom_breadcrumb"] = breadcrumb context['definition'] = ( workflow.definition or 'This workflow was created as part of workbook %s' % workflow.name.split('.')[0]) return context class CodeView(forms.ModalFormView): template_name = 'mistral/default/code.html' modal_header = _("Code view") form_id = "code_view" form_class = mistral_forms.EmptyForm cancel_label = "OK" cancel_url = reverse_lazy("horizon:mistral:workflows:index") page_title = _("Code view") def get_context_data(self, **kwargs): context = super(CodeView, self).get_context_data(**kwargs) workflow = get_single_data(self.request, self.kwargs['workflow_name']) io = {} column = self.kwargs['column'] if column == 'definition': io['name'] = _('Workflow Definition') io['value'] = utils.htmlpre(workflow.definition) elif column == 'input': io['name'] = _('Workflow Input') io['value'] = workflow.input context['io'] = io return context class ExecuteView(forms.ModalFormView): form_class = workflow_forms.ExecuteForm template_name = 'mistral/workflows/execute.html' success_url = reverse_lazy("horizon:mistral:executions:index") submit_label = _("Execute") cancel_url = reverse_lazy("horizon:mistral:workflows:index") def get_context_data(self, **kwargs): context = super(ExecuteView, self).get_context_data(**kwargs) context["workflow_name"] = self.kwargs['workflow_name'] return context def get_initial(self, **kwargs): workflow = get_single_data(self.request, self.kwargs['workflow_name']) return {'workflow_name': self.kwargs['workflow_name'], 'parameter_list': workflow.input} class SelectDefinitionView(forms.ModalFormView): template_name = 'mistral/workflows/select_definition.html' modal_header = _("Create Workflow") form_id = "select_definition" form_class = workflow_forms.DefinitionForm submit_label = _("Validate") submit_url = reverse_lazy("horizon:mistral:workflows:select_definition") success_url = reverse_lazy('horizon:mistral:workflows:create') page_title = _("Select Definition") def get_form_kwargs(self): kwargs = super(SelectDefinitionView, self).get_form_kwargs() kwargs['next_view'] = CreateView return kwargs class ChangeDefinitionView(SelectDefinitionView): modal_header = _("Update Workflow") submit_url = reverse_lazy("horizon:mistral:workflows:change_definition") success_url = reverse_lazy('horizon:mistral:workflows:update') page_title = _("Update Definition") def get_form_kwargs(self): kwargs = super(ChangeDefinitionView, self).get_form_kwargs() kwargs['next_view'] = UpdateView return kwargs class CreateView(forms.ModalFormView): template_name = 'mistral/workflows/create.html' modal_header = _("Create Workflow") form_id = "create_workflow" form_class = workflow_forms.CreateForm submit_label = _("Create") submit_url = reverse_lazy("horizon:mistral:workflows:create") success_url = reverse_lazy('horizon:mistral:workflows:index') page_title = _("Create Workflow") def get_initial(self): initial = {} if 'definition' in self.kwargs: initial['definition'] = self.kwargs['definition'] return initial class UpdateView(CreateView): template_name = 'mistral/workflows/update.html' modal_header = _("Update Workflow") form_id = "update_workflow" form_class = workflow_forms.UpdateForm submit_label = _("Update") submit_url = reverse_lazy("horizon:mistral:workflows:update") page_title = _("Update Workflow") mistral-dashboard-19.0.0/releasenotes/000077500000000000000000000000001457505401100176745ustar00rootroot00000000000000mistral-dashboard-19.0.0/releasenotes/notes/000077500000000000000000000000001457505401100210245ustar00rootroot00000000000000mistral-dashboard-19.0.0/releasenotes/notes/.placeholder000066400000000000000000000000001457505401100232750ustar00rootroot00000000000000mistral-dashboard-19.0.0/releasenotes/notes/bug-1931558-4674cdde721dfab8.yaml000066400000000000000000000005101457505401100256770ustar00rootroot00000000000000--- security: - | `Bug #1931558 `_: Previosuly Mistral Dashboard leaked contents of local files if a user put in a local file path in definitions. Now Mistral Dashboard no longer treats inputs as file path or URL but it always use the raw input as resource definitions. mistral-dashboard-19.0.0/releasenotes/notes/drop-py-2-7-022d0dd59feb8b07.yaml000066400000000000000000000003401457505401100260730ustar00rootroot00000000000000--- upgrade: - | Python 2.7 support has been dropped. Last release of mistral-dashboard to support python 2.7 is OpenStack Train. The minimum version of Python now supported by mistral-dashboard is Python 3.6. mistral-dashboard-19.0.0/releasenotes/notes/drop-python-3-6-and-3-7-ae37bc21f97de767.yaml000066400000000000000000000002011457505401100300430ustar00rootroot00000000000000--- upgrade: - | Python 3.6 & 3.7 support has been dropped. The minimum version of Python now supported is Python 3.8. mistral-dashboard-19.0.0/releasenotes/source/000077500000000000000000000000001457505401100211745ustar00rootroot00000000000000mistral-dashboard-19.0.0/releasenotes/source/2024.1.rst000066400000000000000000000002021457505401100224460ustar00rootroot00000000000000=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.1 mistral-dashboard-19.0.0/releasenotes/source/_static/000077500000000000000000000000001457505401100226225ustar00rootroot00000000000000mistral-dashboard-19.0.0/releasenotes/source/_static/.placeholder000066400000000000000000000000001457505401100250730ustar00rootroot00000000000000mistral-dashboard-19.0.0/releasenotes/source/_templates/000077500000000000000000000000001457505401100233315ustar00rootroot00000000000000mistral-dashboard-19.0.0/releasenotes/source/_templates/.placeholder000066400000000000000000000000001457505401100256020ustar00rootroot00000000000000mistral-dashboard-19.0.0/releasenotes/source/conf.py000066400000000000000000000043651457505401100225030ustar00rootroot00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'oslosphinx', 'reno.sphinxext', ] # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Mistral Dashboard Release Notes' copyright = '2016, Mistral developers' # -- Options for openstackdocstheme ------------------------------------------- openstackdocs_repo_name = 'openstack/mistral-dashboard' openstackdocs_bug_project = 'mistral' openstackdocs_bug_tag = 'doc' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = [] # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] mistral-dashboard-19.0.0/releasenotes/source/index.rst000066400000000000000000000004271457505401100230400ustar00rootroot00000000000000Mistral Dashboard Release Notes =============================== Contents ======== .. toctree:: :maxdepth: 2 unreleased 2024.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata newton mitaka liberty mistral-dashboard-19.0.0/releasenotes/source/liberty.rst000066400000000000000000000002151457505401100233760ustar00rootroot00000000000000============================ Liberty Series Release Notes ============================ .. release-notes:: :branch: origin/stable/liberty mistral-dashboard-19.0.0/releasenotes/source/mitaka.rst000066400000000000000000000002111457505401100231660ustar00rootroot00000000000000=========================== Mitaka Series Release Notes =========================== .. release-notes:: :branch: origin/stable/mitaka mistral-dashboard-19.0.0/releasenotes/source/newton.rst000066400000000000000000000002111457505401100232320ustar00rootroot00000000000000=========================== Newton Series Release Notes =========================== .. release-notes:: :branch: origin/stable/newton mistral-dashboard-19.0.0/releasenotes/source/ocata.rst000066400000000000000000000002301457505401100230100ustar00rootroot00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata mistral-dashboard-19.0.0/releasenotes/source/pike.rst000066400000000000000000000002171457505401100226560ustar00rootroot00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike mistral-dashboard-19.0.0/releasenotes/source/queens.rst000066400000000000000000000002231457505401100232230ustar00rootroot00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens mistral-dashboard-19.0.0/releasenotes/source/rocky.rst000066400000000000000000000002211457505401100230500ustar00rootroot00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky mistral-dashboard-19.0.0/releasenotes/source/stein.rst000066400000000000000000000002211457505401100230430ustar00rootroot00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein mistral-dashboard-19.0.0/releasenotes/source/train.rst000066400000000000000000000001761457505401100230470ustar00rootroot00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train mistral-dashboard-19.0.0/releasenotes/source/unreleased.rst000066400000000000000000000001531457505401100240540ustar00rootroot00000000000000============================ Current Series Release Notes ============================ .. release-notes:: mistral-dashboard-19.0.0/releasenotes/source/ussuri.rst000066400000000000000000000002021457505401100232520ustar00rootroot00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri mistral-dashboard-19.0.0/releasenotes/source/victoria.rst000066400000000000000000000002121457505401100235410ustar00rootroot00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria mistral-dashboard-19.0.0/releasenotes/source/wallaby.rst000066400000000000000000000002061457505401100233570ustar00rootroot00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby mistral-dashboard-19.0.0/releasenotes/source/xena.rst000066400000000000000000000001721457505401100226610ustar00rootroot00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: stable/xena mistral-dashboard-19.0.0/releasenotes/source/yoga.rst000066400000000000000000000001721457505401100226650ustar00rootroot00000000000000========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: stable/yoga mistral-dashboard-19.0.0/releasenotes/source/zed.rst000066400000000000000000000001661457505401100225130ustar00rootroot00000000000000======================== Zed Series Release Notes ======================== .. release-notes:: :branch: stable/zed mistral-dashboard-19.0.0/requirements.txt000066400000000000000000000005431457505401100204710ustar00rootroot00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 iso8601>=0.1.11 # MIT python-mistralclient>=4.3.0 # Apache-2.0 PyYAML>=3.12 # MIT horizon>=17.1.0 # Apache-2.0 mistral-dashboard-19.0.0/run_tests.sh000077500000000000000000000313051457505401100175720ustar00rootroot00000000000000#!/bin/bash set -o errexit function usage { echo "Usage: $0 [OPTION]..." echo "Run Mistral Dashboard's test suite(s)" echo "" echo " -V, --virtual-env Always use virtualenv. Install automatically" echo " if not present" echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local" echo " environment" echo " -c, --coverage Generate reports using Coverage" echo " -f, --force Force a clean re-build of the virtual" echo " environment. Useful when dependencies have" echo " been added." echo " -m, --manage Run a Django management command." echo " --pseudo Pseudo translate a language." echo " -p, --pep8 Just run pep8" echo " -8, --pep8-changed []" echo " Just run PEP8 and HACKING compliance check" echo " on files changed since HEAD~1 (or )" echo " -P, --no-pep8 Don't run pep8 by default" echo " -t, --tabs Check for tab characters in files." echo " -y, --pylint Just run pylint" echo " -q, --quiet Run non-interactively. (Relatively) quiet." echo " Implies -V if -N is not set." echo " --only-selenium Run only the Selenium unit tests" echo " --with-selenium Run unit tests including Selenium tests" echo " --selenium-headless Run Selenium tests headless" echo " --runserver Run the Django development server for" echo " mistraldashboard in the virtual" echo " environment." echo " --docs Just build the documentation" echo " --backup-environment Make a backup of the environment on exit" echo " --restore-environment Restore the environment before running" echo " --destroy-environment Destroy the environment and exit" echo " -h, --help Print this usage message" echo "" echo "Note: with no options specified, the script will try to run the tests in" echo " a virtual environment, If no virtualenv is found, the script will ask" echo " if you would like to create one. If you prefer to run tests NOT in a" echo " virtual environment, simply pass the -N option." exit } # DEFAULTS FOR RUN_TESTS.SH # root=`pwd -P` venv=$root/.venv venv_env_version=$venv/environments with_venv=tools/with_venv.sh included_dirs="mistraldashboard" always_venv=0 backup_env=0 command_wrapper="" destroy=0 force=0 just_pep8=0 just_pep8_changed=0 no_pep8=0 just_pylint=0 just_docs=0 just_tabs=0 never_venv=0 quiet=0 restore_env=0 runserver=0 only_selenium=0 with_selenium=0 selenium_headless=0 testopts="" testargs="" with_coverage=0 check_only=0 pseudo=0 manage=0 # Jenkins sets a "JOB_NAME" variable, if it's not set, we'll make it "default" [ "$JOB_NAME" ] || JOB_NAME="default" function process_option { # If running manage command, treat the rest of options as arguments. if [ $manage -eq 1 ]; then testargs="$testargs $1" return 0 fi case "$1" in -h|--help) usage;; -V|--virtual-env) always_venv=1; never_venv=0;; -N|--no-virtual-env) always_venv=0; never_venv=1;; -p|--pep8) just_pep8=1;; -8|--pep8-changed) just_pep8_changed=1;; -P|--no-pep8) no_pep8=1;; -y|--pylint) just_pylint=1;; -f|--force) force=1;; -t|--tabs) just_tabs=1;; -q|--quiet) quiet=1;; -c|--coverage) with_coverage=1;; -m|--manage) manage=1;; --pseudo) pseudo=1;; --only-selenium) only_selenium=1;; --with-selenium) with_selenium=1;; --selenium-headless) selenium_headless=1;; --docs) just_docs=1;; --runserver) runserver=1;; --backup-environment) backup_env=1;; --restore-environment) restore_env=1;; --destroy-environment) destroy=1;; -*) testopts="$testopts $1";; *) testargs="$testargs $1" esac } function run_management_command { ${command_wrapper} python $root/manage.py $testopts $testargs } function run_server { echo "Starting Django development server..." ${command_wrapper} python $root/manage.py runserver $testopts $testargs echo "Server stopped." } function run_pylint { echo "Running pylint ..." PYTHONPATH=$root ${command_wrapper} pylint --rcfile=.pylintrc -f parseable $included_dirs > pylint.txt || true CODE=$? grep Global -A2 pylint.txt if [ $CODE -lt 32 ]; then echo "Completed successfully." exit 0 else echo "Completed with problems." exit $CODE fi } function warn_on_flake8_without_venv { set +o errexit ${command_wrapper} python -c "import hacking" 2>/dev/null no_hacking=$? set -o errexit if [ $never_venv -eq 1 -a $no_hacking -eq 1 ]; then echo "**WARNING**:" >&2 echo "OpenStack hacking is not installed on your host. Its detection will be missed." >&2 echo "Please install or use virtual env if you need OpenStack hacking detection." >&2 fi } function run_pep8 { echo "Running flake8 ..." warn_on_flake8_without_venv DJANGO_SETTINGS_MODULE=mistraldashboard.test.settings ${command_wrapper} flake8 $included_dirs } function run_pep8_changed { local base_commit=${testargs:-HEAD~1} files=$(git diff --name-only $base_commit | tr '\n' ' ') echo "Running flake8 on ${files}" warn_on_flake8_without_venv diff -u --from-file /dev/null ${files} | DJANGO_SETTINGS_MODULE=mistraldashboard.test.settings ${command_wrapper} flake8 --diff exit } function run_sphinx { echo "Building sphinx..." export DJANGO_SETTINGS_MODULE=mistraldashboard.test.settings ${command_wrapper} sphinx-build -b html doc/source doc/build/html echo "Build complete." } function tab_check { TAB_VIOLATIONS=`find $included_dirs -type f -regex ".*\.\(css\|js\|py\|html\)" -print0 | xargs -0 awk '/\t/' | wc -l` if [ $TAB_VIOLATIONS -gt 0 ]; then echo "TABS! $TAB_VIOLATIONS of them! Oh no!" HORIZON_FILES=`find $included_dirs -type f -regex ".*\.\(css\|js\|py|\html\)"` for TABBED_FILE in $HORIZON_FILES do TAB_COUNT=`awk '/\t/' $TABBED_FILE | wc -l` if [ $TAB_COUNT -gt 0 ]; then echo "$TABBED_FILE: $TAB_COUNT" fi done fi return $TAB_VIOLATIONS; } function destroy_venv { echo "Cleaning environment..." echo "Removing virtualenv..." rm -rf $venv echo "Virtualenv removed." } function environment_check { echo "Checking environment." if [ -f $venv_env_version ]; then set +o errexit cat requirements.txt test-requirements.txt | cmp $venv_env_version - > /dev/null local env_check_result=$? set -o errexit if [ $env_check_result -eq 0 ]; then # If the environment exists and is up-to-date then set our variables command_wrapper="${root}/${with_venv}" echo "Environment is up to date." return 0 fi fi if [ $always_venv -eq 1 ]; then install_venv else if [ ! -e ${venv} ]; then echo -e "Environment not found. Install? (Y/n) \c" else echo -e "Your environment appears to be out of date. Update? (Y/n) \c" fi read update_env if [ "x$update_env" = "xY" -o "x$update_env" = "x" -o "x$update_env" = "xy" ]; then install_venv else # Set our command wrapper anyway. command_wrapper="${root}/${with_venv}" fi fi } function sanity_check { # Anything that should be determined prior to running the tests, server, etc. # Don't sanity-check anything environment-related in -N flag is set if [ $never_venv -eq 0 ]; then if [ ! -e ${venv} ]; then echo "Virtualenv not found at $venv. Did install_venv.py succeed?" exit 1 fi fi # Remove .pyc files. This is sanity checking because they can linger # after old files are deleted. find . -name "*.pyc" -exec rm -rf {} \; } function backup_environment { if [ $backup_env -eq 1 ]; then echo "Backing up environment \"$JOB_NAME\"..." if [ ! -e ${venv} ]; then echo "Environment not installed. Cannot back up." return 0 fi if [ -d /tmp/.mistral_dashboard_environment/$JOB_NAME ]; then mv /tmp/.mistral_dashboard_environment/$JOB_NAME /tmp/.mistral_dashboard_environment/$JOB_NAME.old rm -rf /tmp/.mistral_dashboard_environment/$JOB_NAME fi mkdir -p /tmp/.mistral_dashboard_environment/$JOB_NAME cp -r $venv /tmp/.mistral_dashboard_environment/$JOB_NAME/ cp .environment_version /tmp/.mistral_dashboard_environment/$JOB_NAME/ # Remove the backup now that we've completed successfully rm -rf /tmp/.mistral_dashboard_environment/$JOB_NAME.old echo "Backup completed" fi } function restore_environment { if [ $restore_env -eq 1 ]; then echo "Restoring environment from backup..." if [ ! -d /tmp/.mistral_dashboard_environment/$JOB_NAME ]; then echo "No backup to restore from." return 0 fi cp -r /tmp/.mistral_dashboard_environment/$JOB_NAME/.venv ./ || true echo "Environment restored successfully." fi } function install_venv { # Install with install_venv.py export PIP_DOWNLOAD_CACHE=${PIP_DOWNLOAD_CACHE-/tmp/.pip_download_cache} export PIP_USE_MIRRORS=true if [ $quiet -eq 1 ]; then export PIP_NO_INPUT=true fi echo "Fetching new src packages..." rm -rf $venv/src python tools/install_venv.py command_wrapper="$root/${with_venv}" # Make sure it worked and record the environment version sanity_check chmod -R 754 $venv cat requirements.txt test-requirements.txt > $venv_env_version } function run_tests { sanity_check if [ $with_selenium -eq 1 ]; then export WITH_SELENIUM=1 elif [ $only_selenium -eq 1 ]; then export WITH_SELENIUM=1 export SKIP_UNITTESTS=1 fi if [ $selenium_headless -eq 1 ]; then export SELENIUM_HEADLESS=1 fi if [ -z "$testargs" ]; then run_tests_all else run_tests_subset fi } function run_tests_subset { project=`echo $testargs | awk -F. '{print $1}'` ${command_wrapper} python $root/manage.py test --settings=$project.test.settings $testopts $testargs } function run_tests_all { echo "Running Mistral-Dashboard application tests" export NOSE_XUNIT_FILE=mistraldashboard/nosetests.xml if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then export NOSE_HTML_OUT_FILE='mistral_dashboard_nose_results.html' fi if [ $with_coverage -eq 1 ]; then ${command_wrapper} python -m coverage.__main__ erase coverage_run="python -m coverage.__main__ run -p" fi ${command_wrapper} ${coverage_run} $root/manage.py test mistraldashboard --settings=mistraldashboard.test.settings $testopts # get results of the Horizon tests TUSKAR_UI_RESULT=$? if [ $with_coverage -eq 1 ]; then echo "Generating coverage reports" ${command_wrapper} python -m coverage.__main__ combine ${command_wrapper} python -m coverage.__main__ xml -i --include="mistraldashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' ${command_wrapper} python -m coverage.__main__ html -i --include="mistraldashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' -d reports fi # Remove the leftover coverage files from the -p flag earlier. rm -f .coverage.* PEP8_RESULT=0 if [ $only_selenium -eq 0 ]; then run_pep8 PEP8_RESULT=$? fi TEST_RESULT=$(($TUSKAR_UI_RESULT || $PEP8_RESULT)) if [ $TEST_RESULT -eq 0 ]; then echo "Tests completed successfully." else echo "Tests failed." fi exit $TEST_RESULT } # ---------PREPARE THE ENVIRONMENT------------ # # PROCESS ARGUMENTS, OVERRIDE DEFAULTS for arg in "$@"; do process_option $arg done if [ $quiet -eq 1 ] && [ $never_venv -eq 0 ] && [ $always_venv -eq 0 ] then always_venv=1 fi # If destroy is set, just blow it away and exit. if [ $destroy -eq 1 ]; then destroy_venv exit 0 fi # Ignore all of this if the -N flag was set if [ $never_venv -eq 0 ]; then # Restore previous environment if desired if [ $restore_env -eq 1 ]; then restore_environment fi # Remove the virtual environment if --force used if [ $force -eq 1 ]; then destroy_venv fi # Then check if it's up-to-date environment_check # Create a backup of the up-to-date environment if desired if [ $backup_env -eq 1 ]; then backup_environment fi fi # ---------EXERCISE THE CODE------------ # # Run management commands if [ $manage -eq 1 ]; then run_management_command exit $? fi # Build the docs if [ $just_docs -eq 1 ]; then run_sphinx exit $? fi # PEP8 if [ $just_pep8 -eq 1 ]; then run_pep8 exit $? fi if [ $just_pep8_changed -eq 1 ]; then run_pep8_changed exit $? fi # Pylint if [ $just_pylint -eq 1 ]; then run_pylint exit $? fi # Tab checker if [ $just_tabs -eq 1 ]; then tab_check exit $? fi # Django development server if [ $runserver -eq 1 ]; then run_server exit $? fi # Full test suite run_tests || exit mistral-dashboard-19.0.0/setup.cfg000066400000000000000000000015771457505401100170360ustar00rootroot00000000000000[metadata] name = mistral-dashboard summary = Mistral dashboard description_file = README.rst license = Apache License, Version 2.0 python_requires = >=3.8 classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/mistral/latest/ [global] setup_hooks = pbr.hooks.setup_hook [files] packages = mistraldashboard [upload_sphinx] upload_dir = doc/build/html mistral-dashboard-19.0.0/setup.py000066400000000000000000000020061457505401100167130ustar00rootroot00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. # solution from: http://bugs.python.org/issue15881#msg170215 try: import multiprocessing # noqa except ImportError: pass setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) mistral-dashboard-19.0.0/test-requirements.txt000066400000000000000000000010301457505401100214360ustar00rootroot00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. # hacking should appear first in case something else depends on pep8 hacking>=4.0.0,<4.1.0 # Apache-2.0 # Testing Requirements coverage!=4.4,>=4.0 # Apache-2.0 nodeenv>=0.9.4 # BSD selenium>=2.50.1 # Apache-2.0 xvfbwrapper>=0.1.3 #license: MIT stestr>=2.0.0 # Apache-2.0 # Horizon requirements django-compressor>=2.0 # MIT mistral-dashboard-19.0.0/tox.ini000066400000000000000000000030141457505401100165140ustar00rootroot00000000000000[tox] minversion = 2.0 envlist = pep8,py3 ignore_basepython_conflict = true [testenv] basepython = python3 usedevelop = True setenv = VIRTUAL_ENV={envdir} deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = find . -type f -name "*.pyc" -delete /bin/bash run_tests.sh -N --no-pep8 {posargs} Allowlist_externals = find /bin/bash [testenv:pep8] commands = flake8 [testenv:venv] commands = {posargs} [testenv:cover] setenv = PYTHON=coverage run --source mistraldashboard --parallel-mode commands = stestr run '{posargs}' coverage combine coverage html -d cover coverage xml -o cover/coverage.xml [testenv:docs] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html [testenv:debug] Allowlist_externals = oslo_debug_helper commands = oslo_debug_helper -t mistraldashboard/test {posargs} [flake8] show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,.ropeproject,tools,horizon [testenv:releasenotes] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html