From a2d0c96a50ae343b31b7cd46d9f33a75e0db644d Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 7 Dec 2016 14:23:24 -0500 Subject: [PATCH] Added manual controls for zooming/panning the workflow graph --- awx/ui/client/src/templates/main.js | 3 +- .../workflow-chart.directive.js | 109 +++++++++++++----- .../workflows/workflow-controls/main.js | 11 ++ .../workflow-controls.block.less | 69 +++++++++++ .../workflow-controls.directive.js | 72 ++++++++++++ .../workflow-controls.partial.html | 19 +++ .../workflow-maker/workflow-maker.block.less | 65 +++++++++-- .../workflow-maker.controller.js | 27 +++++ .../workflow-maker.partial.html | 6 +- .../workflow-results.controller.js | 27 +++++ .../workflow-results.partial.html | 46 +++++--- 11 files changed, 391 insertions(+), 63 deletions(-) create mode 100644 awx/ui/client/src/templates/workflows/workflow-controls/main.js create mode 100644 awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.block.less create mode 100644 awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.directive.js create mode 100644 awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.partial.html diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index 7bc7dab18b..1330df98ea 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -14,6 +14,7 @@ import workflowEdit from './workflows/edit-workflow/main'; import labels from './labels/main'; import workflowChart from './workflows/workflow-chart/main'; import workflowMaker from './workflows/workflow-maker/main'; +import workflowControls from './workflows/workflow-controls/main'; import templatesListRoute from './list/templates-list.route'; import workflowService from './workflows/workflow.service'; import templateCopyService from './copy-template/template-copy.service'; @@ -21,7 +22,7 @@ import templateCopyService from './copy-template/template-copy.service'; export default angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesAdd.name, jobTemplatesEdit.name, labels.name, workflowAdd.name, workflowEdit.name, - workflowChart.name, workflowMaker.name + workflowChart.name, workflowMaker.name, workflowControls.name ]) .service('TemplatesService', templatesService) .service('WorkflowService', workflowService) 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 9e5ad95475..3b3bc0939c 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 @@ -14,16 +14,12 @@ export default [ '$state', addNode: '&', editNode: '&', deleteNode: '&', + workflowZoomed: '&', mode: '@' }, restrict: 'E', link: function(scope, element) { - scope.$watch('canAddWorkflowJobTemplate', function() { - // Redraw the graph if permissions change - update(); - }); - let margin = {top: 20, right: 20, bottom: 20, left: 20}, width = 950, height = 590 - margin.top - margin.bottom, @@ -31,8 +27,7 @@ export default [ '$state', rectW = 120, rectH = 60, rootW = 60, - rootH = 40, - m = [40, 240, 40, 240]; + rootH = 40; let tree = d3.layout.tree() .size([height, width]); @@ -41,6 +36,19 @@ export default [ '$state', .x(function(d){return d.x;}) .y(function(d){return d.y;}); + let zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]); + + let baseSvg = d3.select(element[0]).append("svg") + .attr("width", width) + .attr("height", height) + .attr("class", "WorkflowChart-svg") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")") + .call(zoomObj + .on("zoom", naturalZoom) + ); + + let svgGroup = baseSvg.append("g"); + function lineData(d){ let sourceX = d.source.isStartNode ? d.source.y + rootW : d.source.y + rectW; @@ -76,33 +84,55 @@ export default [ '$state', } } - let baseSvg = d3.select(element[0]).append("svg") - .attr("width", width) - .attr("height", height) - .attr("class", "WorkflowChart-svg") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")") - .call(d3.behavior.zoom() - .scaleExtent([0.5, 5]) - .on("zoom", zoom) - ); - - let svgGroup = baseSvg.append("g"); - - function zoom() { + // This is the zoom function called by using the mousewheel/click and drag + function naturalZoom() { let scale = d3.event.scale, - translation = d3.event.translate, - tbound = -height * scale, - bbound = height * scale, - lbound = (-width + m[1]) * scale, - rbound = (width - m[3]) * scale; - // limit translation to thresholds - translation = [ - Math.max(Math.min(translation[0], rbound), lbound), - Math.max(Math.min(translation[1], bbound), tbound) - ]; - + translation = d3.event.translate; svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")"); + + scope.workflowZoomed({ + zoom: scale + }); + } + + // This is the zoom that gets called when the user interacts with the manual zoom controls + function manualZoom(zoom) { + let scale = zoom / 100, + translation = zoomObj.translate(), + origZoom = zoomObj.scale(), + unscaledOffsetX = (translation[0] + ((width*origZoom) - width)/2)/origZoom, + unscaledOffsetY = (translation[1] + ((height*origZoom) - height)/2)/origZoom, + translateX = unscaledOffsetX*scale - ((scale*width)-width)/2, + translateY = unscaledOffsetY*scale - ((scale*height)-height)/2; + + svgGroup.attr("transform", "translate(" + [translateX, translateY] + ")scale(" + scale + ")"); + zoomObj.scale(scale); + zoomObj.translate([translateX, translateY]); + } + + function manualPan(direction) { + let scale = zoomObj.scale(), + distance = 150 * scale, + translateX, + translateY, + translateCoords = zoomObj.translate(); + if (direction === 'left' || direction === 'right') { + translateX = direction === 'left' ? translateCoords[0] - distance : translateCoords[0] + distance; + translateY = translateCoords[1]; + } else if (direction === 'up' || direction === 'down') { + translateX = translateCoords[0]; + translateY = direction === 'up' ? translateCoords[1] - distance : translateCoords[1] + distance; + } + svgGroup.attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")"); + zoomObj.translate([translateX, translateY]); + } + + function resetZoomAndPan() { + svgGroup.attr("transform", "translate(" + 0 + "," + 0 + ")scale(" + 1 + ")"); + // Update the zoomObj + zoomObj.scale(1); + zoomObj.translate([0,0]); } function update() { @@ -637,10 +667,27 @@ export default [ '$state', }); } + scope.$watch('canAddWorkflowJobTemplate', function() { + // Redraw the graph if permissions change + update(); + }); + scope.$on('refreshWorkflowChart', function(){ update(); }); + scope.$on('panWorkflowChart', function(evt, params) { + manualPan(params.direction); + }); + + scope.$on('resetWorkflowChart', function(){ + resetZoomAndPan(); + }); + + scope.$on('zoomWorkflowChart', function(evt, params) { + manualZoom(params.zoom); + }); + } }; }]; diff --git a/awx/ui/client/src/templates/workflows/workflow-controls/main.js b/awx/ui/client/src/templates/workflows/workflow-controls/main.js new file mode 100644 index 0000000000..77a1ce6337 --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-controls/main.js @@ -0,0 +1,11 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import workflowControls from './workflow-controls.directive'; + +export default + angular.module('workflowControls', []) + .directive('workflowControls', workflowControls); diff --git a/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.block.less b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.block.less new file mode 100644 index 0000000000..08fa7e9e57 --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.block.less @@ -0,0 +1,69 @@ +@import "./client/src/shared/branding/colors.default.less"; + +.WorkflowControls { + display: flex; +} + +.WorkflowControls-Zoom { + display: flex; + flex: 1 0 auto; +} +.WorkflowControls-Pan { + flex: 0 0 85px; +} +.WorkflowControls-Pan--button { + color: @default-icon; + font-size: 1.5em; +} +.WorkflowControls-Pan--button:hover { + color: @default-link-hov; +} +.WorkflowControls-Pan--home { + position: relative; + top: 9px; + right: 38px; + font-size: 1em; +} +.WorkflowControls-Pan--up { + position: relative; + top: -4px; + left: 16px; +} +.WorkflowControls-Pan--down { + position: relative; + top: 25px; + right: 0px; +} +.WorkflowControls-Pan--right { + position: relative; + top: 12px; + right: 7px; +} +.WorkflowControls-Pan--left { + position: relative; + top: 12px; + right: 31px; +} +.WorkflowControls-Zoom--button { + line-height: 60px; + color: @default-icon; +} +.WorkflowControls-Zoom--button:hover { + color: @default-link-hov; +} +.WorkflowControls-Zoom--minus { + margin-left: 20px; + padding-right: 8px; +} +.WorkflowControls-Zoom--plus { + padding-left: 8px; +} +.WorkflowControls-zoomSlider { + width: 150px; +} +.WorkflowControls-zoomPercentage { + text-align: center; + font-size: 0.7em; + height: 24px; + line-height: 24px; +} diff --git a/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.directive.js b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.directive.js new file mode 100644 index 0000000000..811cfbaafb --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.directive.js @@ -0,0 +1,72 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['templateUrl', + function(templateUrl) { + return { + scope: { + panChart: '&', + resetChart: '&', + zoomChart: '&' + }, + templateUrl: templateUrl('templates/workflows/workflow-controls/workflow-controls'), + restrict: 'E', + link: function(scope) { + + function init() { + scope.zoom = 100; + $( "#slider" ).slider({ + value:100, + min: 50, + max: 200, + step: 10, + slide: function( event, ui ) { + scope.zoom = ui.value; + scope.zoomChart({ + zoom: scope.zoom + }); + } + }); + } + + scope.pan = function(direction) { + scope.panChart({ + direction: direction + }); + }; + + scope.reset = function() { + scope.zoom = 100; + $("#slider").slider('value',scope.zoom); + scope.resetChart(); + }; + + scope.zoomIn = function() { + scope.zoom = Math.ceil((scope.zoom + 10) / 10) * 10 < 200 ? Math.ceil((scope.zoom + 10) / 10) * 10 : 200; + $("#slider").slider('value',scope.zoom); + scope.zoomChart({ + zoom: scope.zoom + }); + }; + + scope.zoomOut = function() { + scope.zoom = Math.floor((scope.zoom - 10) / 10) * 10 > 50 ? Math.floor((scope.zoom - 10) / 10) * 10 : 50; + $("#slider").slider('value',scope.zoom); + scope.zoomChart({ + zoom: scope.zoom + }); + }; + + scope.$on('workflowZoomed', function(evt, params) { + scope.zoom = Math.round(params.zoom * 10) * 10; + $("#slider").slider('value',scope.zoom); + }); + + init(); + } + }; + } +]; diff --git a/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.partial.html b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.partial.html new file mode 100644 index 0000000000..2115bba62c --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-controls/workflow-controls.partial.html @@ -0,0 +1,19 @@ +
+
+ +
+
+
{{zoom}}%
+
+
+
+ +
+
+
+ + + + + +
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 bd0c733046..8b323570a3 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 @@ -37,7 +37,7 @@ } .WorkflowMaker-contentHolder { display: flex; - border: 1px solid #EBEBEB; + border: 1px solid @default-list-header-bg; height: ~"calc(100% - 85px)"; } .WorkflowMaker-contentLeft { @@ -47,7 +47,7 @@ } .WorkflowMaker-contentRight { flex: 0 0 400px; - border-left: 1px solid #EBEBEB; + border-left: 1px solid @default-list-header-bg; padding: 20px; height: 100%; overflow-y: scroll; @@ -120,14 +120,14 @@ margin-bottom: 20px; } .WorkflowMaker-formHelp { - color: #707070; + color: @default-interface-txt; } .WorkflowMaker-formLists { margin-bottom: 20px; } .WorkflowMaker-formTitle { display: flex; - color: #707070; + color: @default-interface-txt; margin-right: 10px; } .WorkflowMaker-formLabel { @@ -140,13 +140,13 @@ display: flex; } .WorkflowMaker-totalJobs { - margin-right: 10px; + margin-right: 5px; } .WorkflowLegend-maker { display: flex; height: 40px; line-height: 40px; - color: #707070; + color: @default-interface-txt; } .WorkflowLegend-maker--left { display: flex; @@ -157,6 +157,7 @@ flex: 0 0 170px; text-align: right; padding-right: 20px; + position: relative; } .WorkflowLegend-onSuccessLegend { height: 4px; @@ -167,21 +168,21 @@ .WorkflowLegend-onFailLegend { height: 4px; width: 20px; - background-color: #d9534f; + background-color: @default-err; margin: 18px 5px 18px 0px; } .WorkflowLegend-alwaysLegend { height: 4px; width: 20px; - background-color: #337ab7; + background-color: @default-link; margin: 18px 5px 18px 0px; } .WorkflowLegend-letterCircle{ border-radius: 50%; width: 20px; height: 20px; - background: #848992; - color: #FFF; + background: @default-icon; + color: @default-bg; text-align: center; margin: 10px 5px 10px 0px; line-height: 20px; @@ -191,7 +192,7 @@ height: 40px; line-height: 40px; padding-left: 20px; - border: 1px solid #F6F6F6; + border: 1px solid @default-no-items-bord; margin-top:10px; } .WorkflowLegend-legendItem { @@ -200,3 +201,45 @@ .WorkflowLegend-legendItem:not(:last-child) { padding-right: 20px; } +.WorkflowLegend-details--left { + display: flex; + flex: 1 0 auto; +} +.WorkflowLegend-details--right { + flex: 0 0 44px; + text-align: right; + padding-right: 20px; + position:relative; +} +.WorkflowMaker-manualControlsIcon { + color: @default-icon; + vertical-align: middle; + font-size: 1.2em; + margin-left: 10px; +} +.WorkflowMaker-manualControlsIcon:hover { + color: @default-link-hov; + cursor: pointer; +} +.WorkflowMaker-manualControlsIcon--active { + color: @default-link-hov; +} +.WorkflowMaker-manualControls { + position: absolute; + left: -122px; + height: 60px; + width: 293px; + background-color: @default-bg; + display: flex; + border: 1px solid @default-list-header-bg; +} +.WorkflowLegend-manualControls { + position: absolute; + left: -245px; + top: 38px; + height: 60px; + width: 290px; + background-color: @default-bg; + display: flex; + border: 1px solid @default-list-header-bg; +} 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 e6bd83d9b3..ac2fe30396 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 @@ -37,6 +37,7 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr function init() { $scope.treeDataMaster = angular.copy($scope.treeData.data); + $scope.showManualControls = false; $scope.$broadcast("refreshWorkflowChart"); } @@ -574,6 +575,32 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr edgeFlags: $scope.edgeFlags }); } + + $scope.toggleManualControls = function() { + $scope.showManualControls = !$scope.showManualControls; + }; + + $scope.panChart = function(direction) { + $scope.$broadcast('panWorkflowChart', { + direction: direction + }); + }; + + $scope.zoomChart = function(zoom) { + $scope.$broadcast('zoomWorkflowChart', { + zoom: zoom + }); + }; + + $scope.resetChart = function() { + $scope.$broadcast('resetWorkflowChart'); + }; + + $scope.workflowZoomed = function(zoom) { + $scope.$broadcast('workflowZoomed', { + zoom: zoom + }); + }; init(); 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 f3ea67b990..6ecf4d4d0f 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 @@ -58,9 +58,13 @@
TOTAL JOBS + +
+ +
- +
{{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited) ? ((nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : "EDIT TEMPLATE") : "ADD A TEMPLATE"}}
diff --git a/awx/ui/client/src/workflow-results/workflow-results.controller.js b/awx/ui/client/src/workflow-results/workflow-results.controller.js index d80385141c..b694221d7f 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -57,6 +57,7 @@ export default ['workflowData', $scope.workflow_nodes = workflowNodes; $scope.workflowOptions = workflowDataOptions.actions.GET; $scope.labels = jobLabels; + $scope.showManualControls = false; // turn related api browser routes into tower routes getTowerLinks(); @@ -111,6 +112,32 @@ export default ['workflowData', workflowResultsService.relaunchJob($scope); }; + $scope.toggleManualControls = function() { + $scope.showManualControls = !$scope.showManualControls; + }; + + $scope.panChart = function(direction) { + $scope.$broadcast('panWorkflowChart', { + direction: direction + }); + }; + + $scope.zoomChart = function(zoom) { + $scope.$broadcast('zoomWorkflowChart', { + zoom: zoom + }); + }; + + $scope.resetChart = function() { + $scope.$broadcast('resetWorkflowChart'); + }; + + $scope.workflowZoomed = function(zoom) { + $scope.$broadcast('workflowZoomed', { + zoom: zoom + }); + }; + init(); $scope.$on(`ws-workflow_events-${$scope.workflow.id}`, function(e, data) { 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 1358ef5298..5923bf8132 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -217,26 +217,34 @@
-
KEY:
-
-
-
On Success
+
+
KEY:
+
+
+
On Success
+
+
+
+
On Fail
+
+
+
+
Always
+
+
+
P
+
Project Sync
+
+
+
I
+
Inventory Sync
+
-
-
-
On Fail
-
-
-
-
Always
-
-
-
P
-
Project Sync
-
-
-
I
-
Inventory Sync
+
+ +
+ +