From 37a1e5d9b014fa43406068378ad9369aeb571871 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 21 Nov 2019 18:18:36 -0500 Subject: [PATCH 1/5] First pass workflow viz --- .../forms/workflow-node-form.controller.js | 8 +- .../forms/workflow-node-form.partial.html | 2 +- awx/ui_next/package-lock.json | 289 ++++++- awx/ui_next/package.json | 2 + .../src/api/models/WorkflowJobTemplates.js | 4 + .../src/components/AppendBody/AppendBody.jsx | 24 + .../src/components/AppendBody/index.js | 1 + .../src/components/FullPage/FullPage.jsx | 11 + awx/ui_next/src/components/FullPage/index.js | 1 + .../JobTemplateDetail/JobTemplateDetail.jsx | 2 +- .../Template/TemplateList/TemplateList.jsx | 10 +- .../src/screens/Template/Templates.jsx | 21 +- .../screens/Template/WorkflowJobTemplate.jsx | 156 ++++ .../WorkflowJobTemplateDetail.jsx | 18 + .../WorkflowJobTemplateDetail/index.js | 1 + .../WorkflowJobTemplateVisualizer/Graph.jsx | 756 ++++++++++++++++++ .../StartScreen.jsx | 47 ++ .../WorkflowJobTemplateVisualizer/Toolbar.jsx | 93 +++ .../Visualizer.jsx | 205 +++++ .../WorkflowHelp.jsx | 30 + .../WorkflowHelpDetails.jsx | 95 +++ .../WorkflowJobTemplateVisualizer/index.js | 6 + 22 files changed, 1769 insertions(+), 13 deletions(-) create mode 100644 awx/ui_next/src/components/AppendBody/AppendBody.jsx create mode 100644 awx/ui_next/src/components/AppendBody/index.js create mode 100644 awx/ui_next/src/components/FullPage/FullPage.jsx create mode 100644 awx/ui_next/src/components/FullPage/index.js create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/index.js create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Graph.jsx create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/StartScreen.jsx create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Toolbar.jsx create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/WorkflowHelp.jsx create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/WorkflowHelpDetails.jsx create mode 100644 awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/index.js diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js index 136b2e6cdf..911dc88511 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js @@ -31,7 +31,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService launchConf.variables_needed_to_start.length !== 0; $scope.strings = TemplatesStrings; - $scope.editNodeHelpMessage = null; + $scope.editWorkflowHelpMessage = null; $scope.templateList = WorkflowNodeFormService.templateListDefinition(); $scope.inventorySourceList = WorkflowNodeFormService.inventorySourceListDefinition(); @@ -177,7 +177,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService } }; - const getEditNodeHelpMessage = (selectedTemplate, workflowJobTemplateObj) => { + const getEditWorkflowHelpMessage = (selectedTemplate, workflowJobTemplateObj) => { if (selectedTemplate) { if (selectedTemplate.type === "workflow_job_template") { if (workflowJobTemplateObj.inventory && selectedTemplate.ask_inventory_on_launch) { @@ -215,7 +215,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService const ujt = _.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject'); const templateType = _.get(ujt, 'type'); - $scope.editNodeHelpMessage = getEditNodeHelpMessage(ujt, $scope.workflowJobTemplateObj); + $scope.editWorkflowHelpMessage = getEditWorkflowHelpMessage(ujt, $scope.workflowJobTemplateObj); if (!$scope.readOnly) { let jobTemplate = templateType === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate(); @@ -670,7 +670,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService timeoutMinutes: 0, timeoutSeconds: 0 }; - $scope.editNodeHelpMessage = getEditNodeHelpMessage(selectedTemplate, $scope.workflowJobTemplateObj); + $scope.editWorkflowHelpMessage = getEditWorkflowHelpMessage(selectedTemplate, $scope.workflowJobTemplateObj); if (selectedTemplate.type === "job_template" || selectedTemplate.type === "workflow_job_template") { let jobTemplate = selectedTemplate.type === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate(); diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html index b79ca9e79b..841bc559c6 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html @@ -279,7 +279,7 @@ -
+

diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json index a10b4c184e..0a802c8e9a 100644 --- a/awx/ui_next/package-lock.json +++ b/awx/ui_next/package-lock.json @@ -4881,8 +4881,7 @@ "commander": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", - "dev": true + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" }, "commondir": { "version": "1.0.1", @@ -5333,6 +5332,279 @@ "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", "dev": true }, + "d3": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.12.0.tgz", + "integrity": "sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.3.tgz", + "integrity": "sha512-v8bbYyCFKjyCzFk/tdWqXwDykY8YWqhXYjcYxfILIit085VZOpj4XJKOMccTsvWxgzSLMJQg5SiqHjslsipEDg==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.3.0.tgz", + "integrity": "sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz", + "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==" + }, + "d3-drag": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.4.tgz", + "integrity": "sha512-ICPurDETFAelF1CTHdIyiUM4PsyZLaM+7oIBhmyP+cuVjze5vDZ8V//LdOFjg0jGnFIZD/Sfmk0r95PSiu78rw==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz", + "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz", + "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==" + }, + "d3-fetch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", + "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.1.tgz", + "integrity": "sha512-TUswGe6hfguUX1CtKxyG2nymO+1lyThbkS1ifLX0Sr+dOQtAD5gkrffpHnx+yHNKUZ0Bmg5T4AjUQwugPDrm0g==" + }, + "d3-geo": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz", + "integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", + "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w==" + }, + "d3-interpolate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", + "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", + "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==" + }, + "d3-polygon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", + "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==" + }, + "d3-quadtree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz", + "integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz", + "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==" + }, + "d3-shape": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", + "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz", + "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==" + }, + "d3-time-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", + "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz", + "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==" + }, + "d3-transition": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz", + "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "dagre": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.4.tgz", + "integrity": "sha512-Dj0csFDrWYKdavwROb9FccHfTC4fJbyF/oJdL9LNZJ8WUvl968P6PAKEriGqfbdArVJEmmfA+UyumgWEwcHU6A==", + "requires": { + "graphlib": "^2.1.7", + "lodash": "^4.17.4" + } + }, "damerau-levenshtein": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", @@ -8149,6 +8421,14 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, + "graphlib": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.7.tgz", + "integrity": "sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w==", + "requires": { + "lodash": "^4.17.5" + } + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -14260,6 +14540,11 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, "rx": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json index 957f796ffa..8ae0a4d2be 100644 --- a/awx/ui_next/package.json +++ b/awx/ui_next/package.json @@ -65,6 +65,8 @@ "ansi-to-html": "^0.6.11", "axios": "^0.18.1", "codemirror": "^5.47.0", + "d3": "^5.12.0", + "dagre": "^0.8.4", "formik": "^1.5.1", "has-ansi": "^3.0.0", "html-entities": "^1.2.1", diff --git a/awx/ui_next/src/api/models/WorkflowJobTemplates.js b/awx/ui_next/src/api/models/WorkflowJobTemplates.js index 92bbccd9b0..07da2531f4 100644 --- a/awx/ui_next/src/api/models/WorkflowJobTemplates.js +++ b/awx/ui_next/src/api/models/WorkflowJobTemplates.js @@ -5,6 +5,10 @@ class WorkflowJobTemplates extends Base { super(http); this.baseUrl = '/api/v2/workflow_job_templates/'; } + + readNodes(id, params) { + return this.http.get(`${this.baseUrl}${id}/workflow_nodes/`, { params }); + } } export default WorkflowJobTemplates; diff --git a/awx/ui_next/src/components/AppendBody/AppendBody.jsx b/awx/ui_next/src/components/AppendBody/AppendBody.jsx new file mode 100644 index 0000000000..62e2551372 --- /dev/null +++ b/awx/ui_next/src/components/AppendBody/AppendBody.jsx @@ -0,0 +1,24 @@ +import { Component } from 'react'; +import ReactDOM from 'react-dom'; + +class AppendBody extends Component { + constructor(props) { + super(props); + this.el = document.createElement('div'); + } + + componentDidMount() { + document.body.appendChild(this.el); + } + + componentWillUnmount() { + document.body.removeChild(this.el); + } + + render() { + const { children } = this.props; + return ReactDOM.createPortal(children, this.el); + } +} + +export default AppendBody; diff --git a/awx/ui_next/src/components/AppendBody/index.js b/awx/ui_next/src/components/AppendBody/index.js new file mode 100644 index 0000000000..d2c2b8ef93 --- /dev/null +++ b/awx/ui_next/src/components/AppendBody/index.js @@ -0,0 +1 @@ +export { default } from './AppendBody'; diff --git a/awx/ui_next/src/components/FullPage/FullPage.jsx b/awx/ui_next/src/components/FullPage/FullPage.jsx new file mode 100644 index 0000000000..4a1fdb9f1a --- /dev/null +++ b/awx/ui_next/src/components/FullPage/FullPage.jsx @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +export default styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: white; + z-index: 300; +`; diff --git a/awx/ui_next/src/components/FullPage/index.js b/awx/ui_next/src/components/FullPage/index.js new file mode 100644 index 0000000000..71044fd052 --- /dev/null +++ b/awx/ui_next/src/components/FullPage/index.js @@ -0,0 +1 @@ +export { default } from './FullPage'; diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx index 480abc146f..fbd5d9e1bf 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -350,7 +350,7 @@ class JobTemplateDetail extends Component { {summary_fields.user_capabilities.edit && ( + + +
+ ); +} + +export default withI18n()(StartScreen); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Toolbar.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Toolbar.jsx new file mode 100644 index 0000000000..c4579f1710 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Toolbar.jsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Badge as PFBadge, Button } from '@patternfly/react-core'; +import { + BookIcon, + CompassIcon, + DownloadIcon, + RocketIcon, + TimesIcon, + TrashAltIcon, + WrenchIcon, +} from '@patternfly/react-icons'; +import VerticalSeparator from '@components/VerticalSeparator'; +import styled from 'styled-components'; + +const Badge = styled(PFBadge)` + align-items: center; + display: flex; + justify-content: center; + margin-left: 10px; +`; + +function Toolbar({ history, i18n, template }) { + const handleVisualizerCancel = () => { + history.push(`/templates/workflow_job_template/${template.id}/details`); + }; + + return ( +
+
+
+ Workflow Visualizer + + {template.name} +
+
+
Total Nodes
+ 0 + + + + + + + + + + + +
+
+
+ ); +} + +export default withI18n()(withRouter(Toolbar)); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx new file mode 100644 index 0000000000..fbd2dde245 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx @@ -0,0 +1,205 @@ +import React, { useState, useEffect } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import styled from 'styled-components'; +import ContentError from '@components/ContentError'; +import ContentLoading from '@components/ContentLoading'; +import Graph from './Graph'; +import StartScreen from './StartScreen'; +import Toolbar from './Toolbar'; +import { WorkflowJobTemplatesAPI } from '@api'; + +const CenteredContent = styled.div` + display: flex; + flex-flow: column; + height: 100%; + align-items: center; + justify-content: center; +`; + +const VisualizerLayout = styled.div` + display: flex; + flex-flow: column; + height: 100%; +`; + +const fetchWorkflowNodes = async (templateId, pageNo = 1, nodes = []) => { + try { + const { data } = await WorkflowJobTemplatesAPI.readNodes(templateId, { + page_size: 200, + page: pageNo, + }); + if (data.next) { + return await fetchWorkflowNodes( + templateId, + pageNo + 1, + nodes.concat(data.results) + ); + } + return nodes.concat(data.results); + } catch (error) { + throw error; + } +}; + +function Visualizer({ template, i18n }) { + const [contentError, setContentError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [graphLinks, setGraphLinks] = useState([]); + // We'll also need to store the original set of nodes... + const [graphNodes, setGraphNodes] = useState([]); + + useEffect(() => { + const buildGraphArrays = nodes => { + const nonRootNodeIds = []; + const allNodeIds = []; + const arrayOfLinksForChart = []; + const nodeIdToChartNodeIdMapping = {}; + const chartNodeIdToIndexMapping = {}; + const nodeRef = {}; + let nodeIdCounter = 1; + const arrayOfNodesForChart = [ + { + id: nodeIdCounter, + unifiedJobTemplate: { + name: i18n._(t`START`), + }, + type: 'node', + }, + ]; + nodeIdCounter++; + // Assign each node an ID - 0 is reserved for the start node. We need to + // make sure that we have an ID on every node including new nodes so the + // ID returned by the api won't do + nodes.forEach(node => { + node.workflowMakerNodeId = nodeIdCounter; + nodeRef[nodeIdCounter] = { + originalNodeObject: node, + }; + + const nodeObj = { + index: nodeIdCounter - 1, + id: nodeIdCounter, + type: 'node', + }; + + if (node.summary_fields.job) { + nodeObj.job = node.summary_fields.job; + } + if (node.summary_fields.unified_job_template) { + nodeRef[nodeIdCounter].unifiedJobTemplate = + node.summary_fields.unified_job_template; + nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template; + } + + arrayOfNodesForChart.push(nodeObj); + allNodeIds.push(node.id); + nodeIdToChartNodeIdMapping[node.id] = node.workflowMakerNodeId; + chartNodeIdToIndexMapping[nodeIdCounter] = nodeIdCounter - 1; + nodeIdCounter++; + }); + + nodes.forEach(node => { + const sourceIndex = chartNodeIdToIndexMapping[node.workflowMakerNodeId]; + node.success_nodes.forEach(nodeId => { + const targetIndex = + chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[sourceIndex], + target: arrayOfNodesForChart[targetIndex], + edgeType: 'success', + type: 'link', + }); + nonRootNodeIds.push(nodeId); + }); + node.failure_nodes.forEach(nodeId => { + const targetIndex = + chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[sourceIndex], + target: arrayOfNodesForChart[targetIndex], + edgeType: 'failure', + type: 'link', + }); + nonRootNodeIds.push(nodeId); + }); + node.always_nodes.forEach(nodeId => { + const targetIndex = + chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[sourceIndex], + target: arrayOfNodesForChart[targetIndex], + edgeType: 'always', + type: 'link', + }); + nonRootNodeIds.push(nodeId); + }); + }); + + const uniqueNonRootNodeIds = Array.from(new Set(nonRootNodeIds)); + + const rootNodes = allNodeIds.filter( + nodeId => !uniqueNonRootNodeIds.includes(nodeId) + ); + + rootNodes.forEach(rootNodeId => { + const targetIndex = + chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[rootNodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[0], + target: arrayOfNodesForChart[targetIndex], + edgeType: 'always', + type: 'link', + }); + }); + + setGraphNodes(arrayOfNodesForChart); + setGraphLinks(arrayOfLinksForChart); + }; + + async function fetchData() { + try { + const nodes = await fetchWorkflowNodes(template.id); + buildGraphArrays(nodes); + } catch (error) { + setContentError(error); + } finally { + setIsLoading(false); + } + } + fetchData(); + }, [template.id, i18n]); + + if (isLoading) { + return ( + + + + ); + } + + if (contentError) { + return ( + + + + ); + } + + return ( + + + {graphLinks.length > 0 ? ( + + ) : ( + + )} + + ); +} + +export default withI18n()(Visualizer); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/WorkflowHelp.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/WorkflowHelp.jsx new file mode 100644 index 0000000000..4ddd094e40 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/WorkflowHelp.jsx @@ -0,0 +1,30 @@ +import React, { Fragment } from 'react'; +import styled from 'styled-components'; + +const Outer = styled.div` + position: relative; + height: 0; +`; + +const Inner = styled.div` + position: absolute; + left: 10px; + top: 10px; + background-color: #383f44; + color: white; + padding: 5px 10px; + border-radius: 2px; + max-width: 300px; +`; + +function WorkflowHelp({ children }) { + return ( + + + {children} + + + ); +} + +export default WorkflowHelp; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/WorkflowHelpDetails.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/WorkflowHelpDetails.jsx new file mode 100644 index 0000000000..6080de3af4 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/WorkflowHelpDetails.jsx @@ -0,0 +1,95 @@ +import React, { Fragment } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import styled from 'styled-components'; + +const GridDL = styled.dl` + display: grid; + grid-template-columns: max-content; + column-gap: 15px; + row-gap: 0px; + + dt { + grid-column-start: 1; + } + + dd { + grid-column-start: 2; + } +`; + +function WorkflowHelpDetails({ d, i18n }) { + const rows = []; + + if (d.type === 'link') { + let linkType; + switch (d.edgeType) { + case 'always': + linkType = i18n._(t`Always`); + break; + case 'success': + linkType = i18n._(t`On Success`); + break; + case 'failure': + linkType = i18n._(t`On Failure`); + break; + default: + linkType = ''; + } + + rows.push({ + label: i18n._(t`Run`), + value: linkType, + }); + } else if (d.type === 'node') { + if (d.unifiedJobTemplate) { + rows.push({ + label: i18n._(t`Name`), + value: d.unifiedJobTemplate.name, + }); + + let nodeType; + switch (d.unifiedJobTemplate.unified_job_type) { + case 'job': + nodeType = i18n._(t`Job Template`); + break; + case 'workflow_job': + nodeType = i18n._(t`Workflow Job Template`); + break; + case 'project_update': + nodeType = i18n._(t`Project Update`); + break; + case 'inventory_update': + nodeType = i18n._(t`Inventory Update`); + break; + case 'workflow_approval': + nodeType = i18n._(t`Workflow Approval`); + break; + default: + nodeType = ''; + } + + rows.push({ + label: i18n._(t`Type`), + value: nodeType, + }); + } else { + // todo: this scenario (deleted) + } + } + + return ( + + {rows.map(row => ( + +
+ {row.label} +
+
{row.value}
+
+ ))} +
+ ); +} + +export default withI18n()(WorkflowHelpDetails); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/index.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/index.js new file mode 100644 index 0000000000..f7f95d4961 --- /dev/null +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/index.js @@ -0,0 +1,6 @@ +export { default as Visualizer } from './Visualizer'; +export { default as Toolbar } from './Toolbar'; +export { default as Graph } from './Graph'; +export { default as StartScreen } from './StartScreen'; +export { default as WorkflowHelp } from './WorkflowHelp'; +export { default as WorkflowHelpDetails } from './WorkflowHelpDetails'; From 61c38eabf81923f16cbb9ccb957f66e6f578c1ce Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 21 Nov 2019 18:22:05 -0500 Subject: [PATCH 2/5] Revert inadvertent variable name changes in old ui app --- .../workflow-maker/forms/workflow-node-form.controller.js | 8 ++++---- .../workflow-maker/forms/workflow-node-form.partial.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js index 911dc88511..136b2e6cdf 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js @@ -31,7 +31,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService launchConf.variables_needed_to_start.length !== 0; $scope.strings = TemplatesStrings; - $scope.editWorkflowHelpMessage = null; + $scope.editNodeHelpMessage = null; $scope.templateList = WorkflowNodeFormService.templateListDefinition(); $scope.inventorySourceList = WorkflowNodeFormService.inventorySourceListDefinition(); @@ -177,7 +177,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService } }; - const getEditWorkflowHelpMessage = (selectedTemplate, workflowJobTemplateObj) => { + const getEditNodeHelpMessage = (selectedTemplate, workflowJobTemplateObj) => { if (selectedTemplate) { if (selectedTemplate.type === "workflow_job_template") { if (workflowJobTemplateObj.inventory && selectedTemplate.ask_inventory_on_launch) { @@ -215,7 +215,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService const ujt = _.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject'); const templateType = _.get(ujt, 'type'); - $scope.editWorkflowHelpMessage = getEditWorkflowHelpMessage(ujt, $scope.workflowJobTemplateObj); + $scope.editNodeHelpMessage = getEditNodeHelpMessage(ujt, $scope.workflowJobTemplateObj); if (!$scope.readOnly) { let jobTemplate = templateType === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate(); @@ -670,7 +670,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService timeoutMinutes: 0, timeoutSeconds: 0 }; - $scope.editWorkflowHelpMessage = getEditWorkflowHelpMessage(selectedTemplate, $scope.workflowJobTemplateObj); + $scope.editNodeHelpMessage = getEditNodeHelpMessage(selectedTemplate, $scope.workflowJobTemplateObj); if (selectedTemplate.type === "job_template" || selectedTemplate.type === "workflow_job_template") { let jobTemplate = selectedTemplate.type === "workflow_job_template" ? new WorkflowJobTemplate() : new JobTemplate(); diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html index 841bc559c6..b79ca9e79b 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html @@ -279,7 +279,7 @@ -
+

From 2506db88f2a430e539ae4e0fd7a25181b3a5f522 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 2 Dec 2019 14:21:58 -0500 Subject: [PATCH 3/5] Ellipsis workflow node names that are too long to fit on the node --- .../WorkflowJobTemplateVisualizer/Graph.jsx | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Graph.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Graph.jsx index 85b2b50559..dea14d0041 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Graph.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Graph.jsx @@ -10,7 +10,7 @@ import WorkflowHelpDetails from './WorkflowHelpDetails'; const SVG = styled.svg` display: flex; height: 100%; - background-color: #F6F6F6; + background-color: #f6f6f6; .WorkflowChart-tooltip { padding-left: 5px; @@ -36,34 +36,36 @@ const SVG = styled.svg` } .WorkflowChart-action--add:hover { - background-color: #58B957; + background-color: #58b957; } - .WorkflowChart-action--edit:hover,.WorkflowChart-action--link:hover,.WorkflowChart-action--details:hover { - background-color: #0279BC; + .WorkflowChart-action--edit:hover, + .WorkflowChart-action--link:hover, + .WorkflowChart-action--details:hover { + background-color: #0279bc; } .WorkflowChart-action--delete:hover { - background-color: #D9534F; + background-color: #d9534f; } .WorkflowChart-tooltipArrows { - width 10px; + width: 10px; } .WorkflowChart-tooltipArrows--outer { - position:absolute; + position: absolute; top: calc(50% - 10px); width: 0; height: 0; - border-right: 10px solid #C4C4C4; + border-right: 10px solid #c4c4c4; border-top: 10px solid transparent; border-bottom: 10px solid transparent; margin: auto; } .WorkflowChart-tooltipArrows--inner { - position:absolute; + position: absolute; top: calc(50% - 10px); left: 6px; width: 0; @@ -76,7 +78,7 @@ const SVG = styled.svg` .WorkflowChart-tooltipActions { background-color: white; - border: 1px solid #C4C4C4; + border: 1px solid #c4c4c4; border-radius: 2px; padding: 5px; } @@ -84,6 +86,18 @@ const SVG = styled.svg` .WorkflowChart-tooltipContents { display: flex; } + + .WorkflowChart-nameText { + font-size: 13px; + padding: 0px 10px; + text-align: center; + p { + margin-top: 20px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } `; function Graph({ links, nodes, readOnly, i18n }) { @@ -487,20 +501,17 @@ function Graph({ links, nodes, readOnly, i18n }) { }); nodeRef - .append('text') - .attr('x', () => { - return nodeW / 2; - }) - .attr('y', () => { - return nodeH / 2; - }) - .attr('dy', '.35em') - .attr('text-anchor', 'middle') - .attr('class', 'WorkflowChart-defaultText WorkflowChart-nameText') - .text(d => - d.unifiedJobTemplate && d.unifiedJobTemplate.name - ? d.unifiedJobTemplate.name - : 'NO NAME' + .append('foreignObject') + .attr('width', nodeW) + .attr('height', nodeH) + .attr('class', 'WorkflowChart-nameText') + .html( + d => + `

${ + d.unifiedJobTemplate + ? d.unifiedJobTemplate.name + : i18n._(t`DELETED`) + }

` ); nodeRef From 9bdd49bec553240c00464700c597d2266e1cc019 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 2 Dec 2019 14:27:40 -0500 Subject: [PATCH 4/5] Adds translations for missing strings in Toolbar.jsx --- .../Template/WorkflowJobTemplateVisualizer/Toolbar.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Toolbar.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Toolbar.jsx index c4579f1710..dee2e8130b 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Toolbar.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Toolbar.jsx @@ -39,7 +39,7 @@ function Toolbar({ history, i18n, template }) { }} >
- Workflow Visualizer + {i18n._(t`Workflow Visualizer`)} {template.name}
@@ -51,7 +51,7 @@ function Toolbar({ history, i18n, template }) { alignItems: 'center', }} > -
Total Nodes
+
{i18n._(t`Total Nodes`)}
0