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 32cec9f..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. @@ -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..216534c 100644 --- a/scripts/common/services/blueprint-topology.js +++ b/scripts/common/services/blueprint-topology.js @@ -8,13 +8,17 @@ 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) { $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); @@ -39,7 +43,7 @@ angular.module('waldo.Blueprint') return 160; }, height: function() { - return 160 + return 160; }, margin: { top: 10, @@ -80,8 +84,13 @@ angular.module('waldo.Blueprint') } }; + var dragConnectorLine = null; + var state = { + linking: false + }; + var zoom = d3.behavior.zoom() - .scaleExtent([.6, 7]) + .scaleExtent([0.2, 3]) .on("zoom", zoomed); var drag = d3.behavior.drag() @@ -90,11 +99,39 @@ 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) { + 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") @@ -104,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()); @@ -151,10 +211,27 @@ 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) { + 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 +293,8 @@ angular.module('waldo.Blueprint') relation: null }; - scope.select(data); + toggleSelect(d3.select(this), data); + d3.event.stopPropagation(); }); component.append('rect') @@ -245,30 +323,6 @@ angular.module('waldo.Blueprint') }) .attr('class', 'component-icon'); - var linker = component.append('g') - .attr('class', 'relation-linker'); - - linker.append('circle') - .attr('r', 12) - .attr('fill', '#f6f6f6') - .attr('cx', function(d, index) { - return sizes.service.margin.left + (sizes.component.width() * (index + 1)) - 18; - }) - .attr('cy', function(d, index) { - return sizes.component.height() - 7; - }) - .attr('class', 'relation-link-container'); - - linker.append('text') - .html('') - .attr('x', function(d, index) { - return sizes.service.margin.left + (sizes.component.width() * (index + 1)) - 24; - }) - .attr('y', function(d, index) { - return sizes.component.height() - 2; - }) - .attr('class', 'fa-link relation-linker-icon'); - // This adds a component label. var label = component.append('text') .attr('class', 'component-title'); @@ -301,6 +355,129 @@ angular.module('waldo.Blueprint') 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) { + 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(); + }) + .call(linkerDrag); + + linker.append('circle') + .attr('r', 12) + .attr('fill', '#f6f6f6') + .attr('cx', function(d, index) { + return sizes.service.margin.left + (sizes.component.width() * (index + 1)) - 18; + }) + .attr('cy', function(d, index) { + return sizes.component.height() - 7; + }) + .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; + }) + .attr('y', function(d, index) { + return sizes.component.height() - 2; + }) + .attr('class', 'fa-link relation-linker-icon'); + + // 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); + }); + + // 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); + } + }); + + } + + function linkstarted(d) { + state.linking = true; + dragConnectorLine = svg.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); + } + + 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() { @@ -330,6 +507,7 @@ angular.module('waldo.Blueprint') d3.event.sourceEvent.stopPropagation(); d3.select(this).classed("dragging", false); save(); + Drag.reset(); } function save() { @@ -350,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/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/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/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 2467081..969ca38 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; } @@ -252,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; @@ -275,3 +302,9 @@ g.relation-linker:hover circle.relation-link-container { .cm-s-lesser-dark .CodeMirror-gutters { border-right: 1px solid #555; } + +span.CodeMirror-foldmarker { + color: rgb(188, 188, 194); + text-shadow: none; +} +