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 @@ +