1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 08:21:15 +03:00

Merge branch 'mabashian-4254-manual-pan-zoom' into release_3.1.0

# Conflicts:
#	awx/ui/client/src/workflow-results/workflow-results.controller.js
This commit is contained in:
Michael Abashian 2016-12-08 16:14:16 -05:00
commit e8436699a9
11 changed files with 391 additions and 63 deletions

View File

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

View File

@ -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);
});
}
};
}];

View File

@ -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);

View File

@ -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;
}

View File

@ -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();
}
};
}
];

View File

@ -0,0 +1,19 @@
<div class="WorkflowControls-Zoom">
<div class="WorkflowControls-Zoom--button WorkflowControls-Zoom--minus" ng-click="zoomOut()">
<i class="fa fa-minus" aria-hidden="true"></i>
</div>
<div class="WorkflowControls-zoomSlider">
<div class="WorkflowControls-zoomPercentage">{{zoom}}%</div>
<div id="slider"></div>
</div>
<div class="WorkflowControls-Zoom--button WorkflowControls-Zoom--plus" ng-click="zoomIn()">
<i class="fa fa-plus" aria-hidden="true"></i>
</div>
</div>
<div class="WorkflowControls-Pan">
<i class="fa fa-caret-up WorkflowControls-Pan--button WorkflowControls-Pan--up" ng-click="pan('up')"></i>
<i class="fa fa-caret-down WorkflowControls-Pan--button WorkflowControls-Pan--down" ng-click="pan('down')"></i>
<i class="fa fa-caret-left WorkflowControls-Pan--button WorkflowControls-Pan--left" ng-click="pan('left')"></i>
<i class="fa fa-caret-right WorkflowControls-Pan--button WorkflowControls-Pan--right" ng-click="pan('right')"></i>
<i class="fa fa-home WorkflowControls-Pan--button WorkflowControls-Pan--home" ng-click="reset()"></i>
</div>

View File

@ -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;
}

View File

@ -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();

View File

@ -58,9 +58,13 @@
<div class="WorkflowLegend-maker--right">
<span class="WorkflowMaker-totalJobs">TOTAL JOBS</span>
<span class="badge List-titleBadge" ng-bind="treeData.data.totalNodes"></span>
<i ng-class="{'WorkflowMaker-manualControlsIcon--active': showManualControls}" class="fa fa-cog WorkflowMaker-manualControlsIcon" aria-hidden="true" alt="Controls" ng-click="toggleManualControls()"></i>
<div ng-show="showManualControls" class="WorkflowMaker-manualControls noselect">
<workflow-controls class="WorkflowControls" pan-chart="panChart(direction)" zoom-chart="zoomChart(zoom)" reset-chart="resetChart()"></workflow-controls>
</div>
</div>
</div>
<workflow-chart tree-data="treeData.data" add-node="startAddNode(parent, betweenTwoNodes)" edit-node="startEditNode(nodeToEdit)" delete-node="startDeleteNode(nodeToDelete)" can-add-workflow-job-template="canAddWorkflowJobTemplate" mode="edit" class="WorkflowMaker-chart"></workflow-chart>
<workflow-chart tree-data="treeData.data" add-node="startAddNode(parent, betweenTwoNodes)" edit-node="startEditNode(nodeToEdit)" delete-node="startDeleteNode(nodeToDelete)" workflow-zoomed="workflowZoomed(zoom)" can-add-workflow-job-template="canAddWorkflowJobTemplate" mode="edit" class="WorkflowMaker-chart"></workflow-chart>
</div>
<div class="WorkflowMaker-contentRight">
<div class="WorkflowMaker-formTitle">{{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited) ? ((nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : "EDIT TEMPLATE") : "ADD A TEMPLATE"}}</div>

View File

@ -62,6 +62,7 @@ export default ['workflowData',
$scope.workflowOptions = workflowDataOptions.actions.GET;
$scope.labels = jobLabels;
$scope.count = count.val;
$scope.showManualControls = false;
// turn related api browser routes into tower routes
getTowerLinks();
@ -116,6 +117,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();
// Processing of job-status messages from the websocket

View File

@ -217,26 +217,34 @@
</div>
<workflow-status-bar></workflow-status-bar>
<div class="WorkflowLegend-details">
<div class="WorkflowLegend-legendItem">KEY:</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-onSuccessLegend"></div>
<div>On Success</div>
<div class="WorkflowLegend-details--left">
<div class="WorkflowLegend-legendItem">KEY:</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-onSuccessLegend"></div>
<div>On Success</div>
</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-onFailLegend"></div>
<div>On Fail</div>
</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-alwaysLegend"></div>
<div>Always</div>
</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-letterCircle">P</div>
<div>Project Sync</div>
</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-letterCircle">I</div>
<div>Inventory Sync</div>
</div>
</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-onFailLegend"></div>
<div>On Fail</div>
</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-alwaysLegend"></div>
<div>Always</div>
</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-letterCircle">P</div>
<div>Project Sync</div>
</div>
<div class="WorkflowLegend-legendItem">
<div class="WorkflowLegend-letterCircle">I</div>
<div>Inventory Sync</div>
<div class="WorkflowLegend-details--right">
<i ng-class="{'WorkflowMaker-manualControlsIcon--active': showManualControls}" class="fa fa-cog WorkflowMaker-manualControlsIcon" aria-hidden="true" alt="Controls" ng-click="toggleManualControls()"></i>
<div ng-show="showManualControls" class="WorkflowLegend-manualControls noselect">
<workflow-controls class="WorkflowControls" pan-chart="panChart(direction)" zoom-chart="zoomChart(zoom)" reset-chart="resetChart()"></workflow-controls>
</div>
</div>
</div>
<workflow-chart tree-data="treeData.data" can-add-workflow-job-template="canAddWorkflowJobTemplate" mode="details" class="WorkflowMaker-chart"></workflow-chart>