Workflow editor/details graph responsiveness

This commit is contained in:
Michael Abashian
2017-02-10 13:26:36 -05:00
parent fe29446298
commit 9c686f680d
5 changed files with 680 additions and 559 deletions

View File

@@ -46,7 +46,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
return function(params) { return function(params) {
var scope = params.scope, let scope = params.scope,
buttonSet = params.buttons, buttonSet = params.buttons,
width = params.width || 500, width = params.width || 500,
height = params.height || 600, height = params.height || 600,

View File

@@ -4,8 +4,8 @@
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
export default [ '$state','moment', export default [ '$state','moment', '$timeout', '$window',
function($state, moment) { function($state, moment, $timeout, $window) {
return { return {
scope: { scope: {
@@ -21,38 +21,91 @@ export default [ '$state','moment',
link: function(scope, element) { link: function(scope, element) {
let margin = {top: 20, right: 20, bottom: 20, left: 20}, let margin = {top: 20, right: 20, bottom: 20, left: 20},
width = 950,
height = 550,
i = 0, i = 0,
nodeW = 120, nodeW = 120,
nodeH = 60, nodeH = 60,
rootW = 60, rootW = 60,
rootH = 40; rootH = 40,
startNodeOffsetY = scope.mode === 'details' ? 17 : 10,
verticalSpaceBetweenNodes = 20,
windowHeight,
windowWidth,
tree,
line,
zoomObj,
baseSvg,
svgGroup;
let tree = d3.layout.tree() scope.dimensionsSet = false;
.size([height, width]);
let line = d3.svg.line() $timeout(function(){
let dimensions = calcAvailableScreenSpace();
windowHeight = dimensions.height;
windowWidth = dimensions.width;
$('.WorkflowMaker-chart').css("height", windowHeight);
$('.WorkflowMaker-chart').css("width", windowWidth);
scope.dimensionsSet = true;
init();
});
function init() {
tree = d3.layout.tree()
.nodeSize([nodeH + verticalSpaceBetweenNodes,nodeW])
.separation(function(a, b) {
// This should tighten up some of the other nodes so there's not so much wasted space
return a.parent === b.parent ? 1 : 1.25;
});
line = d3.svg.line()
.x(function(d){return d.x;}) .x(function(d){return d.x;})
.y(function(d){return d.y;}); .y(function(d){return d.y;});
let zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]); zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]);
let baseSvg = d3.select(element[0]).append("svg") baseSvg = d3.select(element[0]).append("svg")
.attr("width", width - margin.right - margin.left)
.attr("height", height - margin.top - margin.bottom)
.attr("class", "WorkflowChart-svg") .attr("class", "WorkflowChart-svg")
.call(zoomObj .call(zoomObj
.on("zoom", naturalZoom) .on("zoom", naturalZoom)
); );
let svgGroup = baseSvg.append("g") svgGroup = baseSvg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); .attr("transform", "translate(" + margin.left + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")");
}
function calcAvailableScreenSpace() {
let dimensions = {};
if(scope.mode !== 'details') {
// This is the workflow editor
dimensions.height = $('.WorkflowMaker-contentLeft').outerHeight() - $('.WorkflowLegend-maker').outerHeight();
dimensions.width = $('#workflow-modal-dialog').width() - $('.WorkflowMaker-contentRight').outerWidth();
console.log(dimensions);
}
else {
// This is the workflow details view
let panel = $('.WorkflowResults-rightSide').children('.Panel')[0];
let panelWidth = $(panel).width();
let panelHeight = $(panel).height();
let headerHeight = $('.StandardOut-panelHeader').outerHeight();
let legendHeight = $('.WorkflowLegend-details').outerHeight();
let proposedHeight = panelHeight - headerHeight - legendHeight - 40;
dimensions.height = proposedHeight > 200 ? proposedHeight : 200;
dimensions.width = panelWidth;
}
return dimensions;
}
function lineData(d){ function lineData(d){
let sourceX = d.source.isStartNode ? d.source.y + rootW : d.source.y + nodeW; let sourceX = d.source.isStartNode ? d.source.y + rootW : d.source.y + nodeW;
let sourceY = d.source.isStartNode ? d.source.x + 10 + rootH / 2 : d.source.x + nodeH / 2; let sourceY = d.source.isStartNode ? d.source.x + startNodeOffsetY + rootH / 2 : d.source.x + nodeH / 2;
let targetX = d.target.y; let targetX = d.target.y;
let targetY = d.target.x + nodeH / 2; let targetY = d.target.x + nodeH / 2;
@@ -108,7 +161,7 @@ export default [ '$state','moment',
let scale = d3.event.scale, let scale = d3.event.scale,
translation = d3.event.translate; translation = d3.event.translate;
translation = [translation[0] + (margin.left*scale), translation[1] + (margin.top*scale)]; translation = [translation[0] + (margin.left*scale), translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)];
svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")"); svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")");
@@ -122,12 +175,12 @@ export default [ '$state','moment',
let scale = zoom / 100, let scale = zoom / 100,
translation = zoomObj.translate(), translation = zoomObj.translate(),
origZoom = zoomObj.scale(), origZoom = zoomObj.scale(),
unscaledOffsetX = (translation[0] + ((width*origZoom) - width)/2)/origZoom, unscaledOffsetX = (translation[0] + ((windowWidth*origZoom) - windowWidth)/2)/origZoom,
unscaledOffsetY = (translation[1] + ((height*origZoom) - height)/2)/origZoom, unscaledOffsetY = (translation[1] + ((windowHeight*origZoom) - windowHeight)/2)/origZoom,
translateX = unscaledOffsetX*scale - ((scale*width)-width)/2, translateX = unscaledOffsetX*scale - ((scale*windowWidth)-windowWidth)/2,
translateY = unscaledOffsetY*scale - ((scale*height)-height)/2; translateY = unscaledOffsetY*scale - ((scale*windowHeight)-windowHeight)/2;
svgGroup.attr("transform", "translate(" + [translateX + (margin.left*scale), translateY + (margin.top*scale)] + ")scale(" + scale + ")"); svgGroup.attr("transform", "translate(" + [translateX + (margin.left*scale), translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")");
zoomObj.scale(scale); zoomObj.scale(scale);
zoomObj.translate([translateX, translateY]); zoomObj.translate([translateX, translateY]);
} }
@@ -145,18 +198,19 @@ export default [ '$state','moment',
translateX = translateCoords[0]; translateX = translateCoords[0];
translateY = direction === 'up' ? translateCoords[1] - distance : translateCoords[1] + distance; translateY = direction === 'up' ? translateCoords[1] - distance : translateCoords[1] + distance;
} }
svgGroup.attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")"); svgGroup.attr("transform", "translate(" + translateX + "," + (translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)) + ")scale(" + scale + ")");
zoomObj.translate([translateX, translateY]); zoomObj.translate([translateX, translateY]);
} }
function resetZoomAndPan() { function resetZoomAndPan() {
svgGroup.attr("transform", "translate(" + margin.left + "," + margin.top + ")scale(" + 1 + ")"); svgGroup.attr("transform", "translate(" + margin.left + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")");
// Update the zoomObj // Update the zoomObj
zoomObj.scale(1); zoomObj.scale(1);
zoomObj.translate([0,0]); zoomObj.translate([0,0]);
} }
function update() { function update() {
if(scope.dimensionsSet) {
// Declare the nodes // Declare the nodes
let nodes = tree.nodes(scope.treeData), let nodes = tree.nodes(scope.treeData),
links = tree.links(nodes); links = tree.links(nodes);
@@ -181,7 +235,7 @@ export default [ '$state','moment',
thisNode.append("rect") thisNode.append("rect")
.attr("width", rootW) .attr("width", rootW)
.attr("height", rootH) .attr("height", rootH)
.attr("y", 10) .attr("y", startNodeOffsetY)
.attr("rx", 5) .attr("rx", 5)
.attr("ry", 5) .attr("ry", 5)
.attr("fill", "#337ab7") .attr("fill", "#337ab7")
@@ -191,6 +245,7 @@ export default [ '$state','moment',
thisNode.append("rect") thisNode.append("rect")
.attr("width", rootW) .attr("width", rootW)
.attr("height", rootH) .attr("height", rootH)
//.attr("y", (windowHeight-margin.top-margin.bottom)/2 - rootH)
.attr("y", 10) .attr("y", 10)
.attr("rx", 5) .attr("rx", 5)
.attr("ry", 5) .attr("ry", 5)
@@ -199,6 +254,7 @@ export default [ '$state','moment',
.call(add_node); .call(add_node);
thisNode.append("text") thisNode.append("text")
.attr("x", 13) .attr("x", 13)
//.attr("y", (windowHeight-margin.top-margin.bottom)/2 - rootH + rootH/2)
.attr("y", 30) .attr("y", 30)
.attr("dy", ".35em") .attr("dy", ".35em")
.attr("class", "WorkflowChart-startText") .attr("class", "WorkflowChart-startText")
@@ -491,7 +547,7 @@ export default [ '$state','moment',
return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2; return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2;
}) })
.attr("cy", function(d) { .attr("cy", function(d) {
return (d.source.isStartNode) ? ((d.target.x + 10 + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2; return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2;
}) })
.attr("r", 10) .attr("r", 10)
.attr("class", "addCircle linkCircle") .attr("class", "addCircle linkCircle")
@@ -516,7 +572,7 @@ export default [ '$state','moment',
.attr("transform", function(d) { .attr("transform", function(d) {
let translate; let translate;
if(d.source.isStartNode) { if(d.source.isStartNode) {
translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + 10 + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")"; translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")";
} }
else { else {
translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")"; translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")";
@@ -587,7 +643,7 @@ export default [ '$state','moment',
return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2; return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2;
}) })
.attr("cy", function(d) { .attr("cy", function(d) {
return (d.source.isStartNode) ? ((d.target.x + 10 + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2; return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2;
}); });
t.selectAll(".linkCross") t.selectAll(".linkCross")
@@ -595,7 +651,7 @@ export default [ '$state','moment',
.attr("transform", function(d) { .attr("transform", function(d) {
let translate; let translate;
if(d.source.isStartNode) { if(d.source.isStartNode) {
translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + 10 + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")"; translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")";
} }
else { else {
translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")"; translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")";
@@ -710,7 +766,16 @@ export default [ '$state','moment',
t.selectAll(".WorkflowChart-elapsed") t.selectAll(".WorkflowChart-elapsed")
.style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; });
}
else if(!scope.watchDimensionsSet){
scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){
if(scope.dimensionsSet) {
scope.watchDimensionsSet();
scope.watchDimensionsSet = null;
update();
}
});
}
} }
function add_node() { function add_node() {
@@ -809,6 +874,29 @@ export default [ '$state','moment',
} }
}); });
function onResize(){
let dimensions = calcAvailableScreenSpace();
$('.WorkflowMaker-chart').css("width", dimensions.width);
$('.WorkflowMaker-chart').css("height", dimensions.height);
}
function cleanUpResize() {
angular.element($window).off('resize', onResize);
}
if(scope.mode === 'details') {
angular.element($window).on('resize', onResize);
scope.$on('$destroy', cleanUpResize);
}
else {
scope.$on('workflowMakerModalResized', function(){
let dimensions = calcAvailableScreenSpace();
$('.WorkflowMaker-chart').css("width", dimensions.width);
$('.WorkflowMaker-chart').css("height", dimensions.height);
});
}
} }
}; };
}]; }];

View File

@@ -888,6 +888,10 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr
}); });
}; };
$scope.$on('WorkflowDialogReady', function(){
$scope.modalOpen = true;
});
init(); init();
} }

