diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js index 2cad3ef81..f0ad92d1a 100644 --- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js +++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js @@ -369,9 +369,13 @@ export function catalogSelectorSearchFilter() { let fieldNum = 0; return found && FIELDS_TO_SEARCH.reduce((match, field) => { - if (match) return true; + if (match) { + return true; + } fieldNum++; - if (!item.hasOwnProperty(field) || !item[field]) return false; + if (!item.hasOwnProperty(field) || !item[field]) { + return false; + } let text = item[field]; if (!text.toLowerCase) { text = JSON.stringify(text).toLowerCase(); @@ -379,7 +383,9 @@ export function catalogSelectorSearchFilter() { text = text.toLowerCase(); } let index = text.indexOf(part); - if (index == -1) return false; + if (index == -1) { + return false; + } // found, set relevance -- uses an ad hoc heuristic preferring first fields and short text length, // earlier occurrences and earlier words weighted more highly (smaller number is better) let score = fieldNum * (2 / (1 + wordNum)) * Math.log(1 + text.length * index); diff --git a/ui-modules/blueprint-composer/app/components/designer/designer.directive.js b/ui-modules/blueprint-composer/app/components/designer/designer.directive.js index cbcb0ffba..c5bf3d5fc 100644 --- a/ui-modules/blueprint-composer/app/components/designer/designer.directive.js +++ b/ui-modules/blueprint-composer/app/components/designer/designer.directive.js @@ -18,7 +18,8 @@ */ import angular from 'angular'; import {Entity} from "../util/model/entity.model"; -import {D3Blueprint} from "../util/d3-blueprint"; +import {D3BlueprintMgmtView} from "../util/d3-blueprint-mgmt-view"; +import {D3BlueprintLandscapeView} from "../util/d3-blueprint-landscape-view"; import {EntityFamily} from '../util/model/entity.model'; import {graphicalEditEntityState} from '../../views/main/graphical/edit/entity/edit.entity.controller'; import {graphicalEditSpecState} from '../../views/main/graphical/edit/spec/edit.spec.controller'; @@ -43,13 +44,15 @@ export function designerDirective($log, $state, $q, iconGenerator, catalogApi, b return tAttrs.templateUrl || TEMPLATE_URL; }, scope: { + mode: '@?', onSelectionChange: ' { diff --git a/ui-modules/blueprint-composer/app/components/util/d3-blueprint.js b/ui-modules/blueprint-composer/app/components/util/d3-blueprint-abstract.js similarity index 67% rename from ui-modules/blueprint-composer/app/components/util/d3-blueprint.js rename to ui-modules/blueprint-composer/app/components/util/d3-blueprint-abstract.js index b25bc046d..62c8dd8ca 100755 --- a/ui-modules/blueprint-composer/app/components/util/d3-blueprint.js +++ b/ui-modules/blueprint-composer/app/components/util/d3-blueprint-abstract.js @@ -18,23 +18,25 @@ */ import * as d3 from 'd3'; import {PREDICATE_MEMBERSPEC} from './model/entity.model'; -import addIcon from '../../img/icon-add.svg'; import {ISSUE_LEVEL} from './model/issue.model'; -export function D3Blueprint(container) { - let _svg = d3.select(container).append('svg').attr('class', 'blueprint-canvas'); - let _mirror = _svg.append('path').style('display', 'none'); - let _zoomGroup = _svg.append('g').attr('class', 'zoom-group'); - let _parentGroup = _zoomGroup.append('g').attr('class', 'parent-group'); - let _linkGroup = _parentGroup.append('g').attr('class', 'link-group'); - let _relationGroup = _parentGroup.append('g').attr('class', 'relation-group'); - let _specNodeGroup = _parentGroup.append('g').attr('class', 'spec-node-group'); - let _dropZoneGroup = _parentGroup.append('g').attr('class', 'dropzone-group'); - let _ghostNodeGroup = _parentGroup.append('g').attr('class', 'ghost-node-group'); - let _nodeGroup = _parentGroup.append('g').attr('class', 'node-group'); - let _cloneGroup = _parentGroup.append('g').attr('class', 'clone-group'); - - let _dragState = { +export function D3BlueprintAbstract(container) { + var bp = this; + let result = {}; + + let _svg = bp._svg = d3.select(container).append('svg').attr('class', 'blueprint-canvas'); + let _mirror = bp._mirror = _svg.append('path').style('display', 'none'); + let _zoomGroup = bp._zoomGroup = _svg.append('g').attr('class', 'zoom-group'); + let _parentGroup = bp._parentGroup = _zoomGroup.append('g').attr('class', 'parent-group'); + let _linkGroup = bp._linkGroup = _parentGroup.append('g').attr('class', 'link-group'); + let _relationGroup = bp._relationGroup = _parentGroup.append('g').attr('class', 'relation-group'); + let _specNodeGroup = bp._specNodeGroup = _parentGroup.append('g').attr('class', 'spec-node-group'); + let _dropZoneGroup = bp._dropZoneGroup = _parentGroup.append('g').attr('class', 'dropzone-group'); + let _ghostNodeGroup = bp._ghostNodeGroup = _parentGroup.append('g').attr('class', 'ghost-node-group'); + let _nodeGroup = bp._nodeGroup = _parentGroup.append('g').attr('class', 'node-group'); + let _cloneGroup = bp._cloneGroup = _parentGroup.append('g').attr('class', 'clone-group'); + + let _dragState = bp._dragState = { dragInProgress: false, dragStarted: false, clone: null, @@ -42,130 +44,9 @@ export function D3Blueprint(container) { cloneY: 0, }; - const _configHolder = { - nodes: { - root: { - rect: { - class: 'node-root', - x: -125, - y: -50, - width: 250, - height: 100, - rx: 50, - ry: 50, - }, - text: { - class: 'node-name', - width: 250, - height: 100 - }, - maxNameLength: 18 - }, - child: { - circle: { - r: 50, - class: (d)=>(`node-cluster node-cluster-${d}`) - }, - image: { - class: 'node-icon', - width: 64, - height: 64, - x: -32, - y: -32, - opacity: 0 - } - }, - location: { - rect: { - x: -50, - y: -110, - width: 100, - height: 50 - }, - image: { - x: -50, - y: -110, - width: 100, - height: 50, - opacity: 0 - } - }, - dropzonePrev: { - circle: { - cx: -150, - r: 30, - class: 'dropzone dropzone-prev' - }, - }, - dropzoneNext: { - circle: { - cx: 150, - r: 30, - class: 'dropzone dropzone-next' - } - }, - adjunct: { - rect: { - id: (d)=>(`entity-${d._id}`), - class: 'node-adjunct adjunct entity', - width: 20, - height: 20, - transform: 'scale(0)' - } - }, - memberspec: { - circle: { - r: 35, - cx: 0, - cy: 170, - class: 'node-spec-entity', - 'transform-origin': 0 - }, - image: { - x: -20, - y: 150, - width: 40, - height: 40, - opacity: 0, - class: 'node-spec-image', - 'transform-origin': 0 - } - }, - buttongroup: { - line: { - class: 'link', - x1: 0, - x2: 0, - y1: (d)=>(isRootNode(d) ? _configHolder.nodes.root.rect.height / 2 : _configHolder.nodes.child.circle.r), - y2: (d)=>((isRootNode(d) ? _configHolder.nodes.root.rect.height / 2 : _configHolder.nodes.child.circle.r) + 30), - }, - circle: { - class: 'connector', - r: 6, - cy: (d)=>(isRootNode(d) ? _configHolder.nodes.root.rect.height / 2 : _configHolder.nodes.child.circle.r), - } - }, - buttonAdd: { - circle: { - r: 20, - cy: 100 - }, - image: { - width: 50, - height: 50, - x: -25, - y: 75, - 'xlink:href': addIcon - } - } - }, - transition: 300, - grid: { - itemPerCol: 3, - gutter: 15 - }, - }; - let _d3DataHolder = { + const config = bp.config = {}; + + let d3data = bp.d3data = { nodes: [], ghostNodes: [], orphans: [], @@ -173,7 +54,8 @@ export function D3Blueprint(container) { relationships: [], }; - let zoom = d3.zoom().scaleExtent([0.1, 1]).on('zoom', onSvgZoom); + let zoom = bp.zoom = d3.zoom().scaleExtent([0.1, 1]).on('zoom', onSvgZoom); + _svg .attr('preserveAspectRatio', 'xMinYMin meet') .attr('viewBox', () => { @@ -193,7 +75,8 @@ export function D3Blueprint(container) { .attr('width', 4) .attr('height', 4); pattern.append('path') - .attr('d', 'M1 3h1v1H1V3zm2-2h1v1H3V1z'); + // fill colour set by CSS; no stroke wanted + .attr('d', 'M 1 3 h1 v1 H1 V3 z m 2 -2 h1 v1 H3 V1 z'); let defs = _svg.append('defs'); let arrowhead = defs.append('marker') @@ -349,7 +232,7 @@ export function D3Blueprint(container) { } if (node.depth) { // exclude the root element onSvgDragOver(); - hideInvalidDropzones(node); + hideInvalidDropZones(node); d3.event.sourceEvent.preventDefault(); // disable browser text selection d3.event.sourceEvent.stopPropagation(); @@ -359,8 +242,11 @@ export function D3Blueprint(container) { _dragState.dragStarted = true; let entityId = node.data._id; let mouseCoords = d3.mouse(_nodeGroup.select(`#entity-${node.data._id}`).node()); - _dragState.cloneX = node.x + _configHolder.nodes.child.circle.r + mouseCoords[0]; - _dragState.cloneY = node.y + _configHolder.nodes.child.circle.r + mouseCoords[1]; + // would be more normal not to add child sizes and mouseCoords + // (so cursor stays in the same position relative to the dragged item as where the user clicks) + // but that breaks the code for dropping/hovering when moving items + _dragState.cloneX = node.x + config.child.width/2 + mouseCoords[0]; + _dragState.cloneY = node.y + config.child.height/2 + mouseCoords[1]; _dragState.clone = _cloneGroup.append('use') .attr('xlink:href', function(d) { return `#entity-${entityId}` } ) @@ -424,8 +310,8 @@ export function D3Blueprint(container) { } else { setTimeout(() => { showRelationships(); - showDropzones(); - }, _configHolder.transition); + showDropZones(); + }, config.global.transitionDuration); } } if (_dragState.clone) { @@ -512,43 +398,27 @@ export function D3Blueprint(container) { * * @param {object} blueprint The graph */ - function update(blueprint, relationships) { - let tree = d3.tree() - .nodeSize([_configHolder.nodes.child.circle.r * 6, _configHolder.nodes.child.circle.r * 6]) - .separation((right, left)=> { - let maxColumnsBeforeExpand = 2; - let adjuncts = getImportantAdjuncts(left).length; - let currentCols = Math.floor(adjuncts / _configHolder.grid.itemPerCol) + (adjuncts > 0 && adjuncts % _configHolder.grid.itemPerCol !== 0 ? 1 : 0); - let additionalCol = currentCols > maxColumnsBeforeExpand ? currentCols - maxColumnsBeforeExpand : 0; - - let colWidth = _configHolder.nodes.adjunct.rect.width + 15; - - return 1 + (colWidth / (_configHolder.nodes.child.circle.r * 6)) * additionalCol; - }); - let root = d3.hierarchy(blueprint); - tree(root); - _d3DataHolder.nodes = root.descendants(); - _d3DataHolder.links = root.links(); - _d3DataHolder.relationships = relationships; - return this; + bp.update = (blueprint, relationships) => { + bp.config.global.updateLayout(blueprint, relationships, d3data); + return result; } /** * Redraw the graph */ - function draw() { - drawLinks(); - drawRelationships(); - drawNodeGroup(); - drawSpecNodeGroup(); - drawGhostNode(); - drawDropZoneGroup(); - return this; + bp.draw = () => { + bp.drawLinks(); + bp.drawRelationships(); + bp.drawNodeGroup(); + bp.drawSpecNodeGroup(); + bp.drawGhostNode(); + bp.drawDropZoneGroup(); + return result; } - function drawNodeGroup() { + bp.drawNodeGroup = () => { let nodeData = _nodeGroup.selectAll('g.node') - .data(_d3DataHolder.nodes, (d)=>(`node-${d.data._id}`)); + .data(bp.d3data.nodes, (d)=>(`node-${d.data._id}`)); // Draw group that contains all SVG element: node representation and location/policies/enricher indicators // ----------------------------------------------------- @@ -562,12 +432,12 @@ export function D3Blueprint(container) { .attr('transform', (d)=>(`translate(${d.x}, ${d.y}) scale(${isRootNode(d) ? 1 : 0})`)) .attr('opacity', (d)=> (isRootNode(d) ? 0 : 1)); nodeData.transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('transform', (d)=>(`translate(${d.x}, ${d.y}) scale(1)`)) .attr('opacity', 1); nodeData.exit() .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('transform', (d)=>(`translate(${d.x}, ${d.y}) scale(${isRootNode(d) ? 1 : 0})`)) .attr('opacity', (d)=> (isRootNode(d) ? 0 : 1)) .remove(); @@ -591,21 +461,26 @@ export function D3Blueprint(container) { .classed('loading', (d)=>(d.data.miscData.get('loading'))); // Draw root node - appendElements(entity.filter(isRootNode), _configHolder.nodes.root); + appendElements(entity.filter(isRootNode), config.root); + nodeData.filter(isRootNode).select('.node-entity text') - .text(trimNodeText) + .text(trimRootNodeText) .transition() - .duration(_configHolder.transition) - .text(trimNodeText); + .duration(config.global.transitionDuration) + .text(trimRootNodeText); nodeData.filter(isChildNode).select('.node-entity image') .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('opacity', (d)=>(d.data.hasIcon() ? 1 : 0)) .attr('xlink:href', (d)=>(d.data.icon)); + nodeData.filter(isChildNode).select('.node-entity text') + .text(trimChildNodeText) + .transition() + .duration(config.global.transitionDuration) + .text(trimChildNodeText); // Draw child nodes - appendElement(entity.filter(isChildNode).selectAll('circle').data([2, 1, 0]).enter(), 'circle', _configHolder.nodes.child.circle); - appendElement(entity.filter(isChildNode), 'image', _configHolder.nodes.child.image); + appendElements(entity.filter(isChildNode).selectAll('circle').data([2, 1, 0]).enter(), config.child); // Draw location // ----------------------------------------------------- @@ -614,12 +489,12 @@ export function D3Blueprint(container) { .classed('loading', (d)=>(d.data.miscData.get('loading'))); nodeData.select('g.node-location') .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('opacity', (d)=>(d.data.hasLocation() ? 1 : 0)); - appendElements(location, _configHolder.nodes.location); + appendElements(location, config.location); nodeData.select('g.node-location image') .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('opacity', (d)=>(d.data.miscData.get('locationIcon') ? 1 : 0)) .attr('xlink:href', (d)=>(d.data.miscData.get('locationIcon'))); @@ -636,22 +511,22 @@ export function D3Blueprint(container) { .classed('loading', (d)=>(d.miscData.get('loading'))) .on('click', onEntityClick); adjunctData.transition() - .duration(_configHolder.transition) - .attr('x', (d, i)=>(getGridX(d, i))) - .attr('y', (d, i)=>(getGridY(d, i))) + .duration(config.global.transitionDuration) + .attr('x', bp.getAdjunctGridX) + .attr('y', bp.getAdjunctGridY) .attr('transform', 'scale(1)') - .attr('transform-origin', (d, i)=>(getGridItemCenter(d, i))); + .attr('transform-origin', bp.getAdjunctGridItemCenter); adjunctData.exit() .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('transform', 'scale(0)') .remove(); - appendElement(adjunctData.enter(), 'rect', _configHolder.nodes.adjunct.rect); - } + appendElements(adjunctData.enter(), config.adjunct); + }; - function drawLinks() { + bp.drawLinks = () => { let link = _linkGroup.selectAll('line.link') - .data(_d3DataHolder.links, (d)=>(d.source.data._id + '_to_' + d.target.data._id)); + .data(bp.d3data.links, (d)=>(d.source.data._id + '_to_' + d.target.data._id)); link.enter().insert('line') .attr('class', 'link') @@ -660,7 +535,7 @@ export function D3Blueprint(container) { .attr('x2', (d)=>(d.source.x)) .attr('y2', (d)=>(d.source.y)); link.transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('x1', (d)=>(d.source.x)) .attr('y1', (d)=>(d.source.y)) .attr('x2', (d)=>(d.target.x)) @@ -677,7 +552,7 @@ export function D3Blueprint(container) { * @return {*} a D3 tree node */ function nodeForEntity(entity) { - let node = _d3DataHolder.nodes.find(d => { + let node = bp.d3data.nodes.find(d => { let predicate = d.data._id === entity._id; if (!!d.data.getClusterMemberspecEntity(PREDICATE_MEMBERSPEC)) { predicate |= d.data.getClusterMemberspecEntity(PREDICATE_MEMBERSPEC)._id === entity._id; @@ -690,11 +565,11 @@ export function D3Blueprint(container) { return node; } - function drawRelationships() { + bp.drawRelationships = () => { showRelationships(); let relationData = _relationGroup.selectAll('.relation') - .data(_d3DataHolder.relationships, (d)=>(d.source._id + '_related_to_' + d.target._id)); + .data(bp.d3data.relationships, (d)=>(d.source._id + '_related_to_' + d.target._id)); relationData.enter().insert('path') .attr('class', 'relation') @@ -702,21 +577,23 @@ export function D3Blueprint(container) { .attr('from', (d)=>(d.source._id)) .attr('to', (d)=>(d.target._id)); relationData.transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('opacity', 1) .attr('stroke', 'red') .attr('d', function(d) { let targetNode = nodeForEntity(d.target); let sourceNode = nodeForEntity(d.source); - let sourceY = sourceNode.y + (d.source.isMemberSpec() ? _configHolder.nodes.memberspec.circle.cy : 0); - let targetY = targetNode.y + (d.target.isMemberSpec() ? _configHolder.nodes.memberspec.circle.cy : 0); - let dx = targetNode.x - sourceNode.x; + let sourceX = sourceNode.x + (d.source.isMemberSpec() ? config.memberspec.deltaX : 0); + let targetX = targetNode.x + (d.target.isMemberSpec() ? config.memberspec.deltaX : 0); + let sourceY = sourceNode.y + (d.source.isMemberSpec() ? config.memberspec.deltaY : 0); + let targetY = targetNode.y + (d.target.isMemberSpec() ? config.memberspec.deltaY : 0); + let dx = targetX - sourceX; let dy = targetY - sourceY; let dr = Math.sqrt(dx * dx + dy * dy); let sweep = dx * dy > 0 ? 0 : 1; - _mirror.attr('d', `M ${sourceNode.x},${sourceY} A ${dr},${dr} 0 0,${sweep} ${targetNode.x},${targetY}`); - - let m = _mirror._groups[0][0].getPointAtLength(_mirror._groups[0][0].getTotalLength() - _configHolder.nodes.child.circle.r - 20); + + _mirror.attr('d', `M ${sourceX},${sourceY} A ${dr},${dr} 0 0,${sweep} ${targetX},${targetY}`); + let m = _mirror._groups[0][0].getPointAtLength(_mirror._groups[0][0].getTotalLength() - config.child.radius - 20); dx = m.x - sourceNode.x; dy = m.y - sourceY; @@ -726,14 +603,14 @@ export function D3Blueprint(container) { }); relationData.exit() .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('opacity', 0) .remove(); } - function drawGhostNode() { + bp.drawGhostNode = () => { let ghostNodeData = _ghostNodeGroup.selectAll('g.ghost-node') - .data(_d3DataHolder.nodes, (d)=>(`ghost-node-${d.data._id}`)); + .data(bp.d3data.nodes, (d)=>(`ghost-node-${d.data._id}`)); let ghostNode = ghostNodeData .enter() .append('g') @@ -744,32 +621,32 @@ export function D3Blueprint(container) { .on('mouseleave', onGhostLeave); ghostNodeData .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('transform', (d)=>(`translate(${d.x}, ${d.y})`)); ghostNodeData.exit().remove(); ghostNode.append('rect') .attr('class', 'ghost') - .attr('width', (d)=>(isRootNode(d) ? _configHolder.nodes.root.rect.width : _configHolder.nodes.child.circle.r * 2)) - .attr('height', (d)=>((isRootNode(d) ? _configHolder.nodes.root.rect.height : _configHolder.nodes.child.circle.r * 2) + 80)) - .attr('x', (d)=>(isRootNode(d) ? _configHolder.nodes.root.rect.x : -_configHolder.nodes.child.circle.r)) - .attr('y', (d)=>(isRootNode(d) ? _configHolder.nodes.root.rect.y : -_configHolder.nodes.child.circle.r)); + .attr('width', bp.config.global.nodeWidth) + .attr('height', (d)=>(bp.config.global.nodeHeight(d) + 80)) + .attr('x', (d)=>(-bp.config.global.nodeWidth(d)/2)) + .attr('y', (d)=>(-bp.config.global.nodeHeight(d)/2)); let buttonsGroup = ghostNode.append('g') .attr('class', 'buttons'); - appendElements(buttonsGroup, _configHolder.nodes.buttongroup); + appendElements(buttonsGroup, config.buttongroup); let buttonAdd = buttonsGroup.append('g') .attr('class', 'button button-add') .on('click', onAddChildClick); - appendElements(buttonAdd, _configHolder.nodes.buttonAdd); + appendElements(buttonAdd, config.buttonAdd); } - function drawDropZoneGroup() { - showDropzones(); + bp.drawDropZoneGroup = () => { + showDropZones(); let dropZoneData = _dropZoneGroup.selectAll('g.dropzone-group-node') - .data(_d3DataHolder.nodes, (d)=>(`dropzone-${d.data._id}`)); + .data(bp.d3data.nodes, (d)=>(`dropzone-${d.data._id}`)); let dropZoneGroup = dropZoneData .enter() @@ -779,19 +656,14 @@ export function D3Blueprint(container) { .attr('transform', (d)=>(`translate(${d.x}, ${d.y})`)); dropZoneData .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('transform', (d)=>(`translate(${d.x}, ${d.y})`)); dropZoneData.exit().remove(); - appendElement(dropZoneGroup.filter(isRootNode), 'rect', Object.assign({}, - _configHolder.nodes.root.rect, - // expand above by 7 - {x: -132, y: -57, rx: 57, ry: 57, width: 264, height: 114, class: 'dropzone dropzone-self'})); - appendElement(dropZoneGroup.filter(isChildNode), 'circle', Object.assign({}, - _configHolder.nodes.child.circle, - {transform: (d) => (`scale(${d.data.isCluster() ? 1.5 : 1.15})`), class: 'dropzone dropzone-self'})); - appendElements(dropZoneGroup.filter(isChildNode), _configHolder.nodes.dropzonePrev); - appendElements(dropZoneGroup.filter(isChildNode), _configHolder.nodes.dropzoneNext); + appendElement(dropZoneGroup.filter(isRootNode), bp.config.root.shape, bp.config.root.dropOverrides); + appendElement(dropZoneGroup.filter(isChildNode), bp.config.child.shape, bp.config.child.dropOverrides); + appendElements(dropZoneGroup.filter(isChildNode), config.dropzonePrev); + appendElements(dropZoneGroup.filter(isChildNode), config.dropzoneNext); dropZoneData.select('.dropzone-self') .attr('id', (d)=>(`dropzone-self-${d.data._id}`)) @@ -836,9 +708,9 @@ export function D3Blueprint(container) { .on('drop', (d) => onExternalDrop(d, `dropzone-self-${d.data._id}`)); } - function drawSpecNodeGroup() { + bp.drawSpecNodeGroup = () => { let specNodeData = _specNodeGroup.selectAll('g.spec-node') - .data(_d3DataHolder.nodes.filter((node)=>{ + .data(bp.d3data.nodes.filter((node)=>{ return !!node.data.getClusterMemberspecEntity(PREDICATE_MEMBERSPEC); }), (d)=>(`spec-node-${d.data._id}`)); let specNodeGroup = specNodeData @@ -848,32 +720,32 @@ export function D3Blueprint(container) { .attr('class', 'spec-node') .attr('transform', (d)=>(`translate(${d.x}, ${d.y})`)); specNodeData.transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('transform', (d)=>(`translate(${d.x}, ${d.y}) rotate(${d.data.hasChildren() ? -45 : 0})`)); specNodeData.exit() .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('opacity', 0) .remove(); specNodeGroup.append('polygon') .attr('class', 'node-memberspec-link') .attr('points', (d)=> { - let left = _configHolder.nodes.memberspec.circle.r * -1; - let right = _configHolder.nodes.memberspec.circle.r; - let bottom = _configHolder.nodes.memberspec.circle.cy; + let left = config.memberspec.deltaX + -config.memberspec.width/2; + let right = config.memberspec.deltaX + config.memberspec.width/2; + let bottom = config.memberspec.deltaY; return `0,0 ${right},${bottom} ${left},${bottom}`; }) .attr('transform', 'scale(0)'); specNodeData.select('polygon') .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('transform', 'scale(1)'); let specNode = specNodeGroup.append('g') .attr('class', 'node-memberspec entity') .attr('id', (d)=>(`entity-${d.data.getClusterMemberspecEntity(PREDICATE_MEMBERSPEC)._id}`)) - .attr('transform-origin', `0 ${_configHolder.nodes.memberspec.circle.cy}`) + .attr('transform-origin', `${config.memberspec.deltaX} ${config.memberspec.deltaY}`) .attr('transform', 'scale(0)') .on('click', (d)=>(onEntityClick({data: d.data.getClusterMemberspecEntity(PREDICATE_MEMBERSPEC)}))); specNodeData.select('.node-memberspec') @@ -881,151 +753,168 @@ export function D3Blueprint(container) { .classed('loading', (d)=>(d.data.getClusterMemberspecEntity(PREDICATE_MEMBERSPEC).miscData.get('loading'))); specNodeData.select('.node-memberspec') .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('transform', 'scale(1)'); - appendElements(specNode, _configHolder.nodes.memberspec); + appendElements(specNode, config.memberspec); specNodeData.select('image') .transition() - .duration(_configHolder.transition) + .duration(config.global.transitionDuration) .attr('opacity', (d)=>(d.data.getClusterMemberspecEntity(PREDICATE_MEMBERSPEC).hasIcon() ? 1 : 0)) .attr('xlink:href', (d)=>(d.data.getClusterMemberspecEntity(PREDICATE_MEMBERSPEC).icon)); } - function appendElements(node, definition) { - let elements = []; - Object.keys(definition).forEach((tag)=> { - let properties = definition[tag]; - let element = appendElement(node, tag, properties); - elements.push(element); - }); - return elements; - } + let appendElements = bp.appendElements = (node, definition) => { + return Object.keys(definition).reduce( (elements, tagId) => { + elements.push(appendElement(node, definition[tagId])); + return elements; + }, []); + }; - function appendElement(node, tag, properties) { - let element = node.append(tag); - Object.keys(properties).forEach((property)=> { - element.attr(property, properties[property]); - }); - return element; - } + let appendElement = bp.appendElement = (node, properties, overrides) => { + let tag = properties.tag; + let attrs = properties.attrs; + if (!tag || !attrs) return; + return Object.keys( Object.assign({}, overrides, attrs) ).reduce((element, property)=> { + let val = attrs[property]; + let override = overrides ? overrides[property] : null; + if (override) { + if (typeof override === 'function') { + let ov = val; + if (typeof val === 'function') { + val = (d) => override(ov(d), d); + } else { + val = (d) => override(ov, d); + } + } else { + val = override; + } + } + return element.attr(property, val); + }, node.append(tag)); + }; /** - * Calculate the X coordinate of a policies/enricher to place it on the grid + * Calculate the X coordinate of a policies/enricher to place it on the adjunct grid * * @param d the current {entity} * @param i the index * @returns {number} The X coordinate within the grid */ - function getGridX(d, i) { - let nodeWidth = isRootNode(d.parent) ? _configHolder.nodes.root.rect.width : _configHolder.nodes.child.circle.r * 2; - let offset = (_configHolder.nodes.adjunct.rect.width + _configHolder.grid.gutter) * Math.floor(i / _configHolder.grid.itemPerCol); + bp.getAdjunctGridX = (d, i) => { + let nodeWidth = bp.config.global.nodeWidth(d.parent); + let offset = (config.adjunct.width + config.adjunct.gutterSize) * Math.floor(i / config.adjunct.itemPerCol); if (d.parent.isCluster()) { offset += 20; } - return _configHolder.grid.gutter + (nodeWidth/2) + offset; - } + return config.adjunct.gutterSize + (nodeWidth/2) + offset; + }; /** - * Calculate the Y coordinate of a policies/enricher to place it on the grid + * Calculate the Y coordinate of a policies/enricher to place it on the adjunct grid * * @param d the current {entity} * @param i the index * @returns {number} The Y coordinate within the grid */ - function getGridY(d, i) { - let nodeHeight = isRootNode(d.parent) ? _configHolder.nodes.root.rect.height : _configHolder.nodes.child.circle.r * 2; - let columnHeight = _configHolder.nodes.adjunct.rect.height * _configHolder.grid.itemPerCol + _configHolder.grid.gutter * (_configHolder.grid.itemPerCol - 1); + bp.getAdjunctGridY = (d, i) => { + let nodeHeight = bp.config.global.nodeHeight(d.parent); + let columnHeight = config.adjunct.height * config.adjunct.itemPerCol + config.adjunct.gutterSize * (config.adjunct.itemPerCol - 1); let offset = nodeHeight > columnHeight ? (nodeHeight - columnHeight) / 2 : 0; - return (_configHolder.nodes.adjunct.rect.height + _configHolder.grid.gutter) * (i%_configHolder.grid.itemPerCol) - (nodeHeight/2) + offset; - } + return (config.adjunct.height + config.adjunct.gutterSize) * (i%config.adjunct.itemPerCol) - (nodeHeight/2) + offset; + }; /** - * Calculate the center coordinates of a policies/enricher to place it on the grid + * Calculate the center coordinates of a policies/enricher to place it on the adjunct grid * * @param d the current {entity} * @param i the index * @returns {number} The center coordinates within the grid */ - function getGridItemCenter(d, i) { - let centerX = getGridX(d, i) + _configHolder.nodes.adjunct.rect.width / 2; - let centerY = getGridY(d, i) + _configHolder.nodes.adjunct.rect.height / 2; + bp.getAdjunctGridItemCenter = (d, i) => { + let centerX = getGridX(d, i) + config.adjunct.width / 2; + let centerY = getGridY(d, i) + config.adjunct.height / 2; return `${centerX} ${centerY}`; - } + }; /** * Center the graph in the view, considering palette */ - function center() { + let center = bp.center = () => { let newX = window.innerWidth/2 + (window.innerWidth > 660 ? 220 : 0); - let newY = _configHolder.nodes.child.circle.r + (_configHolder.nodes.child.circle.r * 2); + let newY = config.child.height * 3/2; zoom.translateBy(_svg, newX, newY); - return this; + return result; } - function trimNodeText(d) { + let trimRootNodeText = bp.trimRootNodeText = (d) => { if (!d.data.metadata.has('name') || d.data.metadata.get('name').length === 0) { return 'New application'; } else { let name = d.data.metadata.get('name'); - return name.length > _configHolder.nodes.root.maxNameLength ? name.substring(0, _configHolder.nodes.root.maxNameLength) + '...' : name + return name.length > config.root.maxNameLength ? name.substring(0, config.root.maxNameLength-2) + '...' : name } } - function isRootNode(d) { - return d.depth === 0; + let trimChildNodeText = bp.trimChildNodeText = (d) => { + var name; + if (!d.data.metadata.has('name') || d.data.metadata.get('name').length === 0) { + name = d.data.metadata.get('type'); + } else { + name = d.data.metadata.get('name'); + } + return name.length > config.child.maxNameLength ? name.substring(0, config.child.maxNameLength-2) + '...' : name } - function isChildNode(d) { - return d.depth > 0; - } + let isRootNode = bp.isRootNode = (d) => d.depth === 0; + + let isChildNode = bp.isChildNode = (d) => d.depth > 0; - function getImportantAdjuncts(d) { - let adjuncts = d.data.getPoliciesAsArray().concat(d.data.getEnrichersAsArray()); - return adjuncts.filter((adjunct)=>(adjunct.miscData.has('important') && adjunct.miscData.get('important') === true)); - } + let getImportantAdjuncts = bp.getImportantAdjuncts = (d) => + [].concat(d.data.getPoliciesAsArray()).concat(d.data.getEnrichersAsArray()) + .filter( (adjunct)=>(adjunct.miscData.has('important') && adjunct.miscData.get('important') === true) ); - function selectNode(id) { + let selectNode = bp.selectNode = (id) => { _svg.selectAll('.entity.selected').classed('selected', false); _svg.selectAll('.relation.highlight').classed('highlight', false); _svg.select(`#entity-${id}`).classed('selected', true); _svg.selectAll(`.relation[from='${id}']`).classed('highlight', true); _svg.selectAll(`.relation[to='${id}']`).classed('highlight', true); - return this; - } + return result; + }; - function unselectNode() { + let unselectNode = bp.unselectNode = () => { _svg.selectAll('.entity.selected').classed('selected', false); _svg.selectAll('.relation.highlight').classed('highlight', false); - return this; - } + return result; + }; /** * Hide the relationships for the dragged entity and its descendants * @param node the node for the dragged entity */ - function hideRelationships(node) { - _d3DataHolder.relationships + let hideRelationships = bp.hideRelationships = (node) => { + bp.d3data.relationships .filter(r => r.source.hasAncestor(node.data) || r.target.hasAncestor(node.data)) .forEach(r => { _relationGroup.selectAll(`.relation[from='${r.source._id}'][to='${r.target._id}']`).classed('hidden', true); }); - } + }; /** * Shows all relationships */ - function showRelationships() { + let showRelationships = bp.showRelationships = () => { _relationGroup.selectAll('.relation').classed('hidden', false); - } + }; /** * Hide the invalid dropzones for the dragged node * @param node the node that is being dragged */ - function hideInvalidDropzones(node) { - _d3DataHolder.nodes + let hideInvalidDropZones = bp.hideInvalidDropZones = (node) => { + d3data.nodes .filter(d => d.data.hasAncestor(node.data)) .forEach(d => { _dropZoneGroup.selectAll(`#dropzone-group-node-${d.data._id} .dropzone`).classed('hidden', true); @@ -1037,18 +926,20 @@ export function D3Blueprint(container) { /** * Shows all dropzones */ - function showDropzones() { + let showDropZones = bp.showDropZones = () => { _dropZoneGroup.selectAll('.dropzone').classed('hidden', false); - } + }; // register global key events d3.select('body').on('keyup.body', onKeyUp); - return { - draw: draw, - update: update, - center: center, - select: selectNode, + // public signature + Object.assign(result, { + draw: bp.draw, + update: bp.update, + center: bp.center, + select: bp.selectNode, unselect: unselectNode - }; + }); + return result; } diff --git a/ui-modules/blueprint-composer/app/components/util/d3-blueprint-landscape-view.js b/ui-modules/blueprint-composer/app/components/util/d3-blueprint-landscape-view.js new file mode 100755 index 000000000..7703f7eed --- /dev/null +++ b/ui-modules/blueprint-composer/app/components/util/d3-blueprint-landscape-view.js @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import {D3BlueprintAbstract} from './d3-blueprint-abstract'; +import addIcon from '../../img/icon-add.svg'; +import * as d3 from 'd3'; + +export function D3BlueprintLandscapeView(container) { + let bp = this; + let result = D3BlueprintAbstract.call(bp, container); + + function updateLayout(blueprint, relationships, d3data) { + // TODO new layout will be needed to give fill width + let tree = d3.tree() + .nodeSize([bp.config.child.width * 2, bp.config.child.height * 3]) + .separation((right, left)=> { + let maxColumnsBeforeExpand = 2; + let adjuncts = bp.getImportantAdjuncts(left).length; + let currentCols = Math.floor(adjuncts / bp.config.adjunct.itemPerCol) + (adjuncts > 0 && adjuncts % bp.config.adjunct.itemPerCol !== 0 ? 1 : 0); + let additionalCol = currentCols > maxColumnsBeforeExpand ? currentCols - maxColumnsBeforeExpand : 0; + + let colWidth = bp.config.adjunct.width + 15; + + return 1 + (colWidth / (bp.config.child.radius * 6)) * additionalCol; + }); + let root = d3.hierarchy(blueprint); + tree(root); + d3data.nodes = root.descendants(); + d3data.links = root.links(); + d3data.relationships = relationships; + }; + + let tagWithAttrs = (tag, attrs) => { return { tag, attrs }; }; + let addC = (n, ifMissing) => (x) => typeof x !== 'undefined' ? x+n : typeof ifMissing !== 'undefined' ? ifMissing : n; + + let getNodeProperty = (node, prop) => { + let data = bp.isRootNode(node) ? bp.config.root : bp.config.child; + return data[prop]; + }; + let getPropertyFn = (prop) => (node) => getNodeProperty(node, prop); + // TODO width and height will be set by layout and should then be retrieved from layout + let width = (node) => getNodeProperty(node, 'width'); + let height = (node) => getNodeProperty(node, 'height'); + + let childRadius = 50; + let locationAttrs = { width: 100, height: 50 }; + Object.assign(locationAttrs, { x: -locationAttrs.width/2, y: -110 }); + let memberspecRadius = 35; + + Object.assign(bp.config, { + global: { + transitionDuration: 300, + nodeWidth: width, + nodeHeight: height, + updateLayout, + }, + + root: { + width: 400, + height: 100, + maxNameLength: 30, + shape: tagWithAttrs('rect', { + class: 'node-root', + x: (d) => -width(d)/2, + y: (d) => -height(d)/2, + width, + height, + rx: 15, + ry: 15 + }), + title: tagWithAttrs('text', { + class: 'node-name', + width, + height, + }), + dropOverrides: { + // expand by 7 + x: addC(-7), y: addC(-7), + rx: addC(7, 0), ry: addC(7, 0), + width: addC(2*7), height: addC(2*7), + class: (c) => (c || '') + ' dropzone dropzone-self', + }, + }, + + child: { + radius: childRadius, + width: 250, + height: 100, + imgSize: 96, + maxNameLength: 15, + + shape: tagWithAttrs('rect', { + x: (d) => -width(d)/2, + y: (d) => -height(d)/2, + width, + height, + rx: 15, + ry: 15, + class: (d)=>(`node-cluster node-cluster-${d}`) + }), + title: tagWithAttrs('text', { + class: 'node-name', + width: (d) => width(d) - bp.config.child.imgSize - 16, + height, + x: 40, + y: 0, + }), + icon: tagWithAttrs('image', { + class: 'node-icon', + width: getPropertyFn('imgSize'), + height: getPropertyFn('imgSize'), + x: (d) => -width(d)/2 + 4, + y: (d) => -getPropertyFn('imgSize')(d)/2, + opacity: 0 + }), + dropOverrides: { + // expand by 7 + x: addC(-7), y: addC(-7), + rx: addC(7, 0), ry: addC(7, 0), + width: addC(2*7), height: addC(2*7), + class: (c) => (c || '') + ' dropzone dropzone-self', + }, + }, + + location: { + shape: tagWithAttrs('rect', locationAttrs), + icon: tagWithAttrs('image', Object.assign({ opacity: 0 }, locationAttrs)), + }, + + dropzonePrev: { + shape: tagWithAttrs('circle', { + cx: -4*childRadius, + r: 3/5 * childRadius, + class: 'dropzone dropzone-prev' + }), + }, + dropzoneNext: { + shape: tagWithAttrs('circle', { + cx: 4*childRadius, + r: 3/5 * childRadius, + class: 'dropzone dropzone-next' + }), + }, + + adjunct: { + width: 20, + height: 20, + itemPerCol: 3, + gutterSize: 15, + adjunctBox: tagWithAttrs('rect', { + id: (d)=>(`entity-${d._id}`), // TODO should this be adjunct-d._id ? + class: 'node-adjunct adjunct entity', + width: () => bp.config.adjunct.width, + height: () => bp.config.adjunct.height, + transform: 'scale(0)' + }), + }, + + memberspec: { + deltaX: 0, + deltaY: 170, + width: 2*memberspecRadius, + height: 2*memberspecRadius, + radius: memberspecRadius, + iconSize: 40, + circle: tagWithAttrs('circle', { + r: () => bp.config.memberspec.radius, + cx: () => bp.config.memberspec.deltaX, + cy: () => bp.config.memberspec.deltaY, + class: 'node-spec-entity', + 'transform-origin': 0 + }), + icon: tagWithAttrs('image', { + x: () => bp.config.memberspec.deltaX - bp.config.memberspec.iconSize/2, + y: () => bp.config.memberspec.deltaY - bp.config.memberspec.iconSize/2, + width: () => bp.config.memberspec.iconSize, + height: () => bp.config.memberspec.iconSize, + opacity: 0, + class: 'node-spec-image', + 'transform-origin': 0 + }), + }, + + buttongroup: { + lineToGroup: tagWithAttrs('line', { + class: 'link', + x1: 0, + x2: 0, + y1: (d) => height(d)/2, + y2: (d) => height(d)/2 + 30, + }), + circleAsLineSource: tagWithAttrs('circle', { + class: 'connector', + r: 6, + cy: (d) => height(d)/2, + }), + }, + buttonAdd: { + circleAsTarget: tagWithAttrs('circle', { + r: 20, + cy: 100, + }), + imageInGreenCircle: tagWithAttrs('image', { + width: 50, + height: 50, + x: -25, + y: 75, + 'xlink:href': addIcon + }), + }, + }); + + return result; +} diff --git a/ui-modules/blueprint-composer/app/components/util/d3-blueprint-mgmt-view.js b/ui-modules/blueprint-composer/app/components/util/d3-blueprint-mgmt-view.js new file mode 100755 index 000000000..2299cfad2 --- /dev/null +++ b/ui-modules/blueprint-composer/app/components/util/d3-blueprint-mgmt-view.js @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import {D3BlueprintAbstract} from './d3-blueprint-abstract'; +import addIcon from '../../img/icon-add.svg'; +import * as d3 from 'd3'; + +export function D3BlueprintMgmtView(container) { + let bp = this; + let result = D3BlueprintAbstract.call(bp, container); + + function updateLayout(blueprint, relationships, d3data) { + let tree = d3.tree() + .nodeSize([bp.config.child.radius * 6, bp.config.child.radius * 6]) + .separation((right, left)=> { + let maxColumnsBeforeExpand = 2; + let adjuncts = bp.getImportantAdjuncts(left).length; + let currentCols = Math.floor(adjuncts / bp.config.adjunct.itemPerCol) + (adjuncts > 0 && adjuncts % bp.config.adjunct.itemPerCol !== 0 ? 1 : 0); + let additionalCol = currentCols > maxColumnsBeforeExpand ? currentCols - maxColumnsBeforeExpand : 0; + + let colWidth = bp.config.adjunct.width + 15; + + return 1 + (colWidth / (bp.config.child.radius * 6)) * additionalCol; + }); + let root = d3.hierarchy(blueprint); + tree(root); + d3data.nodes = root.descendants(); + d3data.links = root.links(); + d3data.relationships = relationships; + }; + + let tagWithAttrs = (tag, attrs) => { return { tag, attrs }; }; + let addC = (n, ifMissing) => (x) => typeof x !== 'undefined' ? x+n : typeof ifMissing !== 'undefined' ? ifMissing : n; + + let getNodeProperty = (node, prop) => { + let data = bp.isRootNode(node) ? bp.config.root : bp.config.child; + return data[prop]; + }; + let getPropertyFn = (prop) => (node) => getNodeProperty(node, prop); + let width = (node) => getNodeProperty(node, 'width'); + let height = (node) => getNodeProperty(node, 'height'); + + let childRadius = 50; + let locationAttrs = { width: 100, height: 50 }; + Object.assign(locationAttrs, { x: -locationAttrs.width/2, y: -110 }); + let memberspecRadius = 35; + + Object.assign(bp.config, { + global: { + transitionDuration: 300, + nodeWidth: width, + nodeHeight: height, + updateLayout, + }, + + root: { + width: 250, + height: 100, + maxNameLength: 18, + shape: tagWithAttrs('rect', { + class: 'node-root', + x: (d) => -width(d)/2, + y: (d) => -height(d)/2, + width, + height, + rx: 50, + ry: 50 + }), + title: tagWithAttrs('text', { + class: 'node-name', + width, + height, + }), + dropOverrides: { + // expand by 7 + x: addC(-7), y: addC(-7), + rx: addC(7, 0), ry: addC(7, 0), + width: addC(2*7), height: addC(2*7), + class: (c) => (c || '') + ' dropzone dropzone-self', + }, + }, + + child: { + radius: childRadius, + width: 2*childRadius, + height: 2*childRadius, + imgSize: 64, + + shape: tagWithAttrs('circle', { + r: getPropertyFn('radius'), + class: (d)=>(`node-cluster node-cluster-${d}`) + }), + icon: tagWithAttrs('image', { + class: 'node-icon', + width: getPropertyFn('imgSize'), + height: getPropertyFn('imgSize'), + x: (d) => -getPropertyFn('imgSize')(d)/2, + y: (d) => -getPropertyFn('imgSize')(d)/2, + opacity: 0 + }), + dropOverrides: { + // scale by 1.5 or 1.15 + transform: (ignoreOldVal, d) => (d && d.data && d.data.isCluster ? `scale(${d.data.isCluster() ? 1.5 : 1.15})` : 'scale(1)'), + class: (c) => (c || '') + ' dropzone dropzone-self', + }, + }, + + location: { + shape: tagWithAttrs('rect', locationAttrs), + icon: tagWithAttrs('image', Object.assign({ opacity: 0 }, locationAttrs)), + }, + + dropzonePrev: { + shape: tagWithAttrs('circle', { + cx: -3*childRadius, + r: 3/5 * childRadius, + class: 'dropzone dropzone-prev' + }), + }, + dropzoneNext: { + shape: tagWithAttrs('circle', { + cx: 3*childRadius, + r: 3/5 * childRadius, + class: 'dropzone dropzone-next' + }), + }, + + adjunct: { + width: 20, + height: 20, + itemPerCol: 3, + gutterSize: 15, + adjunctBox: tagWithAttrs('rect', { + id: (d)=>(`entity-${d._id}`), // TODO should this be adjunct-d._id ? + class: 'node-adjunct adjunct entity', + width: () => bp.config.adjunct.width, + height: () => bp.config.adjunct.height, + transform: 'scale(0)' + }), + }, + + memberspec: { + deltaX: 0, + deltaY: 170, + width: 2*memberspecRadius, + height: 2*memberspecRadius, + radius: memberspecRadius, + iconSize: 40, + circle: tagWithAttrs('circle', { + r: () => bp.config.memberspec.radius, + cx: () => bp.config.memberspec.deltaX, + cy: () => bp.config.memberspec.deltaY, + class: 'node-spec-entity', + 'transform-origin': 0 + }), + icon: tagWithAttrs('image', { + x: () => bp.config.memberspec.deltaX - bp.config.memberspec.iconSize/2, + y: () => bp.config.memberspec.deltaY - bp.config.memberspec.iconSize/2, + width: () => bp.config.memberspec.iconSize, + height: () => bp.config.memberspec.iconSize, + opacity: 0, + class: 'node-spec-image', + 'transform-origin': 0 + }), + }, + + buttongroup: { + lineToGroup: tagWithAttrs('line', { + class: 'link', + x1: 0, + x2: 0, + y1: (d) => height(d)/2, + y2: (d) => height(d)/2 + 30, + }), + circleAsLineSource: tagWithAttrs('circle', { + class: 'connector', + r: 6, + cy: (d) => height(d)/2, + }), + }, + buttonAdd: { + circleAsTarget: tagWithAttrs('circle', { + r: 20, + cy: 100, + }), + imageInGreenCircle: tagWithAttrs('image', { + width: 50, + height: 50, + x: -25, + y: 75, + 'xlink:href': addIcon + }), + }, + }); + + return result; +} diff --git a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.html b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.html index 711d4c11c..97aaa394c 100644 --- a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.html +++ b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.html @@ -31,7 +31,8 @@ - + +
diff --git a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.js b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.js index ffb961674..5bb1c134a 100644 --- a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.js +++ b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.js @@ -24,24 +24,29 @@ import template from './graphical.state.html'; export const graphicalState = { name: 'main.graphical', - url: 'graphical', + url: 'graphical?viewMode', templateProvider: function(composerOverrides) { return composerOverrides.paletteGraphicalStateTemplate || template; }, - controller: ['$scope', '$state', '$filter', 'blueprintService', 'paletteService', graphicalController], + controller: ['$scope', '$state', '$stateParams', '$filter', 'blueprintService', 'paletteService', graphicalController], controllerAs: 'vm', data: { label: 'Graphical Designer' - } + }, }; -function graphicalController($scope, $state, $filter, blueprintService, paletteService) { +function graphicalController($scope, $state, $stateParams, $filter, blueprintService, paletteService) { this.EntityFamily = EntityFamily; this.sections = paletteService.getSections(); this.selectedSection = Object.values(this.sections).find(section => section.type === EntityFamily.ENTITY); $scope.paletteState = {}; // share state among all sections + $scope.useLandscapeMode = true; + if ($stateParams.viewMode) $scope.composerState.viewMode = $stateParams.viewMode; + if (!$scope.composerState.viewMode) $scope.composerState.viewMode = 'mgmt'; + this.viewMode = () => $scope.composerState.viewMode || 'mgmt'; + this.onCanvasSelection = (item) => { $scope.canvasSelectedItem = item; } diff --git a/ui-modules/blueprint-composer/app/views/main/main.controller.js b/ui-modules/blueprint-composer/app/views/main/main.controller.js index d4204f891..1fd08bdf5 100644 --- a/ui-modules/blueprint-composer/app/views/main/main.controller.js +++ b/ui-modules/blueprint-composer/app/views/main/main.controller.js @@ -28,7 +28,7 @@ import {graphicalEditSpecState} from './graphical/edit/spec/edit.spec.controller import bottomSheetTemplate from './bottom-sheet.template.html'; import {ISSUE_LEVEL} from '../../components/util/model/issue.model'; -const layers = [ +const LAYERS = [ { id: 'locations', label: 'Locations', @@ -54,7 +54,8 @@ const layers = [ active: true } ]; -const layerCacheKey = 'blueprint-composer.layers'; + +const COMPOSER_STATE_CACHE_KEY = 'blueprint-composer.state'; export function MainController($scope, $element, $log, $state, $stateParams, brBrandInfo, blueprintService, actionService, catalogApi, applicationApi, brSnackbar, brBottomSheet, edit, yaml, composerOverrides) { $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT); @@ -64,25 +65,34 @@ export function MainController($scope, $element, $log, $state, $stateParams, brB throw 'Cannot supply both YAML source and a catalog item to edit'; } + // uncomment this to enable dev work on landscape mode + // vm.experimental = { enableLandscape: true }; + vm.mode = $state.current; $scope.$on('$stateChangeSuccess', (event, toState)=>{ vm.mode = toState; }); - vm.layers = localStorage && localStorage.getItem(layerCacheKey) !== null ? - JSON.parse(localStorage.getItem(layerCacheKey)) : - layers; - $scope.$watch('vm.layers', ()=> { - vm.layers.forEach(layer => { + $scope.composerState = localStorage && localStorage.getItem(COMPOSER_STATE_CACHE_KEY) !== null ? + JSON.parse(localStorage.getItem(COMPOSER_STATE_CACHE_KEY)) : + { + layers: LAYERS, + viewMode: 'mgmt', + }; + if ($stateParams.viewMode) $scope.composerState.viewMode = $stateParams.viewMode; + $scope.$watch('composerState.layers', ()=> { + $scope.composerState.layers.forEach(layer => { document.querySelectorAll(layer.selector).forEach(node => { angular.element(node).css('display', layer.active ? 'block' : 'none'); }); }); + }, true); + $scope.$watch('composerState', ()=> { if (localStorage) { try { - localStorage.setItem(layerCacheKey, JSON.stringify(vm.layers)); + localStorage.setItem(COMPOSER_STATE_CACHE_KEY, JSON.stringify($scope.composerState)); } catch (ex) { - $log.error('Cannot save layers preferences: ' + ex.message); + $log.error('Cannot save composer state preferences: ' + ex.message); } } }, true); @@ -136,6 +146,12 @@ export function MainController($scope, $element, $log, $state, $stateParams, brB vm.isGraphicalMode = () => { return $state.includes(graphicalState.name); }; + vm.viewMode = () => { + if (!vm.isGraphicalMode()) { + return 'yaml'; + } + return $scope.composerState.viewMode; + }; vm.getAllActions = () => { return actionService.getActions(); diff --git a/ui-modules/blueprint-composer/app/views/main/main.template.html b/ui-modules/blueprint-composer/app/views/main/main.template.html index c1c241b86..7ce123799 100644 --- a/ui-modules/blueprint-composer/app/views/main/main.template.html +++ b/ui-modules/blueprint-composer/app/views/main/main.template.html @@ -21,12 +21,22 @@