From 383f9aa97a4496f7c92df06451e5c681c1e10168 Mon Sep 17 00:00:00 2001 From: Ziad Sawalha Date: Tue, 2 Dec 2014 08:45:31 -0600 Subject: [PATCH 1/4] feat(topology): select/deselect components --- .../applications/applications-configure.js | 12 +++- .../applications-configure.tpl.html | 2 +- scripts/common/services/blueprint-topology.js | 60 ++++++++++++++++++- styles/application-configure.css | 7 +++ 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/scripts/app/applications/applications-configure.js b/scripts/app/applications/applications-configure.js index 32cec9f..b427473 100644 --- a/scripts/app/applications/applications-configure.js +++ b/scripts/app/applications/applications-configure.js @@ -59,7 +59,17 @@ angular.module('applications-configure') }); $scope.$on('topology:select', function(event, selection) { - $scope.selection.isVisible = true; $scope.selection.data = selection; + if (selection) { + $scope.selection.isVisible = true; + } else { + $scope.selection.isVisible = false; + $scope.$apply(); // No idea why, but this is needed to hide the properties drawer + } + }); + + $scope.$on('topology:deselect', function(event, selection) { + $scope.selection.data = selection; + $scope.selection.isVisible = false; }); }); diff --git a/scripts/app/applications/applications-configure.tpl.html b/scripts/app/applications/applications-configure.tpl.html index 1d5e702..632b0b7 100644 --- a/scripts/app/applications/applications-configure.tpl.html +++ b/scripts/app/applications/applications-configure.tpl.html @@ -36,7 +36,7 @@

{{type}}

- Header Title + {{ selection.data.service }}\{{ selection.data.component }}
diff --git a/scripts/common/services/blueprint-topology.js b/scripts/common/services/blueprint-topology.js index d052e3f..aca97cb 100644 --- a/scripts/common/services/blueprint-topology.js +++ b/scripts/common/services/blueprint-topology.js @@ -15,6 +15,10 @@ angular.module('waldo.Blueprint') $scope.$emit('topology:select', selection); }; + $scope.deselect = function(selection) { + $scope.$emit('topology:deselect', selection); + }; + $scope.$on('blueprint:update', function(event, data) { $timeout(function() { $scope.blueprint = angular.copy(data); @@ -92,9 +96,28 @@ angular.module('waldo.Blueprint') var svg = d3.select(element[0]); + function toggleSelect(el, data) { + if (el.classed('selected')) { + el.classed('selected', false); + scope.deselect(data); + } else { + svg.selectAll('.selected').classed('selected', false); + el.classed('selected', true); + scope.select(data); + } + } + var zoomer = svg.append("g") .attr("transform", "translate(0,0)") - .call(zoom); + .call(zoom) + .attr('class', 'zoomer') + .on('click', function(d) { + if(d3.event.defaultPrevented) { + return; + } + console.log("CANVAS"); + toggleSelect(d3.select(this), null); + }); var rect = zoomer.append("rect") .style("fill", "none") @@ -155,6 +178,21 @@ angular.module('waldo.Blueprint') d.y = d.annotations['gui-y'] || mouse[1]; return "translate(" + d.x + "," + d.y + ")" + }) + .on('click', function(d) { + if(d3.event.defaultPrevented) { + return; + } + console.log('SERVICE', d._id); + + var data = { + service: d._id, + component: null, + relation: null + }; + + toggleSelect(d3.select(this), data); + d3.event.stopPropagation(); }); // This defines service drag events. @@ -216,7 +254,8 @@ angular.module('waldo.Blueprint') relation: null }; - scope.select(data); + toggleSelect(d3.select(this), data); + d3.event.stopPropagation(); }); component.append('rect') @@ -246,7 +285,22 @@ angular.module('waldo.Blueprint') .attr('class', 'component-icon'); var linker = component.append('g') - .attr('class', 'relation-linker'); + .attr('class', 'relation-linker') + .on('click', function(d) { + if(d3.event.defaultPrevented) { + return; + } + console.log('LINKER', d); + + var data = { + service: d3.select(this.parentNode).datum()._id, + component: d, + relation: null + }; + + toggleSelect(d3.select(this), data); + d3.event.stopPropagation(); + }); linker.append('circle') .attr('r', 12) diff --git a/styles/application-configure.css b/styles/application-configure.css index 2467081..8af5b1c 100644 --- a/styles/application-configure.css +++ b/styles/application-configure.css @@ -240,6 +240,13 @@ stroke-width: 1; } +g.selected > .service-container, +g.selected > .component-container { + stroke-width: 4px; + stroke: #aaa; + stroke-dasharray: 8; +} + g.relation-linker:hover { cursor: pointer; } From bcf5507f60bb98168a0e80a9cf70d3994cb50adc Mon Sep 17 00:00:00 2001 From: Ziad Sawalha Date: Tue, 2 Dec 2014 20:30:31 -0600 Subject: [PATCH 2/4] feat(linker): UI for connecting components This just implements the behavior with some basic UI and calls back to Blueprint to check on components. Relations are not yet created. --- scripts/common/services/blueprint-topology.js | 178 +++++++++++++++--- scripts/common/services/blueprint.js | 15 ++ scripts/common/services/drag.js | 11 +- styles/application-configure.css | 22 ++- 4 files changed, 196 insertions(+), 30 deletions(-) diff --git a/scripts/common/services/blueprint-topology.js b/scripts/common/services/blueprint-topology.js index aca97cb..4a9f71e 100644 --- a/scripts/common/services/blueprint-topology.js +++ b/scripts/common/services/blueprint-topology.js @@ -84,6 +84,11 @@ angular.module('waldo.Blueprint') } }; + var dragConnectorLine = null; + var state = { + linking: false + }; + var zoom = d3.behavior.zoom() .scaleExtent([.6, 7]) .on("zoom", zoomed); @@ -94,6 +99,15 @@ angular.module('waldo.Blueprint') .on("drag", dragged) .on("dragend", dragended); + var linkerDrag = d3.behavior.drag() + .origin(function(d) { + var t = d3.select(this); + return {x: t.attr("x"), y: t.attr("y")}; + }) + .on("dragstart", linkstarted) + .on("drag", linkdragged) + .on("dragend", linkended); + var svg = d3.select(element[0]); function toggleSelect(el, data) { @@ -127,6 +141,29 @@ angular.module('waldo.Blueprint') .call(zoom) .attr('class', 'container'); + d3.selection.prototype.position = function() { + var el = this.node(); + var elPos = el.getBoundingClientRect(); + var vpPos = getVpPos(el); + + function getVpPos(el) { + if(el.parentElement.tagName === 'svg') { + return el.parentElement.getBoundingClientRect(); + } + return getVpPos(el.parentElement); + } + + return { + top: elPos.top - vpPos.top, + left: elPos.left - vpPos.left, + width: elPos.width, + bottom: elPos.bottom - vpPos.top, + height: elPos.height, + right: elPos.right - vpPos.left + }; + + }; + // This listens for mouse events on the entire svg element. svg.on("dragover", function() { mouse = d3.mouse(svg.node()); @@ -284,7 +321,43 @@ angular.module('waldo.Blueprint') }) .attr('class', 'component-icon'); + // This adds a component label. + var label = component.append('text') + .attr('class', 'component-title'); + + label.append('title') + .text(function(d) { + return d; + }); + + label.append('tspan') + .attr('text-anchor', 'middle') + .attr('x', function(d, index) { + var x = sizes.service.margin.left + (sizes.component.width() / 2); + + if(index > 0) { + x = x + (sizes.component.width() * index); + } + + return x; + }) + .attr('y', function(d) { + return sizes.service.margin.top + 25; + }) + .text(function(d) { + var label = d; + + if(d.length > 12) { + label = label.slice(0,9) + '...'; + } + + return label; + }); + + + // This draws the linker thingy var linker = component.append('g') + .style("pointer-events", "all") .attr('class', 'relation-linker') .on('click', function(d) { if(d3.event.defaultPrevented) { @@ -300,7 +373,8 @@ angular.module('waldo.Blueprint') toggleSelect(d3.select(this), data); d3.event.stopPropagation(); - }); + }) + .call(linkerDrag); linker.append('circle') .attr('r', 12) @@ -314,6 +388,7 @@ angular.module('waldo.Blueprint') .attr('class', 'relation-link-container'); linker.append('text') + .style("pointer-events", "none") .html('') .attr('x', function(d, index) { return sizes.service.margin.left + (sizes.component.width() * (index + 1)) - 24; @@ -323,38 +398,84 @@ angular.module('waldo.Blueprint') }) .attr('class', 'fa-link relation-linker-icon'); - // This adds a component label. - var label = component.append('text') - .attr('class', 'component-title'); - - label.append('title') - .text(function(d) { - return d; - }); - - label.append('tspan') - .attr('text-anchor', 'middle') - .attr('x', function(d, index) { - var x = sizes.service.margin.left + (sizes.component.width() / 2); + // This defines linker drag events. + linker.on("dragenter", function(d) { + console.log("enter"); + d3.select(this).classed('target', true); + Drag.target.set({componentId: d, serviceId: d3.select(this.parentNode.parentNode).datum()._id}); + }).on("dragover", function(d) { + console.log("OVER"); + }).on("dragleave", function(d) { + Drag.target.set(null); + d3.select(this).classed('target unsuitable', false); + console.log("LEAVE"); + }).on("drop", function() { + console.log("DROP"); + d3.select(this).classed('target unsuitable', false); + }); - if(index > 0) { - x = x + (sizes.component.width() * index); + // TODO: This is a backup for drag events not firing + linker.on("mouseover", function(d) { + if (state.linking) { + var source = Drag.source.get(); + var target = {componentId: d, serviceId: d3.select(this.parentNode.parentNode).datum()._id}; + if (source.serviceId === target.serviceId && source.componentId === target.componentId) { + console.log("SELF"); + return; + } else { + console.log("OTHER", source, target); + } + d3.select(this).classed('target', true); + if (Blueprint.canConnect(source, target)) { + Drag.target.set(target); + d3.select(this).classed('unsuitable', false); + } else { + d3.select(this).classed('unsuitable', true); } + } + }).on("mouseout", function(d) { + console.log("OUT", d); + if (state.linking) { + d3.select(this).classed('target unsuitable', false); + Drag.target.set(null); + } + }).on("mouseup", function(d) { + console.log("UP", d); + if (state.linking) { + d3.select(this).classed('target unsuitable', false); + } + }); - return x; - }) - .attr('y', function(d) { - return sizes.service.margin.top + 25; - }) - .text(function(d) { - var label = d; + } - if(d.length > 12) { - label = label.slice(0,9) + '...'; - } + function linkstarted(d) { + state.linking = true; + dragConnectorLine = container.append('path') + .style("pointer-events", "none") + .attr('class', 'linker dragline') + .attr('d', 'M0,0L0,0'); + Drag.source.set({componentId: d, serviceId: d3.select(this.parentNode.parentNode).datum()._id}); + d3.event.sourceEvent.stopPropagation(); + d3.select(this).classed("dragging dragged", true); + } - return label; - }); + function linkdragged(d) { + var elem = d3.select(this); + var box = elem.position(); // TODO: account for zoom + var mouse = d3.mouse(zoomer[0][0]); + dragConnectorLine.attr('d', 'M' + (box.left + box.width/2) + ',' + (box.top + box.height/2) + 'L' + mouse[0] + ',' + mouse[1]); + } + + function linkended(d) { + state.linking = false; + dragConnectorLine.remove(); + d3.event.sourceEvent.stopPropagation(); + d3.select(this).classed("dragging", false); + var target = Drag.target.get(); + if (target) { + console.log("DROP", target); + } + Drag.reset(); } function resize() { @@ -384,6 +505,7 @@ angular.module('waldo.Blueprint') d3.event.sourceEvent.stopPropagation(); d3.select(this).classed("dragging", false); save(); + Drag.reset(); } function save() { diff --git a/scripts/common/services/blueprint.js b/scripts/common/services/blueprint.js index bdc9f4f..a35da17 100644 --- a/scripts/common/services/blueprint.js +++ b/scripts/common/services/blueprint.js @@ -40,6 +40,21 @@ angular.module('waldo.Blueprint') this.broadcast(); }, + canConnect: function(from, target, protocol, optionalTag) { + var fromServiceId = from.serviceId, + fromComponentId = from.componentId; + var targetServiceId = target.serviceId, + targetComponentId = target.componentId; + if (!(fromServiceId in this.data.services)) { + return false; + } + if (!(targetServiceId in this.data.services)) { + return false; + } + var fromService = this.data.services[fromServiceId]; + var targetService = this.data.services[targetServiceId]; + return true; + }, connect: function(fromServiceId, toServiceId, protocol, optionalTag) { var fromService = this.data.services[fromServiceId]; if (!angular.isArray(fromService.relations)) { diff --git a/scripts/common/services/drag.js b/scripts/common/services/drag.js index d82f8fe..0337937 100644 --- a/scripts/common/services/drag.js +++ b/scripts/common/services/drag.js @@ -1,6 +1,6 @@ angular.module('waldo.Drag', []); angular.module('waldo.Drag') - .factory('Drag', function() { + .factory('Drag', function() { return { reset: function() { this.target.data = null; @@ -23,6 +23,15 @@ angular.module('waldo.Drag') set: function(data) { this.data = data; } + }, + source: { + data: null, + get: function() { + return this.data; + }, + set: function(data) { + this.data = data; + } } }; }); \ No newline at end of file diff --git a/styles/application-configure.css b/styles/application-configure.css index 8af5b1c..dcdbef8 100644 --- a/styles/application-configure.css +++ b/styles/application-configure.css @@ -259,12 +259,32 @@ g.relation-linker text { fill: #999; } g.relation-linker:hover text { - fill: #1abc9c; + fill: #2980b9; } g.relation-linker:hover circle.relation-link-container { + stroke: #2980b9; +} + +g.relation-linker.dragging circle { + stroke: #1abc9c; +} + +g.relation-linker.target circle { stroke: #1abc9c; } +g.relation-linker.target.unsuitable circle { + stroke: #c0392b; +} + + +path.linker { + fill: none; + stroke: #95a5a6; + stroke-width: 2px; + cursor: default; +} + /* Code Mirror Overrides - blueprint-codemirror.css */ .CodeMirror { border: 1px solid #eee; From 9c505c4ad78a0e206d2d9a8d43abfea255c94e4a Mon Sep 17 00:00:00 2001 From: Ziad Sawalha Date: Tue, 2 Dec 2014 20:36:36 -0600 Subject: [PATCH 3/4] fix(scaling): fix connector when scaled --- scripts/common/services/blueprint-topology.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/common/services/blueprint-topology.js b/scripts/common/services/blueprint-topology.js index 4a9f71e..efd75fd 100644 --- a/scripts/common/services/blueprint-topology.js +++ b/scripts/common/services/blueprint-topology.js @@ -450,7 +450,7 @@ angular.module('waldo.Blueprint') function linkstarted(d) { state.linking = true; - dragConnectorLine = container.append('path') + dragConnectorLine = svg.append('path') .style("pointer-events", "none") .attr('class', 'linker dragline') .attr('d', 'M0,0L0,0'); From 6475e4271ee8667391293edd47f7def4f9b5dfea Mon Sep 17 00:00:00 2001 From: Ziad Sawalha Date: Tue, 2 Dec 2014 20:40:16 -0600 Subject: [PATCH 4/4] fix(style): minor lint, cleanup, and bug fixes --- index.html | 6 +++--- .../app/applications/applications-configure.js | 10 +++++----- scripts/common/services/blueprint-topology.js | 18 ++++++++++-------- scripts/common/services/deployment-editor.js | 2 +- styles/application-configure.css | 6 ++++++ 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/index.html b/index.html index 57ead3b..48fad8b 100644 --- a/index.html +++ b/index.html @@ -12,9 +12,6 @@ - - - @@ -55,6 +52,9 @@
+ + + diff --git a/scripts/app/applications/applications-configure.js b/scripts/app/applications/applications-configure.js index b427473..2afca15 100644 --- a/scripts/app/applications/applications-configure.js +++ b/scripts/app/applications/applications-configure.js @@ -27,8 +27,8 @@ angular.module('applications-configure') // This could toggle an extra sidebar to reveal details about a service. $scope.selection = { - 'data': {}, - 'isVisible': false, + data: {}, + isVisible: false, save: function(component) { Blueprint.update($scope.deployment.blueprint); }, @@ -39,9 +39,9 @@ angular.module('applications-configure') // This is the catalog model for the sidebar. $scope.catalog = { - 'isVisible': false, - 'data': Catalog.get(), - 'components': Catalog.getComponents() + isVisible: false, + data: Catalog.get(), + components: Catalog.getComponents() }; // This is the codemirror model for the sidebar. diff --git a/scripts/common/services/blueprint-topology.js b/scripts/common/services/blueprint-topology.js index efd75fd..216534c 100644 --- a/scripts/common/services/blueprint-topology.js +++ b/scripts/common/services/blueprint-topology.js @@ -8,7 +8,7 @@ angular.module('waldo.Blueprint') $scope.catalog = Catalog.get(); $scope.getTattoo = function(componentId) { - return (((Catalog.getComponent(componentId) || {})['meta-data'] || {})['display-hints'] || {})['tattoo'] || ''; + return (((Catalog.getComponent(componentId) || {})['meta-data'] || {})['display-hints'] || {}).tattoo || ''; }; $scope.select = function(selection) { @@ -43,7 +43,7 @@ angular.module('waldo.Blueprint') return 160; }, height: function() { - return 160 + return 160; }, margin: { top: 10, @@ -90,7 +90,7 @@ angular.module('waldo.Blueprint') }; var zoom = d3.behavior.zoom() - .scaleExtent([.6, 7]) + .scaleExtent([0.2, 3]) .on("zoom", zoomed); var drag = d3.behavior.drag() @@ -211,10 +211,12 @@ angular.module('waldo.Blueprint') return d._id + '-service'; }) .attr("transform", function(d) { - d.x = d.annotations['gui-x'] || mouse[0]; - d.y = d.annotations['gui-y'] || mouse[1]; + var annotations = d.annotations || {}; + var safeMouse = mouse || [100, 100]; + d.x = annotations['gui-x'] || safeMouse[0]; + d.y = annotations['gui-y'] || safeMouse[1]; - return "translate(" + d.x + "," + d.y + ")" + return "translate(" + d.x + "," + d.y + ")"; }) .on('click', function(d) { if(d3.event.defaultPrevented) { @@ -526,8 +528,8 @@ angular.module('waldo.Blueprint') // This adds annotations property. _service.annotations = { - 'gui-x': Number(d.x.toFixed(3)), - 'gui-y': Number(d.y.toFixed(3)) + 'gui-x': Number((d.x || 100).toFixed(3)), + 'gui-y': Number((d.y || 100).toFixed(3)) }; // This extends any current diff --git a/scripts/common/services/deployment-editor.js b/scripts/common/services/deployment-editor.js index 918db7e..947aa1b 100644 --- a/scripts/common/services/deployment-editor.js +++ b/scripts/common/services/deployment-editor.js @@ -76,7 +76,7 @@ angular.module('waldo.Deployment') $scope.$on('deployment:update', function(event, data) { if ($scope.dirty) { - $scope.$emit('editor:outOfSync'); + $scope.$emit('editor:out_of_sync'); console.log('Editor out of sync with topology. TODO: handle better'); return; } diff --git a/styles/application-configure.css b/styles/application-configure.css index dcdbef8..969ca38 100644 --- a/styles/application-configure.css +++ b/styles/application-configure.css @@ -302,3 +302,9 @@ path.linker { .cm-s-lesser-dark .CodeMirror-gutters { border-right: 1px solid #555; } + +span.CodeMirror-foldmarker { + color: rgb(188, 188, 194); + text-shadow: none; +} +