From 4a801c60b974e7b0ec7a589dfdca581cfc947e3c Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 18 Jul 2019 15:36:01 -0400 Subject: [PATCH] Cleanup and changes to the way approval templates are created --- .../features/templates/templates.strings.js | 5 +- awx/ui/client/src/templates/main.js | 316 +------ .../client/src/templates/templates.service.js | 10 +- .../workflow-chart/workflow-chart.block.less | 1 + .../workflow-chart.directive.js | 801 +++++++++--------- .../templates/workflows/workflow-key/main.js | 11 + .../workflow-key/workflow-key.directive.js | 18 + .../workflow-key/workflow-key.partial.html | 43 + .../workflows/workflow-maker/forms/main.js | 4 +- .../forms/workflow-node-form.service.js | 67 ++ .../workflow-maker/workflow-maker.block.less | 15 +- .../workflow-maker.controller.js | 319 ++++--- .../workflow-maker.partial.html | 34 +- .../workflow-results.partial.html | 30 +- 14 files changed, 741 insertions(+), 933 deletions(-) create mode 100644 awx/ui/client/src/templates/workflows/workflow-key/main.js create mode 100644 awx/ui/client/src/templates/workflows/workflow-key/workflow-key.directive.js create mode 100644 awx/ui/client/src/templates/workflows/workflow-key/workflow-key.partial.html create mode 100644 awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.service.js diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js index ffc36b32f0..8b0713ae4c 100644 --- a/awx/ui/client/features/templates/templates.strings.js +++ b/awx/ui/client/features/templates/templates.strings.js @@ -110,9 +110,12 @@ function TemplatesStrings (BaseString) { ON_SUCCESS: t.s('On Success'), ON_FAILURE: t.s('On Failure'), ALWAYS: t.s('Always'), + PAUSE: t.s('Wait For Approval'), + JOB_TEMPLATE: t.s('Job Template'), PROJECT_SYNC: t.s('Project Sync'), INVENTORY_SYNC: t.s('Inventory Sync'), WORKFLOW: t.s('Workflow'), + TEMPLATE: t.s('Template'), WARNING: t.s('Warning'), TOTAL_NODES: t.s('TOTAL NODES'), ADD_A_NODE: t.s('ADD A NODE'), @@ -145,7 +148,7 @@ function TemplatesStrings (BaseString) { EXIT: t.s('EXIT'), CANCEL: t.s('CANCEL'), SAVE_AND_EXIT: t.s('SAVE & EXIT'), - PAUSE_NODE: t.s('Pause Node') + APPROVAL: t.s('Approval') }; } diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index 70d8b24b00..4f301cdb91 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -12,6 +12,7 @@ import workflowEdit from './workflows/edit-workflow/main'; import labels from './labels/main'; import prompt from './prompt/main'; import workflowChart from './workflows/workflow-chart/main'; +import workflowKey from './workflows/workflow-key/main'; import workflowMaker from './workflows/workflow-maker/main'; import workflowControls from './workflows/workflow-controls/main'; import WorkflowForm from './workflows.form'; @@ -31,7 +32,7 @@ import { export default angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, prompt.name, workflowAdd.name, workflowEdit.name, - workflowChart.name, workflowMaker.name, workflowControls.name + workflowChart.name, workflowKey.name, workflowMaker.name, workflowControls.name ]) .service('TemplatesService', templatesService) .factory('WorkflowForm', WorkflowForm) @@ -499,320 +500,7 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p views: { 'modal': { template: `` - }, - 'jobTemplateList@templates.editWorkflowJobTemplate.workflowMaker': { - templateProvider: function(WorkflowMakerJobTemplateList, generateList) { - - let html = generateList.build({ - list: WorkflowMakerJobTemplateList, - input_type: 'radio', - mode: 'lookup' - }); - return html; - }, - // $scope encapsulated in this controller will be a initialized as child of 'modal' $scope, because of element hierarchy - controller: ['$scope', 'WorkflowMakerJobTemplateList', 'JobTemplateDataset', - function($scope, list, Dataset) { - - init(); - - function init() { - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - $scope.$watch('wf_maker_templates', function(){ - if($scope.selectedTemplate){ - $scope.wf_maker_templates.forEach(function(row, i) { - if(row.id === $scope.selectedTemplate.id) { - $scope.wf_maker_templates[i].checked = 1; - } - else { - $scope.wf_maker_templates[i].checked = 0; - } - }); - } - }); - } - - $scope.toggle_row = function(selectedRow) { - if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) { - $scope.wf_maker_templates.forEach(function(row, i) { - if (row.id === selectedRow.id) { - $scope.wf_maker_templates[i].checked = 1; - $scope.selection[list.iterator] = { - id: row.id, - name: row.name - }; - - $scope.templateManuallySelected(row); - } - }); - } - }; - - $scope.$watch('selectedTemplate', () => { - $scope.wf_maker_templates.forEach(function(row, i) { - if(_.has($scope, 'selectedTemplate.id') && row.id === $scope.selectedTemplate.id) { - $scope.wf_maker_templates[i].checked = 1; - } - else { - $scope.wf_maker_templates[i].checked = 0; - } - }); - }); - - $scope.$watch('activeTab', () => { - if(!$scope.activeTab || $scope.activeTab !== "jobs") { - $scope.wf_maker_templates.forEach(function(row, i) { - $scope.wf_maker_templates[i].checked = 0; - }); - } - }); - - $scope.$on('clearWorkflowLists', function() { - $scope.wf_maker_templates.forEach(function(row, i) { - $scope.wf_maker_templates[i].checked = 0; - }); - }); - } - ] - }, - 'inventorySyncList@templates.editWorkflowJobTemplate.workflowMaker': { - templateProvider: function(WorkflowInventorySourcesList, generateList) { - let html = generateList.build({ - list: WorkflowInventorySourcesList, - input_type: 'radio', - mode: 'lookup' - }); - return html; - }, - // encapsulated $scope in this controller will be a initialized as child of 'modal' $scope, because of element hierarchy - controller: ['$scope', 'WorkflowInventorySourcesList', 'InventorySourcesDataset', - function($scope, list, Dataset) { - - init(); - - function init() { - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - $scope.$watch('wf_maker_inventory_sources', function(){ - if($scope.selectedTemplate){ - $scope.wf_maker_inventory_sources.forEach(function(row, i) { - if(row.id === $scope.selectedTemplate.id) { - $scope.wf_maker_inventory_sources[i].checked = 1; - } - else { - $scope.wf_maker_inventory_sources[i].checked = 0; - } - }); - } - }); - } - - $scope.toggle_row = function(selectedRow) { - if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) { - $scope.wf_maker_inventory_sources.forEach(function(row, i) { - if (row.id === selectedRow.id) { - $scope.wf_maker_inventory_sources[i].checked = 1; - $scope.selection[list.iterator] = { - id: row.id, - name: row.name - }; - - $scope.templateManuallySelected(row); - } - }); - } - }; - - $scope.$watch('selectedTemplate', () => { - $scope.wf_maker_inventory_sources.forEach(function(row, i) { - if(_.hasIn($scope, 'selectedTemplate.id') && row.id === $scope.selectedTemplate.id) { - $scope.wf_maker_inventory_sources[i].checked = 1; - } - else { - $scope.wf_maker_inventory_sources[i].checked = 0; - } - }); - }); - - $scope.$watch('activeTab', () => { - if(!$scope.activeTab || $scope.activeTab !== "inventory_sync") { - $scope.wf_maker_inventory_sources.forEach(function(row, i) { - $scope.wf_maker_inventory_sources[i].checked = 0; - }); - } - }); - - $scope.$on('clearWorkflowLists', function() { - $scope.wf_maker_inventory_sources.forEach(function(row, i) { - $scope.wf_maker_inventory_sources[i].checked = 0; - }); - }); - } - ] - }, - 'projectSyncList@templates.editWorkflowJobTemplate.workflowMaker': { - templateProvider: function(WorkflowProjectList, generateList) { - let html = generateList.build({ - list: WorkflowProjectList, - input_type: 'radio', - mode: 'lookup' - }); - return html; - }, - // encapsulated $scope in this controller will be a initialized as child of 'modal' $scope, because of element hierarchy - controller: ['$scope', 'WorkflowProjectList', 'ProjectDataset', - function($scope, list, Dataset) { - - init(); - - function init() { - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - $scope.$watch('wf_maker_projects', function(){ - if($scope.selectedTemplate){ - $scope.wf_maker_projects.forEach(function(row, i) { - if(row.id === $scope.selectedTemplate.id) { - $scope.wf_maker_projects[i].checked = 1; - } - else { - $scope.wf_maker_projects[i].checked = 0; - } - }); - } - }); - } - - $scope.toggle_row = function(selectedRow) { - if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) { - $scope.wf_maker_projects.forEach(function(row, i) { - if (row.id === selectedRow.id) { - $scope.wf_maker_projects[i].checked = 1; - $scope.selection[list.iterator] = { - id: row.id, - name: row.name - }; - - $scope.templateManuallySelected(row); - } - }); - } - }; - - $scope.$watch('selectedTemplate', () => { - $scope.wf_maker_projects.forEach(function(row, i) { - if(_.hasIn($scope, 'selectedTemplate.id') && row.id === $scope.selectedTemplate.id) { - $scope.wf_maker_projects[i].checked = 1; - } - else { - $scope.wf_maker_projects[i].checked = 0; - } - }); - }); - - $scope.$watch('activeTab', () => { - if(!$scope.activeTab || $scope.activeTab !== "project_sync") { - $scope.wf_maker_projects.forEach(function(row, i) { - $scope.wf_maker_projects[i].checked = 0; - }); - } - }); - - $scope.$on('clearWorkflowLists', function() { - $scope.wf_maker_projects.forEach(function(row, i) { - $scope.wf_maker_projects[i].checked = 0; - }); - }); - } - ] } - }, - resolve: { - JobTemplateDataset: ['WorkflowMakerJobTemplateList', 'QuerySet', '$stateParams', 'GetBasePath', - (list, qs, $stateParams, GetBasePath) => { - let path = GetBasePath(list.basePath); - return qs.search(path, $stateParams[`${list.iterator}_search`]); - } - ], - ProjectDataset: ['WorkflowProjectList', 'QuerySet', '$stateParams', 'GetBasePath', - (list, qs, $stateParams, GetBasePath) => { - let path = GetBasePath(list.basePath); - return qs.search(path, $stateParams[`${list.iterator}_search`]); - } - ], - InventorySourcesDataset: ['InventorySourcesList', 'QuerySet', '$stateParams', 'GetBasePath', - (list, qs, $stateParams, GetBasePath) => { - let path = GetBasePath(list.basePath); - return qs.search(path, $stateParams[`${list.iterator}_search`]); - } - ], - WorkflowMakerJobTemplateList: ['TemplateList', 'i18n', - (TemplateList, i18n) => { - let list = _.cloneDeep(TemplateList); - delete list.actions; - delete list.fields.type; - delete list.fields.description; - delete list.fields.smart_status; - delete list.fields.labels; - delete list.fieldActions; - list.name = 'wf_maker_templates'; - list.iterator = 'wf_maker_template'; - list.fields.name.columnClass = "col-md-8"; - list.fields.name.tag = i18n._('WORKFLOW'); - list.fields.name.showTag = "{{wf_maker_template.type === 'workflow_job_template'}}"; - list.disableRow = "{{ !workflowJobTemplateObj.summary_fields.user_capabilities.edit }}"; - list.disableRowValue = '!workflowJobTemplateObj.summary_fields.user_capabilities.edit'; - list.basePath = 'unified_job_templates'; - list.fields.info = { - ngInclude: "'/static/partials/job-template-details.html'", - type: 'template', - columnClass: 'col-md-3', - infoHeaderClass: 'col-md-3', - label: '', - nosort: true - }; - list.maxVisiblePages = 5; - list.searchBarFullWidth = true; - - return list; - } - ], - WorkflowProjectList: ['ProjectList', - (ProjectList) => { - let list = _.cloneDeep(ProjectList); - delete list.fields.status; - delete list.fields.scm_type; - delete list.fields.last_updated; - list.name = 'wf_maker_projects'; - list.iterator = 'wf_maker_project'; - list.fields.name.columnClass = "col-md-11"; - list.maxVisiblePages = 5; - list.searchBarFullWidth = true; - list.disableRow = "{{ !workflowJobTemplateObj.summary_fields.user_capabilities.edit }}"; - list.disableRowValue = '!workflowJobTemplateObj.summary_fields.user_capabilities.edit'; - - return list; - } - ], - WorkflowInventorySourcesList: ['InventorySourcesList', - (InventorySourcesList) => { - let list = _.cloneDeep(InventorySourcesList); - list.name = 'wf_maker_inventory_sources'; - list.iterator = 'wf_maker_inventory_source'; - list.maxVisiblePages = 5; - list.searchBarFullWidth = true; - list.disableRow = "{{ !workflowJobTemplateObj.summary_fields.user_capabilities.edit }}"; - list.disableRowValue = '!workflowJobTemplateObj.summary_fields.user_capabilities.edit'; - - return list; - } - ] } }; diff --git a/awx/ui/client/src/templates/templates.service.js b/awx/ui/client/src/templates/templates.service.js index ef718064fd..f97899ddd8 100644 --- a/awx/ui/client/src/templates/templates.service.js +++ b/awx/ui/client/src/templates/templates.service.js @@ -291,13 +291,13 @@ export default ['Rest', 'GetBasePath', '$q', 'NextPage', function(Rest, GetBaseP Rest.setUrl(url); return Rest.post(params.data); }, - createApprovalTemplate: (params) => { - params = params || {}; - Rest.setUrl(GetBasePath('workflow_approval_templates')); - return Rest.post(params); + createApprovalTemplate: ({url, data}) => { + data = data || {}; + Rest.setUrl(url); + return Rest.post(data); }, patchApprovalTemplate: ({id, data}) => { - Rest.setUrl(`${GetBasePath('workflow_approval_templates')}/${id}`); + Rest.setUrl(`/api/v2/workflow_approval_templates/${id}`); return Rest.patch(data); } }; diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less index c1719b07ac..a9c1550d60 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less @@ -117,6 +117,7 @@ .WorkflowChart-nodeTypeLetter { fill: @default-bg; + font-size: 10px; } .WorkflowChart-nodeStatus--running { diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index 8156407b5b..e4c64231d5 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -109,11 +109,11 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', // TODO: this function is hacky and we need to come up with a better solution // see: http://stackoverflow.com/questions/15975440/add-ellipses-to-overflowing-text-in-svg#answer-27723752 const wrap = (text) => { - if(text && text.length > maxNodeTextLength) { - return text.substring(0,maxNodeTextLength) + '...'; + if(text) { + return text.length > maxNodeTextLength ? text.substring(0,maxNodeTextLength) + '...' : text; } else { - return text; + return ''; } }; @@ -143,7 +143,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', translation = [translation[0], translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)]; - svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")"); + svgGroup.attr("transform", `translate(${translation})scale(${scale})`); scope.workflowZoomed({ zoom: scale @@ -160,7 +160,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', translateX = unscaledOffsetX*scale - ((scale*windowWidth)-windowWidth)/2, translateY = unscaledOffsetY*scale - ((scale*windowHeight)-windowHeight)/2; - svgGroup.attr("transform", "translate(" + [translateX, translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")"); + svgGroup.attr("transform", `translate(${[translateX, translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)]})scale(${scale})`); zoomObj.scale(scale); zoomObj.translate([translateX, translateY]); }; @@ -178,27 +178,32 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', translateX = translateCoords[0]; translateY = direction === 'up' ? translateCoords[1] - distance : translateCoords[1] + distance; } - svgGroup.attr("transform", "translate(" + translateX + "," + (translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)) + ")scale(" + scale + ")"); + svgGroup.attr("transform", `translate(${translateX},${(translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale))})scale(${scale})`); zoomObj.translate([translateX, translateY]); }; const resetZoomAndPan = () => { - svgGroup.attr("transform", "translate(0," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")"); + svgGroup.attr("transform", `translate(0,${(windowHeight/2 - rootH/2 - startNodeOffsetY)})scale(1)`); // Update the zoomObj zoomObj.scale(1); zoomObj.translate([0,0]); }; const zoomToFitChart = () => { - let graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(), + const startNodeWidth = scope.mode === 'details' ? 25 : 60, + graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(), availableScreenSpace = calcAvailableScreenSpace(), - currentZoomValue = zoomObj.scale(), - unscaledH = graphDimensions.height/currentZoomValue, + currentZoomValue = zoomObj.scale(); + + // For some reason the start node isn't accounted for in the width... add it + graphDimensions.width = graphDimensions.width + (startNodeWidth*currentZoomValue); + + const unscaledH = graphDimensions.height/currentZoomValue, unscaledW = graphDimensions.width/currentZoomValue, scaleNeededForMaxHeight = (availableScreenSpace.height)/unscaledH, scaleNeededForMaxWidth = (availableScreenSpace.width)/unscaledW, lowerScale = Math.min(scaleNeededForMaxHeight, scaleNeededForMaxWidth), - scaleToFit = lowerScale < 0.5 ? 0.5 : (lowerScale > 2 ? 2 : Math.floor(lowerScale * 10)/10); + scaleToFit = lowerScale < 0.5 ? 0.5 : (lowerScale > 2 ? 2 : Math.floor(lowerScale * 1000)/1000); manualZoom(scaleToFit*100); @@ -206,100 +211,99 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', zoom: scaleToFit }); - svgGroup.attr("transform", "translate(0," + (windowHeight/2 - (nodeH*scaleToFit/2)) + ")scale(" + scaleToFit + ")"); + svgGroup.attr("transform", `translate(0, ${(windowHeight/2 - (nodeH*scaleToFit/2))})scale(${scaleToFit})`); zoomObj.translate([0, windowHeight/2 - (nodeH*scaleToFit/2) - ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scaleToFit)]); }; + const buildLinkTooltip = (d) => { + let edgeTypeLabel; + switch(d.edgeType) { + case "always": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS'); + break; + case "success": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS'); + break; + case "failure": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE'); + break; + } + let linkInstructionText = !scope.readOnly ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP'); + let linkTooltip = svgGroup.append("g") + .attr("class", "WorkflowChart-tooltip"); + const tipRef = linkTooltip.append("foreignObject") + // In order for this to work in FF a height of at least 1 must be present + .attr("width", 100) + .attr("height", 1) + .style("overflow", "visible") + .html(` +
+
${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}
+
${linkInstructionText}
+
+ `); + const tipDimensions = tipRef.select('.WorkflowChart-tooltipContents').node().getBoundingClientRect(); + let sourceNode = d3.select(`#node-${d.source.id}`); + const sourceNodeX = d3.transform(sourceNode.attr("transform")).translate[0]; + const sourceNodeY = d3.transform(sourceNode.attr("transform")).translate[1]; + let targetNode = d3.select(`#node-${d.target.id}`); + const targetNodeX = d3.transform(targetNode.attr("transform")).translate[0]; + const targetNodeY = d3.transform(targetNode.attr("transform")).translate[1]; + let xPos, yPos, arrowPoints; + const scaledHeight = tipDimensions.height/zoomObj.scale(); + + if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) { + xPos = (sourceNodeX + nodeW + targetNodeX)/2 - 50; + yPos = sourceNodeY + nodeH/2 - scaledHeight - 20; + arrowPoints = { + pt1: { + x: xPos + 40, + y: yPos + scaledHeight + }, + pt2: { + x: xPos + 60, + y: yPos + scaledHeight + }, + pt3: { + x: xPos + 50, + y: yPos + scaledHeight + 10 + } + }; + } else { + xPos = (sourceNodeX + nodeW + targetNodeX)/2 - 120; + yPos = (sourceNodeY + (nodeH/2) + targetNodeY + (nodeH/2))/2 - (scaledHeight/2); + arrowPoints = { + pt1: { + x: xPos + 100, + y: yPos + (scaledHeight/2) - 10 + }, + pt2: { + x: xPos + 100, + y: yPos + (scaledHeight/2) + 10 + }, + pt3: { + x: xPos + 110, + y: yPos + (scaledHeight/2) + } + }; + } + + linkTooltip.append("polygon") + .attr("class", "WorkflowChart-tooltipArrow") + .attr("points", `${arrowPoints.pt1.x},${arrowPoints.pt1.y} ${arrowPoints.pt2.x},${arrowPoints.pt2.y} ${arrowPoints.pt3.x},${arrowPoints.pt3.y}`); + + tipRef.attr('height', scaledHeight); + tipRef.attr("transform", `translate(${xPos},${yPos})`); + }; + const updateGraph = () => { if(scope.dimensionsSet) { - const buildLinkTooltip = (d) => { - let edgeTypeLabel; - switch(d.edgeType) { - case "always": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS'); - break; - case "success": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS'); - break; - case "failure": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE'); - break; - } - let linkInstructionText = !scope.readOnly ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP'); - let linkTooltip = svgGroup.append("g") - .attr("class", "WorkflowChart-tooltip"); - const tipRef = linkTooltip.append("foreignObject") - // In order for this to work in FF a height of at least 1 must be present - .attr("width", 100) - .attr("height", 1) - .style("overflow", "visible") - .html(function(){ - return `
-
${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}
-
${linkInstructionText}
-
`; - }); - const tipDimensions = tipRef.select('.WorkflowChart-tooltipContents').node().getBoundingClientRect(); - let sourceNode = d3.select(`#node-${d.source.id}`); - const sourceNodeX = d3.transform(sourceNode.attr("transform")).translate[0]; - const sourceNodeY = d3.transform(sourceNode.attr("transform")).translate[1]; - let targetNode = d3.select(`#node-${d.target.id}`); - const targetNodeX = d3.transform(targetNode.attr("transform")).translate[0]; - const targetNodeY = d3.transform(targetNode.attr("transform")).translate[1]; - let xPos, yPos, arrowPoints; - const scaledHeight = tipDimensions.height/zoomObj.scale(); - - if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) { - xPos = (sourceNodeX + nodeW + targetNodeX)/2 - 50; - yPos = sourceNodeY + nodeH/2 - scaledHeight - 20; - arrowPoints = { - pt1: { - x: xPos + 40, - y: yPos + scaledHeight - }, - pt2: { - x: xPos + 60, - y: yPos + scaledHeight - }, - pt3: { - x: xPos + 50, - y: yPos + scaledHeight + 10 - } - }; - } else { - xPos = (sourceNodeX + nodeW + targetNodeX)/2 - 120; - yPos = (sourceNodeY + (nodeH/2) + targetNodeY + (nodeH/2))/2 - (scaledHeight/2); - arrowPoints = { - pt1: { - x: xPos + 100, - y: yPos + (scaledHeight/2) - 10 - }, - pt2: { - x: xPos + 100, - y: yPos + (scaledHeight/2) + 10 - }, - pt3: { - x: xPos + 110, - y: yPos + (scaledHeight/2) - } - }; - } - - linkTooltip.append("polygon") - .attr("class", "WorkflowChart-tooltipArrow") - .attr("points", function() { - return `${arrowPoints.pt1.x},${arrowPoints.pt1.y} ${arrowPoints.pt2.x},${arrowPoints.pt2.y} ${arrowPoints.pt3.x},${arrowPoints.pt3.y}`; - }); - - tipRef.attr('height', scaledHeight); - tipRef.attr("transform", `translate(${xPos},${yPos})`); - }; - let g = new dagre.graphlib.Graph(); g.setGraph({rankdir: 'LR', nodesep: 30, ranksep: 120}); - g.setDefaultEdgeLabel(function() { return {}; }); + // This is needed for Dagre + g.setDefaultEdgeLabel(() => { return {}; }); scope.graphState.arrayOfNodesForChart.forEach((node) => { if (node.id === 1) { @@ -326,19 +330,19 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }); let links = svgGroup.selectAll(".WorkflowChart-link") - .data(scope.graphState.arrayOfLinksForChart, function(d) { return `${d.source.id}-${d.target.id}`; }); + .data(scope.graphState.arrayOfLinksForChart, (d) => { return `${d.source.id}-${d.target.id}`; }); // Remove any stale links links.exit().remove(); // Update existing links baseSvg.selectAll(".WorkflowChart-link") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;}); + .attr("id", (d) => `link-${d.source.id}-${d.target.id}`); baseSvg.selectAll(".WorkflowChart-linkPath") .transition() .attr("d", lineData) - .attr('stroke', function(d) { + .attr('stroke', (d) => { let edgeType = d.edgeType; if(edgeType) { if(edgeType === "failure") { @@ -357,8 +361,8 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }); baseSvg.selectAll(".WorkflowChart-linkOverlay") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";}) - .attr("class", function(d) { + .attr("id", (d) => `link-${d.source.id}-${d.target.id}-overlay`) + .attr("class", (d) => { let linkClasses = ["WorkflowChart-linkOverlay"]; if ( scope.graphState.linkBeingEdited && @@ -369,7 +373,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } return linkClasses.join(' '); }) - .attr("points",function(d) { + .attr("points",(d) => { let x1 = nodePositionMap[d.target.id].x; let y1 = normalizeY(nodePositionMap[d.target.id].y) + (nodePositionMap[d.target.id].height/2); let x2 = nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].width; @@ -387,12 +391,12 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }); baseSvg.selectAll(".WorkflowChart-circleBetweenNodes") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) - .style("display", function(d) { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) - .attr("cx", function(d) { + .attr("id", (d) => `link-${d.source.id}-${d.target.id}-add`) + .style("display", (d) => { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .attr("cx", (d) => { return (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2; }) - .attr("cy", function(d) { + .attr("cy", (d) => { const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y); const halfSourceHeight = nodePositionMap[d.source.id].height/2; const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y); @@ -408,8 +412,8 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }); baseSvg.selectAll(".WorkflowChart-betweenNodesIcon") - .style("display", function(d) { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) - .attr("transform", function(d) { + .style("display", (d) => { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .attr("transform", (d) => { let translate; const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y); @@ -423,17 +427,17 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', yPos = yPos + 4; } - translate = "translate(" + (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2 + "," + yPos + ")"; + translate = `translate(${(nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2}, ${yPos})`; return translate; }); // Add any new links let linkEnter = links.enter().append("g") .attr("class", "WorkflowChart-link") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;}); + .attr("id", (d) => `link-${d.source.id}-${d.target.id}`); linkEnter.append("polygon", "g") - .attr("class", function(d) { + .attr("class", (d) => { let linkClasses = ["WorkflowChart-linkOverlay"]; if ( scope.graphState.linkBeingEdited && @@ -444,9 +448,9 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } return linkClasses.join(' '); }) - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";}) + .attr("id", (d) => `link-${d.source.id}-${d.target.id}-overlay`) .call(edit_link) - .attr("points",function(d) { + .attr("points",(d) => { let x1 = nodePositionMap[d.target.id].x; let y1 = normalizeY(nodePositionMap[d.target.id].y) + (nodePositionMap[d.target.id].height/2); let x2 = nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].width; @@ -462,7 +466,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', return [pt1, pt2, pt3, pt4].join(" "); }) - .on("mouseover", function(d) { + .on("mouseover", (d) => { if( d.edgeType !== 'placeholder' && !scope.graphState.isLinkMode && @@ -478,10 +482,10 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', buildLinkTooltip(d); } }) - .on("mouseout", function(d){ + .on("mouseout", (d) => { if(d.source.id !== 1 && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') { $(`#aw-workflow-chart-g`).prepend($(`#link-${d.source.id}-${d.target.id}`)); - d3.select("#link-" + d.source.id + "-" + d.target.id) + d3.select(`#link-${d.source.id}-${d.target.id}`) .classed("WorkflowChart-linkHovering", false); } $('.WorkflowChart-tooltip').remove(); @@ -492,7 +496,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .attr("class", "WorkflowChart-linkPath") .attr("d", lineData) .call(edit_link) - .on("mouseenter", function(d) { + .on("mouseenter", (d) => { if( d.edgeType !== 'placeholder' && !scope.graphState.isLinkMode && @@ -508,15 +512,15 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', buildLinkTooltip(d); } }) - .on("mouseleave", function(d){ + .on("mouseleave", (d) => { if(d.source.id !== 1 && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') { $(`#aw-workflow-chart-g`).prepend($(`#link-${d.source.id}-${d.target.id}`)); - d3.select("#link-" + d.source.id + "-" + d.target.id) + d3.select(`#link-${d.source.id}-${d.target.id}`) .classed("WorkflowChart-linkHovering", false); } $('.WorkflowChart-tooltip').remove(); }) - .attr('stroke', function(d) { + .attr('stroke', (d) => { let edgeType = d.edgeType; if(d.edgeType) { if(edgeType === "failure") { @@ -535,14 +539,14 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }); linkEnter.append("circle") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) + .attr("id", (d) => `link-${d.source.id}-${d.target.id}-add`) .attr("r", 10) .attr("class", "WorkflowChart-addCircle WorkflowChart-circleBetweenNodes") - .style("display", function(d) { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) - .attr("cx", function(d) { + .style("display", (d) => { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .attr("cx", (d) => { return (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2; }) - .attr("cy", function(d) { + .attr("cy", (d) => { const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y); const halfSourceHeight = nodePositionMap[d.source.id].height/2; const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y); @@ -557,14 +561,14 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', return yPos; }) .call(add_node_with_child) - .on("mouseover", function(d) { + .on("mouseover", (d) => { $(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`); - d3.select("#link-" + d.source.id + "-" + d.target.id) + d3.select(`#link-${d.source.id}-${d.target.id}`) .classed("WorkflowChart-addHovering", true); }) - .on("mouseout", function(d){ + .on("mouseout", (d) => { $(`#aw-workflow-chart-g`).prepend($(`#link-${d.source.id}-${d.target.id}`)); - d3.select("#link-" + d.source.id + "-" + d.target.id) + d3.select(`#link-${d.source.id}-${d.target.id}`) .classed("WorkflowChart-addHovering", false); }); @@ -575,10 +579,8 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .size(60) .type("cross") ) - .style("display", function(d) { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) - .attr("transform", function(d) { - let translate; - + .style("display", (d) => { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .attr("transform", (d) => { const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y); const halfSourceHeight = nodePositionMap[d.source.id].height/2; const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y); @@ -590,23 +592,22 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', yPos = yPos + 4; } - translate = "translate(" + (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2 + "," + yPos + ")"; - return translate; + return `translate(${(nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2}, ${yPos})`; }) .call(add_node_with_child) - .on("mouseover", function(d) { + .on("mouseover", (d) => { $(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`); - d3.select("#link-" + d.source.id + "-" + d.target.id) + d3.select(`#link-${d.source.id}-${d.target.id}`) .classed("WorkflowChart-addHovering", true); }) - .on("mouseout", function(d){ + .on("mouseout", (d) => { $(`#aw-workflow-chart-g`).prepend($(`#link-${d.source.id}-${d.target.id}`)); - d3.select("#link-" + d.source.id + "-" + d.target.id) + d3.select(`#link-${d.source.id}-${d.target.id}`) .classed("WorkflowChart-addHovering", false); }); let nodes = svgGroup.selectAll('.WorkflowChart-node') - .data(scope.graphState.arrayOfNodesForChart, function(d) { return d.id; }); + .data(scope.graphState.arrayOfNodesForChart, (d) => { return d.id; }); // Remove any stale nodes nodes.exit().remove(); @@ -614,33 +615,33 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', // Update existing nodes baseSvg.selectAll(".WorkflowChart-node") .transition() - .attr("transform", function (d) { + .attr("transform", (d) => { // Update prior x and prior y d.px = d.x; d.py = d.y; - return "translate(" + nodePositionMap[d.id].x + "," + normalizeY(nodePositionMap[d.id].y) + ")"; + return `translate(${nodePositionMap[d.id].x}, ${normalizeY(nodePositionMap[d.id].y)})`; }); baseSvg.selectAll(".WorkflowChart-nodeAddCircle") - .style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); + .style("display", (d) => { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); baseSvg.selectAll(".WorkflowChart-nodeAddIcon") - .style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); + .style("display", (d) => { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); baseSvg.selectAll(".WorkflowChart-linkCircle") - .style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); + .style("display", (d) => { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); baseSvg.selectAll(".WorkflowChart-nodeLinkIcon") - .style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); + .style("display", (d) => { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); baseSvg.selectAll(".WorkflowChart-nodeRemoveCircle") - .style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); + .style("display", (d) => { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); baseSvg.selectAll(".WorkflowChart-nodeRemoveIcon") - .style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); + .style("display", (d) => { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); baseSvg.selectAll(".WorkflowChart-rect") - .attr('stroke', function(d) { + .attr('stroke', (d) => { if(d.job && d.job.status) { if(d.job.status === "successful"){ return "#5cb85c"; @@ -656,30 +657,26 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', return "#D7D7D7"; } }) - .attr("class", function(d) { + .attr("class", (d) => { let classString = d.id === scope.graphState.nodeBeingAdded ? "WorkflowChart-rect WorkflowChart-isNodeBeingAdded" : "WorkflowChart-rect"; classString += !d.unifiedJobTemplate ? " WorkflowChart-dashedNode" : ""; return classString; }); baseSvg.selectAll(".WorkflowChart-nodeOverlay") - .attr("class", function(d) { return d.isInvalidLinkTarget ? "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--disabled" : "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--transparent"; }); + .attr("class", (d) => { return d.isInvalidLinkTarget ? "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--disabled" : "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--transparent"; }); baseSvg.selectAll(".WorkflowChart-nodeTypeCircle") - .style("display", function (d) { - return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || - d.unifiedJobTemplate.unified_job_type === "project_update" || - d.unifiedJobTemplate.type === "inventory_source" || - d.unifiedJobTemplate.unified_job_type === "inventory_update" || - d.unifiedJobTemplate.type === "workflow_job_template" || - d.unifiedJobTemplate.unified_job_type === "workflow_job") ? null : "none"; - }); + .style("display", (d) => d.unifiedJobTemplate ? null : "none"); baseSvg.selectAll(".WorkflowChart-nodeTypeLetter") - .text(function (d) { + .text((d) => { let nodeTypeLetter = ""; if (d.unifiedJobTemplate && d.unifiedJobTemplate.type) { switch (d.unifiedJobTemplate.type) { + case "job_template": + nodeTypeLetter = "JT"; + break; case "project": nodeTypeLetter = "P"; break; @@ -692,6 +689,9 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } } else if (d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type) { switch (d.unifiedJobTemplate.unified_job_type) { + case "job": + nodeTypeLetter = "JT"; + break; case "project_update": nodeTypeLetter = "P"; break; @@ -705,50 +705,88 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } return nodeTypeLetter; }) - .style("display", function (d) { + .style("display", (d) => { return d.unifiedJobTemplate && - (d.unifiedJobTemplate.type === "project" || - d.unifiedJobTemplate.unified_job_type === "project_update" || - d.unifiedJobTemplate.type === "inventory_source" || - d.unifiedJobTemplate.unified_job_type === "inventory_update" || - d.unifiedJobTemplate.type === "workflow_job_template" || - d.unifiedJobTemplate.unified_job_type === "workflow_job") ? null : "none"; + d.unifiedJobTemplate.type !== "workflow_approval_template" && + d.unifiedJobTemplate.unified_job_type !== "workflow_approval" ? null : "none"; }); - baseSvg.selectAll(".WorkflowChart-nodeStatus") - .attr("class", function(d) { + baseSvg.selectAll(".WorkflowChart-nodeTypeLetter") + .text((d) => { + let nodeTypeLetter = ""; + if (d.unifiedJobTemplate && d.unifiedJobTemplate.type) { + switch (d.unifiedJobTemplate.type) { + case "job_template": + nodeTypeLetter = "JT"; + break; + case "project": + nodeTypeLetter = "P"; + break; + case "inventory_source": + nodeTypeLetter = "I"; + break; + case "workflow_job_template": + nodeTypeLetter = "W"; + break; + } + } else if (d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type) { + switch (d.unifiedJobTemplate.unified_job_type) { + case "job": + nodeTypeLetter = "JT"; + break; + case "project_update": + nodeTypeLetter = "P"; + break; + case "inventory_update": + nodeTypeLetter = "I"; + break; + case "workflow_job": + nodeTypeLetter = "W"; + break; + } + } + return nodeTypeLetter; + }) + .style("display", (d) => { + return d.unifiedJobTemplate && + d.unifiedJobTemplate.type !== "workflow_approval_template" && + d.unifiedJobTemplate.unified_job_type !== "workflow_approval" ? null : "none"; + }); - let statusClass = "WorkflowChart-nodeStatus "; + baseSvg.selectAll(".WorkflowChart-pauseIcon") + .style("display", (d) => { + return d.unifiedJobTemplate && + (d.unifiedJobTemplate.type === "workflow_approval_template" || + d.unifiedJobTemplate.unified_job_type === "workflow_approval") ? null : "none"; + }); + + baseSvg.selectAll(".WorkflowChart-nodeStatus") + .attr("class", (d) => { + let statusClasses = ["WorkflowChart-nodeStatus"]; if(d.job){ switch(d.job.status) { case "pending": - statusClass += "WorkflowChart-nodeStatus--running"; - break; case "waiting": - statusClass += "WorkflowChart-nodeStatus--running"; - break; case "running": - statusClass += "WorkflowChart-nodeStatus--running"; + statusClasses.push("WorkflowChart-nodeStatus--running"); break; case "successful": - statusClass += "WorkflowChart-nodeStatus--success"; + statusClasses.push("WorkflowChart-nodeStatus--success"); break; case "failed": - statusClass += "WorkflowChart-nodeStatus--failed"; - break; case "error": - statusClass += "WorkflowChart-nodeStatus--failed"; + statusClasses.push("WorkflowChart-nodeStatus--failed"); break; case "canceled": - statusClass += "WorkflowChart-nodeStatus--canceled"; + statusClasses.push("WorkflowChart-nodeStatus--canceled"); break; } } - return statusClass; + return statusClasses.join(' '); }) - .style("display", function(d) { return d.job && d.job.status ? null : "none"; }) + .style("display", (d) => { return d.job && d.job.status ? null : "none"; }) .transition() .duration(0) .attr("r", 6) @@ -770,28 +808,20 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }); baseSvg.selectAll(".WorkflowChart-nameText") - .attr("x", 0) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : (nodeH / 2) - 10; }) + .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; }) .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) - .html(function (d) { - const name = _.get(d, 'unifiedJobTemplate.name'); - const wrappedName = name ? wrap(name) : ""; - // TODO: clean this up - if (d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type === 'workflow_approval') { - return ` -
- -
- ${wrappedName} -
`; - } else { - return `${wrappedName}`; - } - }); + .text((d) => wrap(_.get(d, 'unifiedJobTemplate.name'))); baseSvg.selectAll(".WorkflowChart-detailsLink") - .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }) - .html(function (d) { + .style("display", (d) => { + const isApprovalStep = d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type === 'workflow_approval'; + return d.job && + !isApprovalStep && + d.job.status && + d.job.id ? null : "none"; + }) + .html((d) => { let href = ""; if (d.job) { href = `/#/workflow_node_results/${d.job.id}`; @@ -800,27 +830,25 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }); baseSvg.selectAll(".WorkflowChart-deletedText") - .style("display", function(d){ return d.unifiedJobTemplate || d.id === scope.graphState.nodeBeingAdded ? "none" : null; }); + .style("display", (d) => { return d.unifiedJobTemplate || d.id === scope.graphState.nodeBeingAdded ? "none" : null; }); baseSvg.selectAll(".WorkflowChart-activeNode") - .style("display", function(d) { return d.id === scope.graphState.nodeBeingEdited ? null : "none"; }); + .style("display", (d) => { return d.id === scope.graphState.nodeBeingEdited ? null : "none"; }); baseSvg.selectAll(".WorkflowChart-elapsed") - .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); + .style("display", (d) => { return (d.job && d.job.elapsed) ? null : "none"; }); baseSvg.selectAll(".WorkflowChart-addLinkCircle") - .attr("fill", function(d) { return scope.graphState.addLinkSource === d.id ? "#337AB7" : "#D7D7D7"; }) - .style("display", function(d) { return scope.graphState.isLinkMode && !d.isInvalidLinkTarget ? null : "none"; }); + .attr("fill", (d) => { return scope.graphState.addLinkSource === d.id ? "#337AB7" : "#D7D7D7"; }) + .style("display", (d) => { return scope.graphState.isLinkMode && !d.isInvalidLinkTarget ? null : "none"; }); // Add new nodes const nodeEnter = nodes .enter() .append('g') .attr("class", "WorkflowChart-node") - .attr("id", function(d){return "node-" + d.id;}) - .attr("transform", function (d) { - return "translate(" + nodePositionMap[d.id].x + "," + normalizeY(nodePositionMap[d.id].y) + ")"; - }); + .attr("id", (d) => `node-${d.id}`) + .attr("transform", (d) => `translate(${nodePositionMap[d.id].x},${normalizeY(nodePositionMap[d.id].y)})`); nodeEnter.each(function(d) { let thisNode = d3.select(this); @@ -852,7 +880,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .attr("y", 30) .attr("dy", ".35em") .attr("class", "WorkflowChart-startText") - .text(function () { return TemplatesStrings.get('workflow_maker.START'); }) + .text(TemplatesStrings.get('workflow_maker.START')) .call(add_node_without_child); } else { @@ -861,13 +889,13 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .attr("cx", nodeW) .attr("r", 8) .attr("class", "WorkflowChart-addLinkCircle") - .style("display", function() { return scope.graphState.isLinkMode ? null : "none"; }); + .style("display", scope.graphState.isLinkMode ? null : "none"); thisNode.append("rect") .attr("width", nodeW) .attr("height", nodeH) .attr("rx", 5) .attr("ry", 5) - .attr('stroke', function(d) { + .attr('stroke', (d) => { if(d.job && d.job.status) { if(d.job.status === "successful"){ return "#5cb85c"; @@ -884,7 +912,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } }) .attr('stroke-width', "2px") - .attr("class", function(d) { + .attr("class", (d) => { let classString = d.id === scope.graphState.nodeBeingAdded ? "WorkflowChart-rect WorkflowChart-isNodeBeingAdded" : "WorkflowChart-rect"; classString += !_.get(d, 'unifiedJobTemplate.name') ? " WorkflowChart-dashedNode" : ""; return classString; @@ -893,30 +921,15 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', thisNode.append("path") .attr("d", rounded_rect(1, 0, 5, nodeH, 5, 1, 0, 1, 0)) .attr("class", "WorkflowChart-activeNode") - .style("display", function(d) { return d.id === scope.graphState.nodeBeingEdited ? null : "none"; }); + .style("display", (d) => { return d.id === scope.graphState.nodeBeingEdited ? null : "none"; }); - thisNode.append("foreignObject") - // .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) - .attr("x", 0) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : (nodeH / 2) - 10; }) + thisNode.append("text") + .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; }) .attr("dy", ".35em") .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) .attr("class", "WorkflowChart-defaultText WorkflowChart-nameText") - .html(function (d) { - const name = _.get(d, 'unifiedJobTemplate.name'); - const wrappedName = name ? wrap(name) : ""; - // TODO: clean this up - if (d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type === 'workflow_approval') { - return ` -
- -
- ${wrappedName} -
`; - } else { - return `${wrappedName}`; - } - }); + .text((d) => wrap(_.get(d, 'unifiedJobTemplate.name'))); thisNode.append("foreignObject") .attr("x", 0) @@ -924,94 +937,107 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .attr("dy", ".35em") .attr("text-anchor", "middle") .attr("class", "WorkflowChart-defaultText WorkflowChart-deletedText") - .html(function () { - return `${TemplatesStrings.get('workflow_maker.DELETED')}`; - }) - .style("display", function(d) { return d.unifiedJobTemplate || d.id === scope.graphState.nodeBeingAdded ? "none" : null; }); + .html(`${TemplatesStrings.get('workflow_maker.DELETED')}`) + .style("display", (d) => { return d.unifiedJobTemplate || d.id === scope.graphState.nodeBeingAdded ? "none" : null; }); thisNode.append("circle") .attr("cy", nodeH) .attr("r", 10) .attr("class", "WorkflowChart-nodeTypeCircle") - .style("display", function (d) { - return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || - d.unifiedJobTemplate.unified_job_type === "project_update" || - d.unifiedJobTemplate.type === "inventory_source" || - d.unifiedJobTemplate.unified_job_type === "inventory_update" || - d.unifiedJobTemplate.type === "workflow_job_template" || - d.unifiedJobTemplate.unified_job_type === "workflow_job") ? null : "none"; - }); + .style("display", (d) => d.unifiedJobTemplate ? null : "none"); + thisNode.append("text") .attr("y", nodeH) .attr("dy", ".35em") .attr("text-anchor", "middle") .attr("class", "WorkflowChart-nodeTypeLetter") - .text(function (d) { - let nodeTypeLetter = ""; - if (d.unifiedJobTemplate && d.unifiedJobTemplate.type) { - switch (d.unifiedJobTemplate.type) { - case "project": - nodeTypeLetter = "P"; - break; - case "inventory_source": - nodeTypeLetter = "I"; - break; - case "workflow_job_template": - nodeTypeLetter = "W"; - break; - } - } else if (d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type) { - switch (d.unifiedJobTemplate.unified_job_type) { - case "project_update": - nodeTypeLetter = "P"; - break; - case "inventory_update": - nodeTypeLetter = "I"; - break; - case "workflow_job": - nodeTypeLetter = "W"; - break; - } - } - return nodeTypeLetter; + .text((d) => { + let nodeTypeLetter = ""; + if (d.unifiedJobTemplate && d.unifiedJobTemplate.type) { + switch (d.unifiedJobTemplate.type) { + case "job_template": + nodeTypeLetter = "JT"; + break; + case "project": + nodeTypeLetter = "P"; + break; + case "inventory_source": + nodeTypeLetter = "I"; + break; + case "workflow_job_template": + nodeTypeLetter = "W"; + break; + } + } else if (d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type) { + switch (d.unifiedJobTemplate.unified_job_type) { + case "job": + nodeTypeLetter = "JT"; + break; + case "project_update": + nodeTypeLetter = "P"; + break; + case "inventory_update": + nodeTypeLetter = "I"; + break; + case "workflow_job": + nodeTypeLetter = "W"; + break; + } + } + return nodeTypeLetter; }) - .style("display", function (d) { + .style("display", (d) => { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || - d.unifiedJobTemplate.unified_job_type === "project_update" || + d.unifiedJobTemplate.unified_job_type === "project_update" || + d.unifiedJobTemplate.type === "job_template" || + d.unifiedJobTemplate.unified_job_type === "job" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update" || d.unifiedJobTemplate.type === "workflow_job_template" || d.unifiedJobTemplate.unified_job_type === "workflow_job") ? null : "none"; - }); + }); + + thisNode.append("foreignObject") + .attr("x", -5) + .attr("y", nodeH - 9) + .attr("dy", ".35em") + .attr("height", "15px") + .attr("width", "11px") + .attr("class", "WorkflowChart-pauseIcon") + .html(``) + .style("display", (d) => { + return d.unifiedJobTemplate && + (d.unifiedJobTemplate.type === "workflow_approval_template" || + d.unifiedJobTemplate.unified_job_type === "workflow_approval") ? null : "none"; + }); thisNode.append("rect") .attr("width", nodeW) .attr("height", nodeH) - .attr("class", function(d) { return d.isInvalidLinkTarget ? "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--disabled" : "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--transparent"; }) + .attr("class", (d) => { return d.isInvalidLinkTarget ? "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--disabled" : "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--transparent"; }) .call(node_click) - .on("mouseover", function(d) { + .on("mouseover", (d) => { if(d.id !== 1) { $(`#node-${d.id}`).appendTo(`#aw-workflow-chart-g`); let resourceName = (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : ""; if(resourceName && resourceName.length > maxNodeTextLength) { + const sanitizedResourceName = $filter('sanitize')(resourceName); // When the graph is initially rendered all the links come after the nodes (when you look at the dom). // SVG components are painted in order of appearance. There is no concept of z-index, only the order. // As such, we need to move the nodes after the links so that when the tooltip renders it shows up on top // of the links and not underneath them. I tried rendering the links before the nodes but that lead to // some weird link animation that I didn't care to try to fix. - svgGroup.selectAll("g.WorkflowChart-node").each(function() { - this.parentNode.appendChild(this); - }); + svgGroup.selectAll("g.WorkflowChart-node").each(() => this.parentNode.appendChild(this)); // After the nodes have been properly placed after the links, we need to make sure that the node that // the user is hovering over is at the very end of the list. This way the tooltip will appear on top // of all other nodes. - svgGroup.selectAll("g.WorkflowChart-node").sort(function (a) { + svgGroup.selectAll("g.WorkflowChart-node").sort((a) => { return (a.index !== d.index) ? -1 : 1; }); // Render the tooltip quickly in the dom and then remove. This lets us know how big the tooltip is so that we can place // it properly on the workflow - let tooltipDimensionChecker = $(""); + let tooltipDimensionChecker = $(``); $('body').append(tooltipDimensionChecker); let tipWidth = $(tooltipDimensionChecker).outerWidth(); let tipHeight = $(tooltipDimensionChecker).outerHeight(); @@ -1023,9 +1049,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .attr("width", tipWidth) .attr("height", tipHeight+20) .attr("class", "WorkflowChart-tooltip") - .html(function(){ - return "
" + $filter('sanitize')(resourceName) + "
"; - }); + .html(`
${sanitizedResourceName}
`); } if (scope.graphState.isLinkMode && !d.isInvalidLinkTarget && scope.graphState.addLinkSource !== d.id) { @@ -1076,15 +1100,15 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .attr("stroke", "#D7D7D7") .attr('marker-mid', "url(#aw-workflow-chart-arrow)"); } - d3.select("#node-" + d.id) + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", true); } }) - .on("mouseout", function(d){ + .on("mouseout", (d) => { $('.WorkflowChart-tooltip').remove(); $('.WorkflowChart-potentialLink').remove(); if(d.id !== 1) { - d3.select("#node-" + d.id) + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", false); } }); @@ -1095,11 +1119,15 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .attr("width", "40px") .attr("dy", ".35em") .attr("class", "WorkflowChart-detailsLink") - .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }) - .on("mousedown", function(){ - d3.event.stopPropagation(); + .style("display", (d) => { + const isApprovalStep = d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type === 'workflow_approval'; + return d.job && + !isApprovalStep && + d.job.status && + d.job.id ? null : "none"; }) - .html(function (d) { + .on("mousedown", () => d3.event.stopPropagation()) + .html((d) => { let href = ""; if (d.job) { href = `/#/workflow_node_results/${d.job.id}`; @@ -1107,64 +1135,64 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', return `${TemplatesStrings.get('workflow_maker.DETAILS')}`; }); thisNode.append("circle") - .attr("id", function(d){return "node-" + d.id + "-add";}) + .attr("id", (d) => `node-${d.id}-add`) .attr("cx", nodeW) .attr("r", 10) .attr("class", "WorkflowChart-addCircle WorkflowChart-nodeAddCircle") - .style("display", function(d) { return d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }) + .style("display", (d) => { return d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }) .call(add_node_without_child) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) + .on("mouseover", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", true); - d3.select("#node-" + d.id + "-add") + d3.select(`#node-${d.id}-add`) .classed("WorkflowChart-addHovering", true); }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) + .on("mouseout", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", false); - d3.select("#node-" + d.id + "-add") + d3.select(`#node-${d.id}-add`) .classed("WorkflowChart-addHovering", false); }); thisNode.append("path") .attr("class", "WorkflowChart-nodeAddIcon") .style("fill", "white") - .attr("transform", function() { return "translate(" + nodeW + "," + 0 + ")"; }) + .attr("transform", `translate(${nodeW}, 0)`) .attr("d", d3.svg.symbol() .size(60) .type("cross") ) - .style("display", function(d) { return d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }) + .style("display", (d) => { return d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }) .call(add_node_without_child) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) + .on("mouseover", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", true); - d3.select("#node-" + d.id + "-add") + d3.select(`#node-${d.id}-add`) .classed("WorkflowChart-addHovering", true); }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) + .on("mouseout", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", false); - d3.select("#node-" + d.id + "-add") + d3.select(`#node-${d.id}-add`) .classed("WorkflowChart-addHovering", false); }); thisNode.append("circle") - .attr("id", function(d){return "node-" + d.id + "-link";}) + .attr("id", (d) => `node-${d.id}-link`) .attr("cx", nodeW) .attr("cy", nodeH/2) .attr("r", 10) .attr("class", "WorkflowChart-linkCircle") - .style("display", function(d) { return d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }) + .style("display", (d) => { return d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }) .call(add_link) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) + .on("mouseover", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", true); - d3.select("#node-" + d.id + "-link") + d3.select(`#node-${d.id}-link`) .classed("WorkflowChart-linkButtonHovering", true); }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) + .on("mouseout", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", false); - d3.select("#node-" + d.id + "-link") + d3.select(`#node-${d.id}-link`) .classed("WorkflowChart-linkButtonHovering", false); }); thisNode.append("foreignObject") @@ -1173,123 +1201,130 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', .attr("height", "17px") .attr("width", "13px") .style("font-size","14px") - .html(function () { - return ``; - }) + .html(``) .attr("class", "WorkflowChart-nodeLinkIcon") - .style("display", function(d) { return d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }) + .style("display", (d) => { return d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }) .call(add_link) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) + .on("mouseover", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", true); - d3.select("#node-" + d.id + "-link") + d3.select(`#node-${d.id}-link`) .classed("WorkflowChart-linkButtonHovering", true); }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) + .on("mouseout", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", false); - d3.select("#node-" + d.id + "-link") + d3.select(`#node-${d.id}-link`) .classed("WorkflowChart-linkButtonHovering", false); }); thisNode.append("circle") - .attr("id", function(d){return "node-" + d.id + "-remove";}) + .attr("id", (d) => `node-${d.id}-remove`) .attr("cx", nodeW) .attr("cy", nodeH) .attr("r", 10) .attr("class", "WorkflowChart-nodeRemoveCircle") - .style("display", function(d) { return (d.id === 1 || d.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .style("display", (d) => { return (d.id === 1 || d.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) .call(remove_node) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) + .on("mouseover", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", true); - d3.select("#node-" + d.id + "-remove") + d3.select(`#node-${d.id}-remove`) .classed("removeHovering", true); }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) + .on("mouseout", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", false); - d3.select("#node-" + d.id + "-remove") + d3.select(`#node-${d.id}-remove`) .classed("removeHovering", false); }); thisNode.append("path") .attr("class", "WorkflowChart-nodeRemoveIcon") .style("fill", "white") - .attr("transform", function() { return "translate(" + nodeW + "," + nodeH + ") rotate(-45)"; }) + .attr("transform", `translate(${nodeW}, ${nodeH}) rotate(-45)`) .attr("d", d3.svg.symbol() .size(60) .type("cross") ) - .style("display", function(d) { return (d.id === 1 || d.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .style("display", (d) => { return (d.id === 1 || d.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) .call(remove_node) - .on("mouseover", function(d) { - d3.select("#node-" + d.id) + .on("mouseover", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", true); - d3.select("#node-" + d.id + "-remove") + d3.select(`#node-${d.id}-remove`) .classed("removeHovering", true); }) - .on("mouseout", function(d){ - d3.select("#node-" + d.id) + .on("mouseout", (d) => { + d3.select(`#node-${d.id}`) .classed("WorkflowChart-nodeHovering", false); - d3.select("#node-" + d.id + "-remove") + d3.select(`#node-${d.id}-remove`) .classed("removeHovering", false); }); thisNode.append("circle") - .attr("class", function(d) { - - let statusClass = "WorkflowChart-nodeStatus "; - + .attr("class", (d) => { + let statusClasses = ["WorkflowChart-nodeStatus"]; + if(d.job){ switch(d.job.status) { case "pending": - statusClass += "WorkflowChart-nodeStatus--running"; - break; case "waiting": - statusClass += "WorkflowChart-nodeStatus--running"; - break; case "running": - statusClass += "WorkflowChart-nodeStatus--running"; + statusClasses.push("WorkflowChart-nodeStatus--running"); break; case "successful": - statusClass += "WorkflowChart-nodeStatus--success"; + statusClasses.push("WorkflowChart-nodeStatus--success"); break; case "failed": - statusClass += "WorkflowChart-nodeStatus--failed"; - break; case "error": - statusClass += "WorkflowChart-nodeStatus--failed"; + statusClasses.push("WorkflowChart-nodeStatus--failed"); break; case "canceled": - statusClass += "WorkflowChart-nodeStatus--canceled"; + statusClasses.push("WorkflowChart-nodeStatus--canceled"); break; } } - - return statusClass; + + return statusClasses.join(' '); }) - .style("display", function(d) { return d.job && d.job.status ? null : "none"; }) + .style("display", (d) => { return d.job && d.job.status ? null : "none"; }) .attr("cy", 10) .attr("cx", 10) - .attr("r", 6); + .attr("r", 6) + .each(function(d) { + if(d.job && d.job.status && (d.job.status === "pending" || d.job.status === "waiting" || d.job.status === "running")) { + // Pulse the circle + let circle = d3.select(this); + (function repeat() { + circle = circle.transition() + .duration(2000) + .attr("r", 6) + .transition() + .duration(2000) + .attr("r", 0) + .ease('sine') + .each("end", repeat); + })(); + } + }); thisNode.append("foreignObject") .attr("x", 5) .attr("y", 43) .style("font-size","0.7em") .attr("class", "WorkflowChart-elapsed") - .html(function (d) { + .html((d) => { if(d.job && d.job.elapsed) { let elapsedMs = d.job.elapsed * 1000; let elapsedMoment = moment.duration(elapsedMs); let paddedElapsedMoment = Math.floor(elapsedMoment.asHours()) < 10 ? "0" + Math.floor(elapsedMoment.asHours()) : Math.floor(elapsedMoment.asHours()); let elapsedString = paddedElapsedMoment + moment.utc(elapsedMs).format(":mm:ss"); - return "
" + elapsedString + "
"; + return `
${elapsedString}
`; } else { return ""; } }) - .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); + .style("display", (d) => { return (d.job && d.job.elapsed) ? null : "none"; }); } }); @@ -1303,7 +1338,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', svgGroup.selectAll(".WorkflowChart-node").order(); } else if(!scope.watchDimensionsSet){ - scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){ + scope.watchDimensionsSet = scope.$watch('dimensionsSet', () => { if(scope.dimensionsSet) { scope.watchDimensionsSet(); scope.watchDimensionsSet = null; @@ -1314,7 +1349,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }; function add_node_without_child() { - this.on("click", function(d) { + this.on("click", (d) => { if(!scope.readOnly && !scope.graphState.isLinkMode) { scope.addNodeWithoutChild({ parent: d @@ -1324,7 +1359,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } function add_node_with_child() { - this.on("click", function(d) { + this.on("click", (d) => { if(!scope.readOnly && !scope.graphState.isLinkMode && d.edgeType !== 'placeholder') { scope.addNodeWithChild({ link: d @@ -1334,7 +1369,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } function remove_node() { - this.on("click", function(d) { + this.on("click", (d) => { if(d.id !== 1 && !scope.readOnly && !scope.graphState.isLinkMode) { scope.deleteNode({ nodeToDelete: d @@ -1344,7 +1379,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } function node_click() { - this.on("click", function(d) { + this.on("click", (d) => { if(d.id !== scope.graphState.nodeBeingAdded){ if(scope.graphState.isLinkMode && !d.isInvalidLinkTarget && scope.graphState.addLinkSource !== d.id) { $('.WorkflowChart-potentialLink').remove(); @@ -1362,7 +1397,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } function edit_link() { - this.on("click", function(d) { + this.on("click", (d) => { if(!scope.graphState.isLinkMode && d.source.id !== 1 && d.source.id !== scope.graphState.nodeBeingAdded && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details'){ scope.editLink({ linkToEdit: d @@ -1372,7 +1407,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', } function add_link() { - this.on("click", function(d) { + this.on("click", (d) => { if (!scope.readOnly && !scope.graphState.isLinkMode) { scope.selectNodeForLinking({ nodeToStartLink: d @@ -1381,29 +1416,29 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', }); } - scope.$on('refreshWorkflowChart', function(){ + scope.$on('refreshWorkflowChart', () => { if(scope.graphState) { updateGraph(); } }); - scope.$on('panWorkflowChart', function(evt, params) { + scope.$on('panWorkflowChart', (evt, params) => { manualPan(params.direction); }); - scope.$on('resetWorkflowChart', function(){ + scope.$on('resetWorkflowChart', () => { resetZoomAndPan(); }); - scope.$on('zoomWorkflowChart', function(evt, params) { + scope.$on('zoomWorkflowChart', (evt, params) => { manualZoom(params.zoom); }); - scope.$on('zoomToFitChart', function() { + scope.$on('zoomToFitChart', () => { zoomToFitChart(); }); - let clearWatchGraphState = scope.$watch('graphState.arrayOfNodesForChart', function(newVal) { + let clearWatchGraphState = scope.$watch('graphState.arrayOfNodesForChart', (newVal) => { if(newVal) { updateGraph(); clearWatchGraphState(); @@ -1432,10 +1467,10 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', scope.dimensionsSet = true; line = d3.svg.line() - .x(function (d) { + .x((d) => { return d.x; }) - .y(function (d) { + .y((d) => { return d.y; }); @@ -1449,7 +1484,7 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', svgGroup = baseSvg.append("g") .attr("id", "aw-workflow-chart-g") - .attr("transform", "translate(0," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")"); + .attr("transform", `translate(0, ${(windowHeight/2 - rootH/2 - startNodeOffsetY)})`); const defs = baseSvg.append("defs"); @@ -1469,16 +1504,16 @@ export default ['moment', '$timeout', '$window', '$filter', 'TemplatesStrings', angular.element($window).on('resize', onResize); scope.$on('$destroy', cleanUpResize); - scope.$on('workflowDetailsResized', function(){ + scope.$on('workflowDetailsResized', () => { $('.WorkflowMaker-chart').hide(); - $timeout(function(){ + $timeout(() => { onResize(); $('.WorkflowMaker-chart').show(); }); }); } else { - scope.$on('workflowMakerModalResized', function(){ + scope.$on('workflowMakerModalResized', () => { let dimensions = calcAvailableScreenSpace(); $('.WorkflowMaker-chart').css("width", dimensions.width); diff --git a/awx/ui/client/src/templates/workflows/workflow-key/main.js b/awx/ui/client/src/templates/workflows/workflow-key/main.js new file mode 100644 index 0000000000..5721249659 --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-key/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2019 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import workflowKey from './workflow-key.directive'; + +export default + angular.module('workflowKey', []) + .directive('workflowKey', workflowKey); diff --git a/awx/ui/client/src/templates/workflows/workflow-key/workflow-key.directive.js b/awx/ui/client/src/templates/workflows/workflow-key/workflow-key.directive.js new file mode 100644 index 0000000000..b8f92c0fa9 --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-key/workflow-key.directive.js @@ -0,0 +1,18 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['templateUrl', 'TemplatesStrings', + function(templateUrl, TemplatesStrings) { + return { + scope: {}, + templateUrl: templateUrl('templates/workflows/workflow-key/workflow-key'), + restrict: 'E', + link: function(scope) { + scope.strings = TemplatesStrings; + } + }; + } +]; diff --git a/awx/ui/client/src/templates/workflows/workflow-key/workflow-key.partial.html b/awx/ui/client/src/templates/workflows/workflow-key/workflow-key.partial.html new file mode 100644 index 0000000000..6c3aac61a6 --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-key/workflow-key.partial.html @@ -0,0 +1,43 @@ + \ No newline at end of file diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/main.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/main.js index 426eaa131c..39153756eb 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/main.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/main.js @@ -1,7 +1,9 @@ import workflowLinkForm from './workflow-link-form.directive'; import workflowNodeForm from './workflow-node-form.directive'; +import workflowNodeFormService from './workflow-node-form.service'; export default angular.module('templates.workflowMaker.forms', []) .directive('workflowLinkForm', workflowLinkForm) - .directive('workflowNodeForm', workflowNodeForm); + .directive('workflowNodeForm', workflowNodeForm) + .service('WorkflowNodeFormService', workflowNodeFormService); diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.service.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.service.js new file mode 100644 index 0000000000..460f6ccfa0 --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.service.js @@ -0,0 +1,67 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['TemplateList', 'ProjectList', 'InventorySourcesList', 'i18n', + function(TemplateList, ProjectList, InventorySourcesList, i18n){ + return { + inventorySourceListDefinition: function() { + const inventorySourceList = _.cloneDeep(InventorySourcesList); + inventorySourceList.name = 'wf_maker_inventory_sources'; + inventorySourceList.iterator = 'wf_maker_inventory_source'; + inventorySourceList.maxVisiblePages = 5; + inventorySourceList.searchBarFullWidth = true; + inventorySourceList.disableRow = "{{ readOnly }}"; + inventorySourceList.disableRowValue = 'readOnly'; + + return inventorySourceList; + }, + projectListDefinition: function(){ + const projectList = _.cloneDeep(ProjectList); + delete projectList.fields.status; + delete projectList.fields.scm_type; + delete projectList.fields.last_updated; + projectList.name = 'wf_maker_projects'; + projectList.iterator = 'wf_maker_project'; + projectList.fields.name.columnClass = "col-md-11"; + projectList.maxVisiblePages = 5; + projectList.searchBarFullWidth = true; + projectList.disableRow = "{{ readOnly }}"; + projectList.disableRowValue = 'readOnly'; + + return projectList; + }, + templateListDefinition: function(){ + const templateList = _.cloneDeep(TemplateList); + delete templateList.actions; + delete templateList.fields.type; + delete templateList.fields.description; + delete templateList.fields.smart_status; + delete templateList.fields.labels; + delete templateList.fieldActions; + templateList.name = 'wf_maker_templates'; + templateList.iterator = 'wf_maker_template'; + templateList.fields.name.columnClass = "col-md-8"; + templateList.fields.name.tag = i18n._('WORKFLOW'); + templateList.fields.name.showTag = "{{wf_maker_template.type === 'workflow_job_template'}}"; + templateList.disableRow = "{{ readOnly }}"; + templateList.disableRowValue = 'readOnly'; + templateList.basePath = 'unified_job_templates'; + templateList.fields.info = { + ngInclude: "'/static/partials/job-template-details.html'", + type: 'template', + columnClass: 'col-md-3', + infoHeaderClass: 'col-md-3', + label: '', + nosort: true + }; + templateList.maxVisiblePages = 5; + templateList.searchBarFullWidth = true; + + return templateList; + } + }; + } +]; diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less index 87f20ce5db..f163c845e3 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less @@ -7,6 +7,15 @@ .ui-dialog-buttonpane, .ui-dialog-titlebar { display:none; } + + input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + input[type="number"] { + -moz-appearance: textfield; + } } .WorkflowMaker-header { @@ -380,11 +389,11 @@ .Key-icon--circle { border-radius: 50%; - width: 20px; - height: 20px; + width: 24px; + height: 24px; color: @default-bg; text-align: center; - line-height: 20px; + line-height: 24px; margin: 4px 5px 5px 0px; } diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js index 925c9a03fd..fb81dd1618 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js @@ -145,21 +145,24 @@ export default ['$scope', 'TemplatesService', let editPromises = []; let credentialRequests = []; + // TODO: clean up data generation of approval template requests Object.keys(nodeRef).map((workflowMakerNodeId) => { const node = nodeRef[workflowMakerNodeId]; if (node.isNew) { if (node.unifiedJobTemplate && node.unifiedJobTemplate.unified_job_type === "workflow_approval") { - approvalTemplatePromises.push(TemplatesService.createApprovalTemplate({ - name: node.unifiedJobTemplate.name - }).then(({data: approvalTemplateData}) => { - addPromises.push(TemplatesService.addWorkflowNode({ - url: $scope.workflowJobTemplateObj.related.workflow_nodes, + addPromises.push(TemplatesService.addWorkflowNode({ + url: $scope.workflowJobTemplateObj.related.workflow_nodes, + data: {} + }).then(({data}) => { + approvalTemplatePromises.push(TemplatesService.createApprovalTemplate({ + url: data.related.create_approval_template, data: { - unified_job_template: approvalTemplateData.id + name: node.unifiedJobTemplate.name, + description: node.unifiedJobTemplate.description } - }).then(({data: nodeData}) => { - node.originalNodeObject = nodeData; - nodeIdToChartNodeIdMapping[nodeData.id] = parseInt(workflowMakerNodeId); + }).then(() => { + node.originalNodeObject = data; + nodeIdToChartNodeIdMapping[data.id] = parseInt(workflowMakerNodeId); }).catch(({ data, status }) => { Wait('stop'); ProcessErrors($scope, data, status, null, { @@ -222,20 +225,11 @@ export default ['$scope', 'TemplatesService', })); } else { approvalTemplatePromises.push(TemplatesService.createApprovalTemplate({ - name: node.unifiedJobTemplate.name - }).then(({data: approvalTemplateData}) => { - // Make sure that this isn't overwriting everything on the node... - editPromises.push(TemplatesService.editWorkflowNode({ - id: node.originalNodeObject.id, - data: { - unified_job_template: approvalTemplateData.id - } - }).catch(({ data, status }) => { - Wait('stop'); - ProcessErrors($scope, data, status, null, { - hdr: $scope.strings.get('error.HEADER') - }); - })); + url: node.originalNodeObject.related.create_approval_template, + data: { + name: node.unifiedJobTemplate.name, + description: node.unifiedJobTemplate.description + } }).catch(({ data, status }) => { Wait('stop'); ProcessErrors($scope, data, status, null, { @@ -302,176 +296,173 @@ export default ['$scope', 'TemplatesService', return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); }); - $q.all(approvalTemplatePromises) + $q.all(addPromises.concat(editPromises, deletePromises)) .then(() => { - $q.all(addPromises.concat(editPromises, deletePromises)) - .then(() => { - let disassociatePromises = []; - let associatePromises = []; - let linkMap = {}; + $q.all(approvalTemplatePromises) + .then(() => { + let disassociatePromises = []; + let associatePromises = []; + let linkMap = {}; - // Build a link map for easy access - $scope.graphState.arrayOfLinksForChart.forEach(link => { - // link.source.id of 1 is our artificial start node - if (link.source.id !== 1) { - const sourceNodeId = nodeRef[link.source.id].originalNodeObject.id; - const targetNodeId = nodeRef[link.target.id].originalNodeObject.id; - if (!linkMap[sourceNodeId]) { - linkMap[sourceNodeId] = {}; + // Build a link map for easy access + $scope.graphState.arrayOfLinksForChart.forEach(link => { + // link.source.id of 1 is our artificial start node + if (link.source.id !== 1) { + const sourceNodeId = nodeRef[link.source.id].originalNodeObject.id; + const targetNodeId = nodeRef[link.target.id].originalNodeObject.id; + if (!linkMap[sourceNodeId]) { + linkMap[sourceNodeId] = {}; + } + + linkMap[sourceNodeId][targetNodeId] = link.edgeType; } + }); - linkMap[sourceNodeId][targetNodeId] = link.edgeType; - } - }); - - Object.keys(nodeRef).map((workflowNodeId) => { - let nodeId = nodeRef[workflowNodeId].originalNodeObject.id; - if (nodeRef[workflowNodeId].originalNodeObject.success_nodes) { - nodeRef[workflowNodeId].originalNodeObject.success_nodes.forEach((successNodeId) => { - if ( - !deletedNodeIds.includes(successNodeId) && - (!linkMap[nodeId] || - !linkMap[nodeId][successNodeId] || - linkMap[nodeId][successNodeId] !== "success") - ) { - disassociatePromises.push( - TemplatesService.disassociateWorkflowNode({ - parentId: nodeId, - nodeId: successNodeId, - edge: "success" - }) - ); - } - }); - } - if (nodeRef[workflowNodeId].originalNodeObject.failure_nodes) { - nodeRef[workflowNodeId].originalNodeObject.failure_nodes.forEach((failureNodeId) => { - if ( - !deletedNodeIds.includes(failureNodeId) && - (!linkMap[nodeId] || - !linkMap[nodeId][failureNodeId] || - linkMap[nodeId][failureNodeId] !== "failure") - ) { - disassociatePromises.push( - TemplatesService.disassociateWorkflowNode({ - parentId: nodeId, - nodeId: failureNodeId, - edge: "failure" - }) - ); - } - }); - } - if (nodeRef[workflowNodeId].originalNodeObject.always_nodes) { - nodeRef[workflowNodeId].originalNodeObject.always_nodes.forEach((alwaysNodeId) => { - if ( - !deletedNodeIds.includes(alwaysNodeId) && - (!linkMap[nodeId] || - !linkMap[nodeId][alwaysNodeId] || - linkMap[nodeId][alwaysNodeId] !== "always") - ) { - disassociatePromises.push( - TemplatesService.disassociateWorkflowNode({ - parentId: nodeId, - nodeId: alwaysNodeId, - edge: "always" - }) - ); - } - }); - } - }); - - Object.keys(linkMap).map((sourceNodeId) => { - Object.keys(linkMap[sourceNodeId]).map((targetNodeId) => { - const sourceChartNodeId = nodeIdToChartNodeIdMapping[sourceNodeId]; - const targetChartNodeId = nodeIdToChartNodeIdMapping[targetNodeId]; - switch(linkMap[sourceNodeId][targetNodeId]) { - case "success": + Object.keys(nodeRef).map((workflowNodeId) => { + let nodeId = nodeRef[workflowNodeId].originalNodeObject.id; + if (nodeRef[workflowNodeId].originalNodeObject.success_nodes) { + nodeRef[workflowNodeId].originalNodeObject.success_nodes.forEach((successNodeId) => { if ( - !nodeRef[sourceChartNodeId].originalNodeObject.success_nodes || - !nodeRef[sourceChartNodeId].originalNodeObject.success_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + !deletedNodeIds.includes(successNodeId) && + (!linkMap[nodeId] || + !linkMap[nodeId][successNodeId] || + linkMap[nodeId][successNodeId] !== "success") ) { - associatePromises.push( - TemplatesService.associateWorkflowNode({ - parentId: parseInt(sourceNodeId), - nodeId: parseInt(targetNodeId), + disassociatePromises.push( + TemplatesService.disassociateWorkflowNode({ + parentId: nodeId, + nodeId: successNodeId, edge: "success" }) ); } - break; - case "failure": + }); + } + if (nodeRef[workflowNodeId].originalNodeObject.failure_nodes) { + nodeRef[workflowNodeId].originalNodeObject.failure_nodes.forEach((failureNodeId) => { if ( - !nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes || - !nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + !deletedNodeIds.includes(failureNodeId) && + (!linkMap[nodeId] || + !linkMap[nodeId][failureNodeId] || + linkMap[nodeId][failureNodeId] !== "failure") ) { - associatePromises.push( - TemplatesService.associateWorkflowNode({ - parentId: parseInt(sourceNodeId), - nodeId: parseInt(targetNodeId), + disassociatePromises.push( + TemplatesService.disassociateWorkflowNode({ + parentId: nodeId, + nodeId: failureNodeId, edge: "failure" }) ); } - break; - case "always": + }); + } + if (nodeRef[workflowNodeId].originalNodeObject.always_nodes) { + nodeRef[workflowNodeId].originalNodeObject.always_nodes.forEach((alwaysNodeId) => { if ( - !nodeRef[sourceChartNodeId].originalNodeObject.always_nodes || - !nodeRef[sourceChartNodeId].originalNodeObject.always_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + !deletedNodeIds.includes(alwaysNodeId) && + (!linkMap[nodeId] || + !linkMap[nodeId][alwaysNodeId] || + linkMap[nodeId][alwaysNodeId] !== "always") ) { - associatePromises.push( - TemplatesService.associateWorkflowNode({ - parentId: parseInt(sourceNodeId), - nodeId: parseInt(targetNodeId), + disassociatePromises.push( + TemplatesService.disassociateWorkflowNode({ + parentId: nodeId, + nodeId: alwaysNodeId, edge: "always" }) ); } - break; + }); } }); - }); - $q.all(disassociatePromises) - .then(() => { - let credentialPromises = credentialRequests.map((request) => { - return TemplatesService.postWorkflowNodeCredential({ - id: request.id, - data: request.data - }); - }); - - return $q.all(associatePromises.concat(credentialPromises)) - .then(() => { - Wait('stop'); - $scope.workflowChangesUnsaved = false; - $scope.workflowChangesStarted = false; - $scope.closeDialog(); - }).catch(({ data, status }) => { - Wait('stop'); - ProcessErrors($scope, data, status, null, { - hdr: $scope.strings.get('error.HEADER') - }); - }); - }).catch(({ - data, - status - }) => { - Wait('stop'); - ProcessErrors($scope, data, status, null, { - hdr: $scope.strings.get('error.HEADER') + Object.keys(linkMap).map((sourceNodeId) => { + Object.keys(linkMap[sourceNodeId]).map((targetNodeId) => { + const sourceChartNodeId = nodeIdToChartNodeIdMapping[sourceNodeId]; + const targetChartNodeId = nodeIdToChartNodeIdMapping[targetNodeId]; + switch(linkMap[sourceNodeId][targetNodeId]) { + case "success": + if ( + !nodeRef[sourceChartNodeId].originalNodeObject.success_nodes || + !nodeRef[sourceChartNodeId].originalNodeObject.success_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + ) { + associatePromises.push( + TemplatesService.associateWorkflowNode({ + parentId: parseInt(sourceNodeId), + nodeId: parseInt(targetNodeId), + edge: "success" + }) + ); + } + break; + case "failure": + if ( + !nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes || + !nodeRef[sourceChartNodeId].originalNodeObject.failure_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + ) { + associatePromises.push( + TemplatesService.associateWorkflowNode({ + parentId: parseInt(sourceNodeId), + nodeId: parseInt(targetNodeId), + edge: "failure" + }) + ); + } + break; + case "always": + if ( + !nodeRef[sourceChartNodeId].originalNodeObject.always_nodes || + !nodeRef[sourceChartNodeId].originalNodeObject.always_nodes.includes(nodeRef[targetChartNodeId].originalNodeObject.id) + ) { + associatePromises.push( + TemplatesService.associateWorkflowNode({ + parentId: parseInt(sourceNodeId), + nodeId: parseInt(targetNodeId), + edge: "always" + }) + ); + } + break; + } }); }); - }).catch(({ data, status }) => { - Wait('stop'); - ProcessErrors($scope, data, status, null, { - hdr: $scope.strings.get('error.HEADER') + + $q.all(disassociatePromises) + .then(() => { + let credentialPromises = credentialRequests.map((request) => { + return TemplatesService.postWorkflowNodeCredential({ + id: request.id, + data: request.data + }); + }); + + return $q.all(associatePromises.concat(credentialPromises)) + .then(() => { + Wait('stop'); + $scope.workflowChangesUnsaved = false; + $scope.workflowChangesStarted = false; + $scope.closeDialog(); + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') + }); + }); + }).catch(({ + data, + status + }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') + }); + }); }); + }).catch(({ data, status }) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: $scope.strings.get('error.HEADER') }); - }) - .catch(() => { - // TODO: handle }); } else { diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html index fabaa4c04c..92e904aa3d 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html @@ -68,39 +68,7 @@
-
    -
  • -

    {{strings.get('workflow_maker.KEY')}}

    -
  • -
  • -
    -

    {{strings.get('workflow_maker.ON_SUCCESS')}}

    -
  • -
  • -
    -

    {{strings.get('workflow_maker.ON_FAILURE')}}

    -
  • -
  • -
    -

    {{strings.get('workflow_maker.ALWAYS')}}

    -
  • -
  • -
    P
    -

    {{strings.get('workflow_maker.PROJECT_SYNC')}}

    -
  • -
  • -
    I
    -

    {{strings.get('workflow_maker.INVENTORY_SYNC')}}

    -
  • -
  • -
    W
    -

    {{strings.get('workflow_maker.WORKFLOW')}}

    -
  • -
  • -
    !
    -

    {{strings.get('workflow_maker.WARNING')}}

    -
  • -
+
{{strings.get('workflow_maker.TOTAL_NODES')}} diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index 4fad491288..61ca637641 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -327,35 +327,7 @@
-
    -
  • -

    {{strings.legend.KEY}}

    -
  • -
  • -
    -

    {{strings.legend.ON_SUCCESS}}

    -
  • -
  • -
    -

    {{strings.legend.ON_FAILURE}}

    -
  • -
  • -
    -

    {{strings.legend.ALWAYS}}

    -
  • -
  • -
    P
    -

    {{strings.legend.PROJECT_SYNC}}

    -
  • -
  • -
    I
    -

    {{strings.legend.INVENTORY_SYNC}}

    -
  • -
  • -
    W
    -

    {{strings.legend.WORKFLOW}}

    -
  • -
+