View File

@@ -6,8 +6,8 @@
import workflowMakerController from './workflow-maker.controller'; import workflowMakerController from './workflow-maker.controller';
export default ['templateUrl', 'CreateDialog', 'Wait', '$state', export default ['templateUrl', 'CreateDialog', 'Wait', '$state', '$window',
function(templateUrl, CreateDialog, Wait, $state) { function(templateUrl, CreateDialog, Wait, $state, $window) {
return { return {
scope: { scope: {
workflowJobTemplateObj: '=', workflowJobTemplateObj: '=',
@@ -17,11 +17,17 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state',
templateUrl: templateUrl('templates/workflows/workflow-maker/workflow-maker'), templateUrl: templateUrl('templates/workflows/workflow-maker/workflow-maker'),
controller: workflowMakerController, controller: workflowMakerController,
link: function(scope) { link: function(scope) {
let availableHeight = $(window).height(),
availableWidth = $(window).width(),
minimumWidth = 1300,
minimumHeight = 550;console.log(availableHeight, availableWidth);
CreateDialog({ CreateDialog({
id: 'workflow-modal-dialog', id: 'workflow-modal-dialog',
scope: scope, scope: scope,
width: 1400, width: availableWidth > minimumWidth ? availableWidth : minimumWidth,
height: 720, height: availableHeight > minimumHeight ? availableHeight : minimumHeight,
draggable: false, draggable: false,
dialogClass: 'SurveyMaker-dialog', dialogClass: 'SurveyMaker-dialog',
position: ['center', 20], position: ['center', 20],
@@ -34,6 +40,10 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state',
// Let the modal height be variable based on the content // Let the modal height be variable based on the content
// and set a uniform padding // and set a uniform padding
$('#workflow-modal-dialog').css({ 'padding': '20px' }); $('#workflow-modal-dialog').css({ 'padding': '20px' });
$('#workflow-modal-dialog').parent('.ui-dialog').height(availableHeight > minimumHeight ? availableHeight : minimumHeight);
$('#workflow-modal-dialog').parent('.ui-dialog').width(availableWidth > minimumWidth ? availableWidth : minimumWidth);
$('#workflow-modal-dialog').outerHeight(availableHeight > minimumHeight ? availableHeight : minimumHeight);
$('#workflow-modal-dialog').outerWidth(availableWidth > minimumWidth ? availableWidth : minimumWidth);
}, },
_allowInteraction: function(e) { _allowInteraction: function(e) {
@@ -55,6 +65,25 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state',
$state.go('^'); $state.go('^');
}; };
function onResize(){
availableHeight = $(window).height();
availableWidth = $(window).width();
console.log(availableHeight, availableWidth);
$('#workflow-modal-dialog').parent('.ui-dialog').height(availableHeight > minimumHeight ? availableHeight : minimumHeight);
$('#workflow-modal-dialog').parent('.ui-dialog').width(availableWidth > minimumWidth ? availableWidth : minimumWidth);
$('#workflow-modal-dialog').outerHeight(availableHeight > minimumHeight ? availableHeight : minimumHeight);
$('#workflow-modal-dialog').outerWidth(availableWidth > minimumWidth ? availableWidth : minimumWidth);
scope.$broadcast('workflowMakerModalResized');
}
function cleanUpResize() {
angular.element($window).off('resize', onResize);
}
angular.element($window).on('resize', onResize);
scope.$on('$destroy', cleanUpResize);
} }
}; };
} }

View File

@@ -64,7 +64,7 @@
</div> </div>
</div> </div>
</div> </div>
<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> <workflow-chart ng-if="modalOpen" 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>
<div class="WorkflowMaker-contentRight"> <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> <div class="WorkflowMaker-formTitle">{{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited) ? ((nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : "EDIT TEMPLATE") : "ADD A TEMPLATE"}}</div>