[CORD-1943] New service graph
- labels
- enforcing service position
- started documentation
- toggling service instances
- toggle fullscreen
Change-Id: I01b71fb2607fb58711d4624f6b5b6479609b2f4f
(cherry picked from commit 8cf33a3881ee15ebf93094f5b24d757af89ee9e9)
diff --git a/src/app/service-graph/components/coarse/coarse.component.scss b/src/app/service-graph/components/coarse/coarse.component.scss
deleted file mode 100644
index fabfb91..0000000
--- a/src/app/service-graph/components/coarse/coarse.component.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-
-/*
- * Copyright 2017-present Open Networking Foundation
-
- * Licensed 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 './../../../style/vars.scss';
-@import '../../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/variables';
-
-xos-coarse-tenancy-graph {
- display: block;
- // background: $color-accent;
-
- svg {
- height: 400px;
- width: 100%;
- background-color: $panel-filled-bg;
- border-radius: 3px;
- }
-
- .node-group {
-
- .node {
- cursor: pointer;
- }
-
- .node > rect {
- stroke: $color-accent;
- fill: $background-color;
- }
-
- .node > text {
- fill: #fff;
- font-size: 20px;
- }
- }
-
- .link-group {
- line {
- stroke: $color-accent;
- }
- }
- .arrow-marker {
- stroke: $color-accent;
- fill: $color-accent;
- }
-}
\ No newline at end of file
diff --git a/src/app/service-graph/components/coarse/coarse.component.ts b/src/app/service-graph/components/coarse/coarse.component.ts
deleted file mode 100644
index c8e2361..0000000
--- a/src/app/service-graph/components/coarse/coarse.component.ts
+++ /dev/null
@@ -1,265 +0,0 @@
-
-/*
- * Copyright 2017-present Open Networking Foundation
-
- * Licensed 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 './coarse.component.scss';
-import * as d3 from 'd3';
-import * as $ from 'jquery';
-import * as _ from 'lodash';
-import {IXosServiceGraphStore} from '../../services/service-graph.store';
-import {IXosServiceGraph, IXosServiceGraphNode, IXosServiceGraphLink} from '../../interfaces';
-import {XosServiceGraphConfig as config} from '../../graph.config';
-import {IXosDebouncer} from '../../../core/services/helpers/debounce.helper';
-import {Subscription} from 'rxjs';
-import {IXosGraphHelpers} from '../../services/d3-helpers/graph.helpers';
-import {IXosServiceGraphReducer, IXosServiceGraphExtender} from '../../services/graph.extender';
-
-class XosCoarseTenancyGraphCtrl {
-
- static $inject = [
- '$log',
- 'XosServiceGraphStore',
- 'XosDebouncer',
- 'XosGraphHelpers',
- 'XosServiceGraphExtender'
- ];
-
- public graph: IXosServiceGraph;
-
- private CoarseGraphSubscription: Subscription;
- private svg;
- private forceLayout;
- private linkGroup;
- private nodeGroup;
- private textSize = 20;
- private textOffset = this.textSize / 4;
-
- // debounced functions
- private renderGraph;
-
- constructor (
- private $log: ng.ILogService,
- private XosServiceGraphStore: IXosServiceGraphStore,
- private XosDebouncer: IXosDebouncer,
- private XosGraphHelpers: IXosGraphHelpers,
- private XosServiceGraphExtender: IXosServiceGraphExtender
- ) {
-
- }
-
- $onInit() {
- this.renderGraph = this.XosDebouncer.debounce(this._renderGraph, 500, this);
-
- this.CoarseGraphSubscription = this.XosServiceGraphStore.getCoarse()
- .subscribe(
- (graph: IXosServiceGraph) => {
- this.$log.debug(`[XosCoarseTenancyGraph] Coarse Event and render`, graph);
-
- // id there are no data, do nothing
- if (graph.nodes.length === 0) {
- return;
- }
- this.graph = graph;
-
- _.forEach(this.XosServiceGraphExtender.getCoarse(), (r: IXosServiceGraphReducer) => {
- graph = r.reducer(graph);
- });
- this.renderGraph();
- },
- err => {
- this.$log.error(`[XosCoarseTenancyGraph] Coarse Event error`, err);
- });
-
- this.handleSvg();
- this.setupForceLayout();
-
- $(window).on('resize', () => {
- this.setupForceLayout();
- this.renderGraph();
- });
- }
-
- $onDestroy() {
- this.CoarseGraphSubscription.unsubscribe();
- }
-
- private _renderGraph() {
- if (!angular.isDefined(this.graph) || !angular.isDefined(this.graph.nodes) || !angular.isDefined(this.graph.links)) {
- return;
- }
- this.addNodeLinksToForceLayout(this.graph);
- this.renderNodes(this.graph.nodes);
- this.renderLinks(this.graph.links);
- }
-
- private getSvgDimensions(): {width: number, height: number} {
- return {
- width: $('xos-coarse-tenancy-graph svg').width() || 0,
- height: $('xos-coarse-tenancy-graph svg').height() || 0
- };
- }
-
- private handleSvg() {
- this.svg = d3.select('svg');
-
- this.svg.append('svg:defs')
- .selectAll('marker')
- .data(config.markers)
- .enter()
- .append('svg:marker')
- .attr('id', d => d.id)
- .attr('viewBox', d => d.viewBox)
- .attr('refX', d => d.refX)
- .attr('refY', d => d.refY)
- .attr('markerWidth', d => d.width)
- .attr('markerHeight', d => d.height)
- .attr('orient', 'auto')
- .attr('class', d => `${d.id}-marker`)
- .append('svg:path')
- .attr('d', d => d.path);
-
- this.linkGroup = this.svg.append('g')
- .attr({
- class: 'link-group'
- });
-
- this.nodeGroup = this.svg.append('g')
- .attr({
- class: 'node-group'
- });
- }
-
- private collide(n: any) {
- const svgDim = this.getSvgDimensions();
- const x = Math.max(n.width / 2, Math.min(n.x, svgDim.width - (n.width / 2)));
- const y = Math.max(n.height / 2, Math.min(n.y, svgDim.height - (n.height / 2)));
- return `${x}, ${y}`;
- }
-
- private setupForceLayout() {
-
- let svgDim = this.getSvgDimensions();
-
- const tick = () => {
-
- this.nodeGroup.selectAll('g.node')
- .attr({
- transform: d => `translate(${this.collide(d)})`
- });
-
- this.linkGroup.selectAll('line')
- .attr({
- x1: l => l.source.x || 0,
- y1: l => l.source.y || 0,
- x2: l => l.target.x || 0,
- y2: l => l.target.y || 0,
- });
- };
-
- this.forceLayout = d3.layout.force()
- .size([svgDim.width, svgDim.height])
- .linkDistance(config.force.linkDistance)
- .charge(config.force.charge)
- .gravity(config.force.gravity)
- .on('tick', tick);
- }
-
- private addNodeLinksToForceLayout(data: IXosServiceGraph) {
- this.forceLayout
- .nodes(data.nodes)
- .links(data.links)
- .start();
- }
-
- private renderNodes(nodes: IXosServiceGraphNode[]) {
- const self = this;
- const node = this.nodeGroup
- .selectAll('g.node')
- .data(nodes, n => n.id);
-
- const svgDim = this.getSvgDimensions();
- const entering = node.enter()
- .append('g')
- .attr({
- id: n => n.id,
- class: n => `node ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
- transform: `translate(${svgDim.width / 2}, ${svgDim.height / 2})`
- })
- .call(this.forceLayout.drag)
- .on('mousedown', () => {
- d3.event.stopPropagation();
- })
- .on('mouseup', (d) => {
- d.fixed = true;
- });
-
- entering.append('rect')
- .attr({
- rx: config.node.radius,
- ry: config.node.radius
- });
-
- entering.append('text')
- .attr({
- 'text-anchor': 'middle',
- 'transform': `translate(0,${this.textOffset})`
- })
- .text(n => n.label);
- // .text(n => `${n.id} - ${n.label}`);
-
- const existing = node.selectAll('rect');
-
- // resize node > rect as contained text
-
- existing.each(function(d: any) {
- const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
- const rect = d3.select(this);
- rect.attr({
- width: textBBox.width + config.node.padding,
- height: textBBox.height + config.node.padding,
- x: textBBox.x - (config.node.padding / 2),
- y: (textBBox.y + self.textOffset) - (config.node.padding / 2)
- });
- d.width = textBBox.width + config.node.padding;
- d.height = textBBox.height + config.node.padding;
- });
-
- }
-
- private renderLinks(links: IXosServiceGraphLink[]) {
- const link = this.linkGroup
- .selectAll('line')
- .data(links, l => l.id);
-
- const entering = link.enter();
-
- // TODO read classes from graph links
-
- entering.append('line')
- .attr({
- id: n => n.id,
- class: l => `link ${this.XosGraphHelpers.parseElemClasses(l.d3Class)}`,
- 'marker-start': 'url(#arrow)'
- });
- }
-}
-
-export const XosCoarseTenancyGraph: angular.IComponentOptions = {
- template: require('./coarse.component.html'),
- controllerAs: 'vm',
- controller: XosCoarseTenancyGraphCtrl,
-};
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.html b/src/app/service-graph/components/fine-grained/fine-grained.component.html
deleted file mode 100644
index 2e8b92e..0000000
--- a/src/app/service-graph/components/fine-grained/fine-grained.component.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
-<!--
-Copyright 2017-present Open Networking Foundation
-
-Licensed 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.
--->
-
-
-<h1>Fine Grained Tenancy Graph</h1>
-
-<svg>
-</svg>
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.scss b/src/app/service-graph/components/fine-grained/fine-grained.component.scss
deleted file mode 100644
index 29ea116..0000000
--- a/src/app/service-graph/components/fine-grained/fine-grained.component.scss
+++ /dev/null
@@ -1,96 +0,0 @@
-
-/*
- * Copyright 2017-present Open Networking Foundation
-
- * Licensed 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 './../../../style/vars.scss';
-@import '../../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/variables';
-
-xos-fine-grained-tenancy-graph {
- display: block;
- height: 100%;
- // background: $color-accent;
-
- svg {
- height: 90%;
- width: 100%;
- background-color: $panel-filled-bg;
- border-radius: 3px;
- }
-
- .node-group {
-
- .node {
- cursor: pointer;
- }
-
- .node .symbol {
- fill-rule: evenodd;
- stroke: #bbddff;
- stroke-width: 4.0px;
- fill: none;
- }
- .node .symbol-bg {
- fill-rule: evenodd;
- stroke: none;
- fill: $background-color;
- }
-
- .node {
- rect, circle {
- stroke: $color-accent;
- fill: $background-color;
- }
-
- &.network > circle{
- stroke: blue;
- }
-
- &.tenant, &.serviceinstance > rect{
- stroke: green;
- }
-
- &.subscriber > rect,
- &.tenantroot > rect{
- stroke: red;
- }
- }
-
- .node > text {
- fill: #fff;
- font-size: 20px;
- }
- }
-
- .link-group {
- line {
- stroke: $color-accent;
-
- &.ext-service-instance {
- stroke: green;
- }
-
- &.ext-owner {
- stroke: green;
- stroke-dasharray: 5;
- }
- }
- }
- .arrow-marker {
- stroke: $color-accent;
- fill: $color-accent;
- }
-}
\ No newline at end of file
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.ts b/src/app/service-graph/components/fine-grained/fine-grained.component.ts
deleted file mode 100644
index 7d21131..0000000
--- a/src/app/service-graph/components/fine-grained/fine-grained.component.ts
+++ /dev/null
@@ -1,388 +0,0 @@
-
-/*
- * Copyright 2017-present Open Networking Foundation
-
- * Licensed 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 './fine-grained.component.scss';
-import * as d3 from 'd3';
-import * as $ from 'jquery';
-import * as _ from 'lodash';
-import {Subscription} from 'rxjs';
-import {XosServiceGraphConfig as config} from '../../graph.config';
-import {IXosDebouncer} from '../../../core/services/helpers/debounce.helper';
-import {IXosServiceGraph, IXosServiceGraphLink, IXosServiceGraphNode} from '../../interfaces';
-import {IXosSidePanelService} from '../../../core/side-panel/side-panel.service';
-import {IXosGraphHelpers} from '../../services/d3-helpers/graph.helpers';
-import {IXosServiceGraphExtender, IXosServiceGraphReducer} from '../../services/graph.extender';
-import {IXosServiceInstanceGraphStore} from '../../services/service-instance.graph.store';
-import {IXosModeldefsCache} from '../../../datasources/helpers/modeldefs.service';
-
-class XosFineGrainedTenancyGraphCtrl {
- static $inject = [
- '$log',
- 'XosServiceInstanceGraphStore',
- 'XosDebouncer',
- 'XosModelDiscoverer',
- 'XosSidePanel',
- 'XosGraphHelpers',
- 'XosServiceGraphExtender'
- ];
-
- public graph: IXosServiceGraph;
-
- private GraphSubscription: Subscription;
- private svg;
- private forceLayout;
- private linkGroup;
- private nodeGroup;
- private defs;
- private textSize = 20;
- private textOffset = this.textSize / 4;
-
- // debounced functions
- private renderGraph;
-
- constructor(
- private $log: ng.ILogService,
- private XosServiceInstanceGraphStore: IXosServiceInstanceGraphStore,
- private XosDebouncer: IXosDebouncer,
- private XosModeldefsCache: IXosModeldefsCache,
- private XosSidePanel: IXosSidePanelService,
- private XosGraphHelpers: IXosGraphHelpers,
- private XosServiceGraphExtender: IXosServiceGraphExtender
- ) {
- this.handleSvg();
- this.loadDefs();
- this.setupForceLayout();
- this.renderGraph = this.XosDebouncer.debounce(this._renderGraph, 1000, this);
-
- $(window).on('resize', () => {
- this.setupForceLayout();
- this.renderGraph();
- });
-
- this.GraphSubscription = this.XosServiceInstanceGraphStore.get()
- .subscribe(
- (graph) => {
- this.$log.debug(`[XosServiceInstanceGraphStore] Fine-Grained Event and render`, graph);
-
- if (!graph || !graph.nodes || !graph.links) {
- return;
- }
-
- _.forEach(this.XosServiceGraphExtender.getFinegrained(), (r: IXosServiceGraphReducer) => {
- graph = r.reducer(graph);
- });
-
- this.graph = graph;
- this.renderGraph();
- },
- (err) => {
- this.$log.error(`[XosFineGrainedTenancyGraphCtrl] Error: `, err);
- }
- );
- }
-
- $onDestroy() {
- this.GraphSubscription.unsubscribe();
- }
-
- private _renderGraph() {
- if (!angular.isDefined(this.graph) || !angular.isDefined(this.graph.nodes) || !angular.isDefined(this.graph.links)) {
- return;
- }
- this.addNodeLinksToForceLayout(this.graph);
- this.renderNodes(this.graph.nodes);
- this.renderLinks(this.graph.links);
- }
-
- private getSvgDimensions(): {width: number, height: number} {
- return {
- width: $('xos-fine-grained-tenancy-graph svg').width(),
- height: $('xos-fine-grained-tenancy-graph svg').height()
- };
- }
-
- private handleSvg() {
- this.svg = d3.select('svg');
-
- this.defs = this.svg.append('defs');
-
- this.linkGroup = this.svg.append('g')
- .attr({
- class: 'link-group'
- });
-
- this.nodeGroup = this.svg.append('g')
- .attr({
- class: 'node-group'
- });
- }
-
- private loadDefs() {
- const cloud = {
- vbox: '0 0 303.8 185.8',
- path: `M88.6,44.3c31.7-45.5,102.1-66.7,135-3
- M37.8,142.9c-22.5,3.5-60.3-32.4-16.3-64.2
- M101.8,154.2c-15.6,59.7-121.4,18.8-77.3-13
- M194.6,150c-35.4,51.8-85.7,34.3-98.8-9.5
- M274.4,116.4c29.4,73.2-81.9,80.3-87.7,44.3
- M28.5,89.2C3.7,77.4,55.5,4.8,95.3,36.1
- M216.1,28.9C270.9-13,340.8,91,278.4,131.1`,
- bgpath: `M22,78.3C21.5,55.1,62.3,10.2,95.2,36
- h0c31.9-33.4,88.1-50.5,120.6-7.2l0.3,0.2
- C270.9-13,340.8,91,278.4,131.1v-0.5
- c10.5,59.8-86.4,63.7-91.8,30.1h-0.4
- c-30.2,33.6-67.6,24-84.6-6v-0.4
- c-15.6,59.7-121.4,18.8-77.3-13
- l-0.2-.2c-20.2-7.9-38.6-36.5-2.8-62.3Z`
- };
-
- this.defs.append('symbol')
- .attr({ id: 'cloud', viewBox: cloud.vbox })
- .append('path').attr('d', cloud.path);
-
- this.defs.append('symbol')
- .attr({ id: 'cloud_bg', viewBox: cloud.vbox })
- .append('path').attr('d', cloud.bgpath);
- }
-
- private setupForceLayout() {
- this.$log.debug(`[XosFineGrainedTenancyGraphCtrl] Setup Force Layout`);
- const tick = () => {
- this.nodeGroup.selectAll('g.node')
- .attr({
- transform: d => `translate(${d.x}, ${d.y})`
- });
-
- this.linkGroup.selectAll('line')
- .attr({
- x1: l => l.source.x || 0,
- y1: l => l.source.y || 0,
- x2: l => l.target.x || 0,
- y2: l => l.target.y || 0,
- });
- };
- const getLinkStrenght = (l: IXosServiceGraphLink) => {
- return 1;
- };
- const svgDim = this.getSvgDimensions();
- this.forceLayout = d3.layout.force()
- .size([svgDim.width, svgDim.height])
- .linkDistance(config.force.linkDistance)
- .linkStrength(l => getLinkStrenght(l))
- .charge(config.force.charge)
- .gravity(config.force.gravity)
- .on('tick', tick);
- }
-
- private addNodeLinksToForceLayout(data: IXosServiceGraph) {
- this.forceLayout
- .nodes(data.nodes)
- .links(data.links)
- .start();
- }
-
- private renderServiceNodes(nodes: any) {
-
- const self = this;
-
- nodes.append('rect')
- .attr({
- rx: config.node.radius,
- ry: config.node.radius
- });
-
- nodes.append('text')
- .attr({
- 'text-anchor': 'middle',
- 'transform': `translate(0,${this.textOffset})`
- })
- .text(n => n.label);
- // .text(n => `${n.id} - ${n.label}`);
-
- const existing = nodes.selectAll('rect');
-
- // resize node > rect as contained text
- existing.each(function() {
- const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
- const rect = d3.select(this);
- rect.attr({
- width: textBBox.width + config.node.padding,
- height: textBBox.height + config.node.padding,
- x: textBBox.x - (config.node.padding / 2),
- y: (textBBox.y + self.textOffset) - (config.node.padding / 2)
- });
- });
- }
-
- private renderTenantNodes(nodes: any) {
- nodes.append('rect')
- .attr({
- width: 40,
- height: 40,
- x: -20,
- y: -20,
- transform: `rotate(45)`
- });
-
- nodes.append('text')
- .attr({
- 'text-anchor': 'middle',
- 'transform': `translate(0,${this.textOffset})`
- })
- .text(n => n.label);
- }
-
- private renderNetworkNodes(nodes: any) {
- const self = this;
-
- nodes.append('use')
- .attr({
- class: 'symbol-bg',
- 'xlink:href': '#cloud_bg'
- });
-
- nodes.append('use')
- .attr({
- class: 'symbol',
- 'xlink:href': '#cloud'
- });
-
- nodes.append('text')
- .attr({
- 'text-anchor': 'middle',
- 'transform': `translate(0,${this.textOffset})`
- })
- .text(n => n.label);
-
- const existing = nodes.selectAll('use');
-
- // resize node > rect as contained text
- existing.each(function() {
- const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
- const useElem = d3.select(this);
- const w = textBBox.width + config.node.padding * 2;
- const h = w;
- const xoff = -(w / 2);
- const yoff = -(h / 2);
-
- useElem.attr({
- width: w,
- height: h,
- transform: 'translate(' + xoff + ',' + yoff + ')'
- });
- });
- }
-
- private renderSubscriberNodes(nodes: any) {
- const self = this;
- nodes.append('rect');
-
- nodes.append('text')
- .attr({
- 'text-anchor': 'middle',
- 'transform': `translate(0,${this.textOffset})`
- })
- .text(n => n.label);
-
- const existing = nodes.selectAll('rect');
-
- // resize node > rect as contained text
- existing.each(function() {
- const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
- const rect = d3.select(this);
- rect.attr({
- width: textBBox.width + config.node.padding,
- height: textBBox.height + config.node.padding,
- x: textBBox.x - (config.node.padding / 2),
- y: (textBBox.y + self.textOffset) - (config.node.padding / 2)
- });
- });
- }
-
- private renderNodes(nodes: IXosServiceGraphNode[]) {
- const node = this.nodeGroup
- .selectAll('g.node')
- .data(nodes, n => n.id);
-
- let mouseEventsTimer, selectedModel;
- const svgDim = this.getSvgDimensions();
- const hStep = svgDim.width / (nodes.length - 1);
- const vStep = svgDim.height / (nodes.length - 1);
- const entering = node.enter()
- .append('g')
- .attr({
- id: n => n.id,
- class: n => `node ${n.type} ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
- transform: (n, i) => `translate(${hStep * i}, ${vStep * i})`
- })
- .call(this.forceLayout.drag)
- .on('mousedown', () => {
- mouseEventsTimer = new Date().getTime();
- d3.event.stopPropagation();
- })
- .on('mouseup', (n) => {
- mouseEventsTimer = new Date().getTime() - mouseEventsTimer;
- n.fixed = true;
- })
- .on('click', (n: IXosServiceGraphNode) => {
- if (mouseEventsTimer > 100) {
- // it is a drag
- return;
- }
- if (selectedModel === n.id) {
- // this model is already selected, so close the panel
- this.XosSidePanel.removeInjectedComponents();
- selectedModel = null;
- return;
- }
- selectedModel = n.id;
- const modelName = n.model['class_names'].split(',')[0];
- const formConfig = this.XosModeldefsCache.get(modelName).formCfg;
- const model = angular.copy(n.model);
- delete model.d3Id;
- this.XosSidePanel.injectComponent('xosForm', {config: formConfig, ngModel: model});
- });
-
- this.renderServiceNodes(entering.filter('.service'));
- this.renderTenantNodes(entering.filter('.serviceinstance'));
- this.renderNetworkNodes(entering.filter('.network'));
- this.renderSubscriberNodes(entering.filter('.subscriber'));
- // this.renderSubscriberNodes(entering.filter('.tenantroot'));
- }
-
- private renderLinks(links: IXosServiceGraphLink[]) {
- const link = this.linkGroup
- .selectAll('line')
- .data(links, l => l.id);
-
- const entering = link.enter();
-
- entering.append('line')
- .attr({
- id: n => n.id,
- class: n => `link ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
- 'marker-start': 'url(#arrow)'
- });
- }
-}
-
-export const XosFineGrainedTenancyGraph: angular.IComponentOptions = {
- template: require('./fine-grained.component.html'),
- controllerAs: 'vm',
- controller: XosFineGrainedTenancyGraphCtrl,
-};
diff --git a/src/app/service-graph/components/coarse/coarse.component.html b/src/app/service-graph/components/graph/graph.component.html
similarity index 69%
rename from src/app/service-graph/components/coarse/coarse.component.html
rename to src/app/service-graph/components/graph/graph.component.html
index c0bb4e5..6510a9f 100644
--- a/src/app/service-graph/components/coarse/coarse.component.html
+++ b/src/app/service-graph/components/graph/graph.component.html
@@ -1,4 +1,3 @@
-
<!--
Copyright 2017-present Open Networking Foundation
@@ -15,8 +14,12 @@
limitations under the License.
-->
+<h1>Service Graph</h1>
-<h1>Service Dependency Graph</h1>
-
-<svg>
-</svg>
+<div class="graph-container">
+ <div class="loader-container" ng-if="vm.loader">
+ <div class="loader"></div>
+ </div>
+ <a ng-click="vm.closeFullscreen()" class="close-btn"><i class="fa fa-times"></i></a>
+ <svg></svg>
+</div>
diff --git a/src/app/service-graph/components/graph/graph.component.scss b/src/app/service-graph/components/graph/graph.component.scss
new file mode 100644
index 0000000..40b6f11
--- /dev/null
+++ b/src/app/service-graph/components/graph/graph.component.scss
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 './../../../style/vars.scss';
+@import '../../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/variables';
+
+$svg-size: 600px;
+
+xos-service-graph {
+ display: block;
+ // background: $color-accent;
+
+ .graph-container {
+ position: relative;
+
+ .loader-container {
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: $svg-size;
+ width: 100%;
+ }
+ }
+
+ svg {
+ height: $svg-size;
+ width: 100%;
+ background-color: $panel-filled-bg;
+ border-radius: 3px;
+ }
+
+ .close-btn {
+ display: none;
+ }
+
+ .fullscreen {
+ svg {
+ z-index: 1040;
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ top: 0;
+ left: 0;
+ background-color: $background-color;
+ transition: all .5s;
+ }
+
+ .close-btn {
+ cursor: pointer;
+ display: block;
+ position: fixed;
+ top: 10px;
+ right: 10px;
+ z-index: 1041;
+ }
+ }
+
+ .node-group {
+
+ .node {
+ cursor: pointer;
+ }
+
+ .node.service {
+ > rect {
+ stroke: $color-accent;
+ fill: $background-color;
+ }
+ > .icon {
+ fill: $color-accent;
+ }
+ }
+
+ .node.serviceinstance {
+ > rect {
+ stroke: green;
+ fill: $background-color;
+ }
+ > .icon {
+ fill: green;
+ }
+ }
+
+ .node {
+ >.label {
+ >text {
+ fill: #fff;
+ }
+ >rect {
+ fill: $background-color;
+ stroke: #fff;
+ }
+ }
+ }
+ }
+
+ .link-group {
+ line {
+ stroke: $color-accent;
+ }
+
+ line.ownership {
+ stroke: green;
+ stroke-dasharray: 5;
+ }
+
+ line.serviceinstancelink {
+ stroke: green;
+ }
+ }
+ .arrow-marker {
+ stroke: $color-accent;
+ fill: $color-accent;
+ }
+}
\ No newline at end of file
diff --git a/src/app/service-graph/components/graph/graph.component.ts b/src/app/service-graph/components/graph/graph.component.ts
new file mode 100644
index 0000000..5769b6f
--- /dev/null
+++ b/src/app/service-graph/components/graph/graph.component.ts
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 './graph.component.scss';
+
+import * as d3 from 'd3';
+import * as $ from 'jquery';
+
+import {IXosGraphStore} from '../../services/graph.store';
+import {Subscription} from 'rxjs/Subscription';
+import {XosServiceGraphConfig as config} from '../../graph.config';
+import {IXosGraphHelpers} from '../../services/d3-helpers/graph-elements.helpers';
+import {IXosServiceGraphIcons} from '../../services/d3-helpers/graph-icons.service';
+import {IXosNodePositioner} from '../../services/node-positioner.service';
+import {IXosNodeRenderer} from '../../services/renderer/node.renderer';
+import {IXosSgNode} from '../../interfaces';
+import {IXosGraphConfig} from '../../services/graph.config';
+
+class XosServiceGraphCtrl {
+ static $inject = [
+ '$log',
+ '$scope',
+ 'XosGraphStore',
+ 'XosGraphHelpers',
+ 'XosServiceGraphIcons',
+ 'XosNodePositioner',
+ 'XosNodeRenderer',
+ 'XosGraphConfig'
+ ];
+
+ public loader: boolean = true;
+
+ private GraphSubscription: Subscription;
+ private graph: any; // this is the Graph instance
+
+ // graph element
+ private svg;
+ private linkGroup;
+ private nodeGroup;
+ private forceLayout;
+
+ constructor (
+ private $log: ng.ILogService,
+ private $scope: ng.IScope,
+ private XosGraphStore: IXosGraphStore,
+ private XosGraphHelpers: IXosGraphHelpers,
+ private XosServiceGraphIcons: IXosServiceGraphIcons,
+ private XosNodePositioner: IXosNodePositioner,
+ private XosNodeRenderer: IXosNodeRenderer,
+ private XosGraphConfig: IXosGraphConfig
+ ) {
+ this.$log.info('[XosServiceGraph] Component setup');
+
+ this.XosGraphConfig.setupKeyboardShortcuts();
+
+ this.setupSvg();
+ this.setupForceLayout();
+
+ this.GraphSubscription = this.XosGraphStore.get()
+ .subscribe(
+ graph => {
+ this.graph = graph;
+ if (this.graph.nodes().length > 0) {
+ this.loader = false;
+ this.renderGraph(this.graph);
+ }
+ },
+ error => {
+ this.$log.error('[XosServiceGraph] XosGraphStore observable error: ', error);
+ }
+ );
+
+ this.$scope.$on('xos.sg.update', () => {
+ this.$log.info(`[XosServiceGraph] Received event: xos.sg.update`);
+ this.renderGraph(this.graph);
+ });
+ }
+
+ $onDestroy() {
+ this.GraphSubscription.unsubscribe();
+ }
+
+ public closeFullscreen() {
+ this.XosGraphConfig.toggleFullscreen();
+ }
+
+ private setupSvg() {
+ this.svg = d3.select('xos-service-graph svg');
+
+ this.linkGroup = this.svg.append('g')
+ .attr({
+ class: 'link-group'
+ });
+
+ this.nodeGroup = this.svg.append('g')
+ .attr({
+ class: 'node-group'
+ });
+ }
+
+ private setupForceLayout() {
+ this.$log.debug(`[XosServiceGraph] Setup Force Layout`);
+ const tick = () => {
+ this.nodeGroup.selectAll('g.node')
+ .attr({
+ transform: d => `translate(${d.x}, ${d.y})`
+ });
+
+ this.linkGroup.selectAll('line')
+ .attr({
+ x1: l => l.source.x || 0,
+ y1: l => l.source.y || 0,
+ x2: l => l.target.x || 0,
+ y2: l => l.target.y || 0,
+ });
+ };
+
+ const svgDim = this.getSvgDimensions();
+
+ this.forceLayout =
+ d3.layout.force()
+ .size([svgDim.width, svgDim.height])
+ .on('tick', tick);
+ }
+
+ private getSvgDimensions(): {width: number, height: number} {
+ return {
+ width: $('xos-service-graph svg').width(),
+ height: $('xos-service-graph svg').height()
+ };
+ }
+
+ private renderGraph(graph: any) {
+ let nodes: IXosSgNode[] = this.XosGraphStore.nodesFromGraph(graph);
+ let links = this.XosGraphStore.linksFromGraph(graph);
+ const svgDim = this.getSvgDimensions();
+
+ this.XosNodePositioner.positionNodes(svgDim, nodes)
+ .then((nodes: IXosSgNode[]) => {
+
+ this.forceLayout
+ .nodes(nodes)
+ .links(links)
+ .size([svgDim.width, svgDim.height])
+ .linkDistance(config.force.linkDistance)
+ .charge(config.force.charge)
+ .gravity(config.force.gravity)
+ .start();
+
+ // render nodes
+ this.XosNodeRenderer.renderNodes(this.forceLayout, this.nodeGroup, nodes);
+ this.renderLinks(links);
+ });
+ }
+
+ private renderLinks(links: any[]) {
+
+ const link = this.linkGroup
+ .selectAll('line')
+ .data(links, l => l.id);
+
+ const entering = link.enter();
+
+ entering.append('line')
+ .attr({
+ id: n => n.id,
+ class: n => n.type
+ });
+
+ link.exit().remove();
+ }
+
+}
+
+export const XosServiceGraph: angular.IComponentOptions = {
+ template: require('./graph.component.html'),
+ controllerAs: 'vm',
+ controller: XosServiceGraphCtrl,
+};
diff --git a/src/app/service-graph/graph.config.ts b/src/app/service-graph/graph.config.ts
index abfd415..13a8939 100644
--- a/src/app/service-graph/graph.config.ts
+++ b/src/app/service-graph/graph.config.ts
@@ -27,6 +27,7 @@
}
export interface IXosServiceGraphConfig {
+ duration: number;
force: {
linkDistance: number;
charge: number;
@@ -35,11 +36,13 @@
node: {
padding: number;
radius: number;
+ text: number;
};
markers: ISvgMarker[];
}
export const XosServiceGraphConfig: IXosServiceGraphConfig = {
+ duration: 750,
force: {
linkDistance: 80,
charge: -60,
@@ -47,7 +50,8 @@
},
node: {
padding: 10,
- radius: 2
+ radius: 2,
+ text: 14
},
markers: [
{
diff --git a/src/app/service-graph/index.ts b/src/app/service-graph/index.ts
index 2acb259..770862d 100644
--- a/src/app/service-graph/index.ts
+++ b/src/app/service-graph/index.ts
@@ -1,4 +1,3 @@
-
/*
* Copyright 2017-present Open Networking Foundation
@@ -15,32 +14,30 @@
* limitations under the License.
*/
-
-import {xosDataSources} from '../datasources/index';
-import {XosServiceGraphStore} from './services/service-graph.store';
-import {xosCore} from '../core/index';
-import {XosCoarseTenancyGraph} from './components/coarse/coarse.component';
-import {XosFineGrainedTenancyGraph} from './components/fine-grained/fine-grained.component';
import {XosServiceGraphExtender, IXosServiceGraphExtender} from './services/graph.extender';
-import {XosGraphHelpers} from './services/d3-helpers/graph.helpers';
-import {XosServiceInstanceGraphStore} from './services/service-instance.graph.store';
+import {XosGraphHelpers} from './services/d3-helpers/graph-elements.helpers';
+import {XosServiceGraph} from './components/graph/graph.component';
+import {XosGraphStore} from './services/graph.store';
+import {XosServiceGraphIcons} from './services/d3-helpers/graph-icons.service';
+import {XosNodePositioner} from './services/node-positioner.service';
+import {XosGraphConfig} from './services/graph.config';
+import {XosNodeRenderer} from './services/renderer/node.renderer';
+
export const xosServiceGraph = 'xosServiceGraph';
angular
- .module(xosServiceGraph, [xosDataSources, xosCore])
- .service('XosServiceGraphStore', XosServiceGraphStore)
- .service('XosServiceInstanceGraphStore', XosServiceInstanceGraphStore)
+ .module(xosServiceGraph, [])
.service('XosServiceGraphExtender', XosServiceGraphExtender)
.service('XosGraphHelpers', XosGraphHelpers)
- .component('xosCoarseTenancyGraph', XosCoarseTenancyGraph)
- .component('xosFineGrainedTenancyGraph', XosFineGrainedTenancyGraph)
- .config(($stateProvider) => {
- $stateProvider
- .state('xos.fine-grained-graph', {
- url: 'tenancy-graph',
- component: 'xosFineGrainedTenancyGraph',
- });
- })
- .run(($log: ng.ILogService, XosServiceGraphExtender: IXosServiceGraphExtender) => {
+ .service('XosGraphStore', XosGraphStore)
+ .service('XosServiceGraphIcons', XosServiceGraphIcons)
+ .service('XosNodePositioner', XosNodePositioner)
+ .service('XosGraphConfig', XosGraphConfig)
+ .service('XosNodeRenderer', XosNodeRenderer)
+ .component('xosServiceGraph', XosServiceGraph)
+ .run((
+ $log: ng.ILogService,
+ XosServiceGraphExtender: IXosServiceGraphExtender
+ ) => {
$log.info(`[${xosServiceGraph}] Module Setup`);
});
diff --git a/src/app/service-graph/interfaces.ts b/src/app/service-graph/interfaces.ts
index af55c54..861ba75 100644
--- a/src/app/service-graph/interfaces.ts
+++ b/src/app/service-graph/interfaces.ts
@@ -1,4 +1,3 @@
-
/*
* Copyright 2017-present Open Networking Foundation
@@ -15,95 +14,42 @@
* limitations under the License.
*/
-
interface Id3Element {
- d3Class?: string;
- d3Id?: string;
-}
-
-export interface IXosServiceModel {
- id: number;
- d3Id?: string;
- backend_status: string;
- kind: string;
- name: string;
- class_names: string;
- service_specific_attributes: string; // this is json stringified
-}
-
-export interface IXosTenantModel extends Id3Element {
- id: number;
- d3Id?: string;
- backend_status: string;
- kind: string;
-
- // source
- provider_service_id: number;
-
- // destination
- subscriber_service_id: number;
- subscriber_tenant_id: number;
- subscriber_root_id: number;
- subscriber_network_id: number;
-
- subscriber_user_id: number;
-
- // extra informations
- service_specific_id: string;
- service_specific_attribute: string;
- connect_method: string;
-
- // reverse of subscriber tenants
- subscribed_tenants_ids: number[];
-}
-
-export interface IXosCoarseGraphData {
- services: IXosServiceModel[];
- servicedependencies: any[];
-}
-
-// TODO outdated, remove
-export interface IXosFineGrainedGraphData extends IXosCoarseGraphData {
- tenants: IXosTenantModel[];
- subscribers: any[];
- networks: any[];
-}
-
-export interface IXosServiceInstanceGraphData {
- serviceGraph: IXosServiceGraph;
- serviceInstances: any[];
- serviceInstanceLinks: any[];
- networks: any[];
-}
-
-export interface IXosServiceGraphNodeBadge {
- type: 'info'|'success'|'warning'|'danger';
- text: string;
-}
-
-export interface IXosServiceGraphNode extends Id3Element {
- id: number | string;
- label: string;
- x?: number;
- y?: number;
- px?: number;
- py?: number;
- width?: number;
- height?: number;
+ x: number;
+ y: number;
fixed?: boolean;
- badge?: IXosServiceGraphNodeBadge; // TODO implement badges
- model: IXosServiceModel;
- type: 'service' | 'tenant' | 'network' | 'subscriber';
}
-export interface IXosServiceGraphLink extends Id3Element {
+export interface IXosSgNode extends Id3Element {
id: string;
+ data: any; // this can be a Service, ServiceInstance or Instance
+
+ // do we need those?
+ type: string;
+ d3Class?: string;
+}
+
+export interface IXosSgLink {
+ id: string;
+ type: string;
source: number;
target: number;
- model: IXosTenantModel;
+ data: any; // this can be a ServiceDependency, ServiceInstanceLink or a representation of ServiceInstance.owner_id
}
-export interface IXosServiceGraph {
- nodes: IXosServiceGraphNode[];
- links: IXosServiceGraphLink[];
+export interface IXosSgConfig {
+ labels: boolean;
+}
+
+export interface IXosBaseModel {
+ id: number;
+ class_names: string;
+ name?: string;
+ [x: string]: any; // allow extra properties
+}
+
+export interface IXosOwnershipLink {
+ service: number;
+ service_instance: number;
+ type: 'ownership';
}
diff --git a/src/app/service-graph/services/d3-helpers/graph-elements.helpers.ts b/src/app/service-graph/services/d3-helpers/graph-elements.helpers.ts
new file mode 100644
index 0000000..bbacbdc
--- /dev/null
+++ b/src/app/service-graph/services/d3-helpers/graph-elements.helpers.ts
@@ -0,0 +1,67 @@
+
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 * as d3 from 'd3';
+import {XosServiceGraphConfig as config} from '../../graph.config';
+
+export interface Id3BBox {
+ x?: number;
+ y?: number;
+ width: number;
+ height: number;
+}
+
+export interface IXosGraphHelpers {
+ parseElemClasses (classes: string): string;
+ getBBox(context: any): Id3BBox;
+ getSiblingTextBBox(contex: any /* D3 this */): Id3BBox;
+ getSiblingIconBBox(contex: any /* D3 this */): Id3BBox;
+ getSiblingBBox(contex: any): Id3BBox;
+}
+
+export class XosGraphHelpers implements IXosGraphHelpers {
+ public parseElemClasses (classes: string): string {
+ return angular.isDefined(classes) ? classes.split(' ')
+ .map(c => `ext-${c}`)
+ .join(' ') : '';
+ }
+
+ public getBBox(context: any): Id3BBox {
+ return d3.select(context).node().getBBox();
+ }
+
+ public getSiblingTextBBox(contex: any): Id3BBox {
+ const text: d3.Selection<any> = d3.select(contex.parentNode).select('text');
+ return text.empty() ? {width: 0, height: 0} : text.node().getBBox();
+ }
+
+ public getSiblingIconBBox(contex: any): Id3BBox {
+ return d3.select(contex.parentNode).select('path').node().getBBox();
+ }
+
+ public getSiblingBBox(contex: any): Id3BBox {
+ // NOTE consider that inside a node we can have 1 text and 1 icon
+ const textBBox: Id3BBox = this.getSiblingTextBBox(contex);
+ const iconBBox: Id3BBox = this.getSiblingIconBBox(contex);
+
+ return {
+ width: iconBBox.width + (textBBox.width ? config.node.padding + textBBox.width : 0),
+ height: iconBBox.height
+ };
+ }
+}
diff --git a/src/app/service-graph/services/d3-helpers/graph-icons.service.ts b/src/app/service-graph/services/d3-helpers/graph-icons.service.ts
new file mode 100644
index 0000000..9baeb57
--- /dev/null
+++ b/src/app/service-graph/services/d3-helpers/graph-icons.service.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 * as _ from 'lodash';
+
+export interface IXosServiceGraphIcon {
+ name: string;
+ path: string;
+ transform: string; // do we need it??
+}
+
+const icons: IXosServiceGraphIcon[] = [
+ {
+ name: 'service',
+ path: 'M.08,15.51V10.7h0L1.5,10.5,3,10.29s0,0,.06,0A10.13,10.13,0,0,1,3.9,8.05a.11.11,0,0,0,0-.12L2.24,5.66s0-.08,0-.12Q3.91,3.91,5.56,2.26a.08.08,0,0,1,.1,0L7.87,3.83a.21.21,0,0,0,.25,0A10,10,0,0,1,10.17,3s0,0,.05-.05l.09-.56c.11-.65.22-1.29.32-1.94a2.57,2.57,0,0,0,.06-.4h4.78a.11.11,0,0,0,0,.08c.15.92.31,1.84.46,2.76a.07.07,0,0,0,.06.06,11.76,11.76,0,0,1,2.11.88.09.09,0,0,0,.12,0l2.34-1.62a.05.05,0,0,1,.08,0l3.28,3.36a.05.05,0,0,1,0,.08L23.66,6c-.47.63-.93,1.27-1.39,1.91,0,0,0,.06,0,.1l.14.25a9.19,9.19,0,0,1,.74,1.88s0,.06.06.06l.8.13,1.76.3a1.17,1.17,0,0,0,.32,0v4.81H26l-1.41.22-1.26.19c-.14,0-.14,0-.18.15v0a10.33,10.33,0,0,1-.84,2,.1.1,0,0,0,0,.12L24,20.56c0,.05,0,.07,0,.11-1.09,1.1-2.18,2.19-3.26,3.29,0,0-.06,0-.09,0L18.3,22.31a.11.11,0,0,0-.16,0,11,11,0,0,1-2.07.86.11.11,0,0,0-.08.09c-.05.33-.11.66-.16,1-.1.59-.2,1.18-.29,1.77,0,.06,0,.07-.09.07H10.83c-.07,0-.09,0-.1-.08-.15-.91-.31-1.82-.46-2.72,0,0,0-.05,0-.06s-.2,0-.3-.09a10.59,10.59,0,0,1-1.87-.79.09.09,0,0,0-.11,0L5.62,24s-.08,0-.12,0L2.24,20.72s0,0,0-.1c.55-.78,1.1-1.56,1.66-2.33a.11.11,0,0,0,0-.12A9.87,9.87,0,0,1,3,16S3,16,3,16l-.72-.12-1.76-.3C.35,15.55.22,15.52.08,15.51Zm13,2.61a5,5,0,1,0-5-5A5,5,0,0,0,13.08,18.12Z',
+ transform: 'translate(-0.08 -0.04)'
+ },
+ {
+ name: 'serviceinstance',
+ path: 'M11.87,19.94v5.47c0,.61-.18.72-.69.42l-9.6-5.55a.62.62,0,0,1-.32-.64c0-3.64,0-7.29,0-10.94,0-.7.16-.79.79-.42,2.89,1.67,5.77,3.39,8.69,5a1.81,1.81,0,0,1,1.14,2C11.8,16.81,11.87,18.38,11.87,19.94Z\n' +
+ 'M13,0a1,1,0,0,1,.53.2l9.4,5.45c.54.32.54.45,0,.78C19.78,8.2,16.7,10,13.63,11.74a1.12,1.12,0,0,1-1.24,0C9.28,9.92,6.15,8.12,3,6.31c-.55-.31-.55-.46,0-.78L12.45.19A.86.86,0,0,1,13,0Z\n' +
+ 'M24.73,14.16c0,1.81,0,3.61,0,5.42a.8.8,0,0,1-.46.79l-9.36,5.41c-.64.38-.79.29-.79-.47,0-3.53,0-7.07,0-10.6a.92.92,0,0,1,.52-.94q4.63-2.65,9.26-5.35l.24-.14c.43-.2.58-.11.58.36Z',
+ transform: ''
+ }
+];
+
+export interface IXosServiceGraphIcons {
+ get(icon: string): IXosServiceGraphIcon;
+}
+
+export class XosServiceGraphIcons implements IXosServiceGraphIcons {
+ public get(icon: string): IXosServiceGraphIcon {
+ return _.find(icons, {name: icon});
+ }
+}
diff --git a/src/app/service-graph/services/d3-helpers/graph.helpers.ts b/src/app/service-graph/services/d3-helpers/graph.helpers.ts
deleted file mode 100644
index 15988cd..0000000
--- a/src/app/service-graph/services/d3-helpers/graph.helpers.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-
-/*
- * Copyright 2017-present Open Networking Foundation
-
- * Licensed 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 * as d3 from 'd3';
-
-export interface Id3BBox {
- x: number;
- y: number;
- width: number;
- height: number;
-}
-
-export interface IXosGraphHelpers {
- parseElemClasses (classes: string): string;
- getSiblingTextBBox(contex: any /* D3 this */): Id3BBox;
-}
-
-export class XosGraphHelpers implements IXosGraphHelpers {
- public parseElemClasses (classes: string): string {
- return classes ? classes.split(' ')
- .map(c => `ext-${c}`)
- .join(' ') : '';
- }
-
- public getSiblingTextBBox(contex: any): Id3BBox {
- return d3.select(contex.parentNode).select('text').node().getBBox();
- }
-}
diff --git a/src/app/service-graph/services/graph.config.ts b/src/app/service-graph/services/graph.config.ts
new file mode 100644
index 0000000..6c58c3a
--- /dev/null
+++ b/src/app/service-graph/services/graph.config.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 * as $ from 'jquery';
+import {IXosKeyboardShortcutService} from '../../core/services/keyboard-shortcut';
+import {IXosSgConfig} from '../interfaces';
+import {IXosGraphStore} from './graph.store';
+
+export interface IXosGraphConfig {
+ getUserConfig(): IXosSgConfig;
+ setupKeyboardShortcuts(): void;
+ toggleFullscreen(): void;
+}
+
+export class XosGraphConfig {
+
+ static $inject = [
+ '$log',
+ '$cookies',
+ '$rootScope',
+ '$timeout',
+ 'XosKeyboardShortcut',
+ 'XosGraphStore'
+ ];
+
+ private defaultUserConfig: IXosSgConfig = {
+ labels: false
+ };
+
+ private userConfig: IXosSgConfig = this.defaultUserConfig;
+
+ constructor (
+ private $log: ng.ILogService,
+ private $cookies: ng.cookies.ICookiesService,
+ private $rootScope: ng.IRootScopeService,
+ private $timeout: ng.ITimeoutService,
+ private XosKeyboardShortcut: IXosKeyboardShortcutService,
+ private XosGraphStore: IXosGraphStore
+ ) {
+ this.userConfig = this.getUserConfig();
+
+
+ }
+
+ public setupKeyboardShortcuts() {
+
+ this.$log.info(`[XosGraphConfig] Setting up keyboard shortcuts`);
+
+ // Setup keyboard shortcuts
+ this.XosKeyboardShortcut.registerKeyBinding({
+ key: 'f',
+ modifiers: ['shift'],
+ cb: () => {
+ this.toggleFullscreen();
+ },
+ label: 'f',
+ description: 'Toggle graph fullscreen'
+ });
+
+ this.XosKeyboardShortcut.registerKeyBinding({
+ key: 's',
+ modifiers: ['shift'],
+ cb: () => {
+ // NOTE anytime the graph change the observable is updated,
+ // no need to manually retrigger here
+ this.XosGraphStore.toggleServiceInstances();
+ },
+ label: 's',
+ description: 'Toggle ServiceInstances'
+ });
+ }
+
+ public toggleFullscreen() {
+ $('.graph-container').toggleClass('fullscreen');
+ this.$timeout(() => {
+ // NOTE wait for the CSS transition to complete before repositioning
+ this.$rootScope.$broadcast('xos.sg.update');
+ }, 500);
+ }
+
+ public getUserConfig(): IXosSgConfig {
+ let config = this.$cookies.get('xos-service-graph-user-config');
+ if (!config || config.length === 0) {
+ this.$cookies.put('xos-service-graph-user-config', JSON.stringify(this.defaultUserConfig));
+ config = this.$cookies.get('xos-service-graph-user-config');
+ }
+ return JSON.parse(config);
+ }
+}
diff --git a/src/app/service-graph/services/graph.extender.ts b/src/app/service-graph/services/graph.extender.ts
index a601e87..467e34b 100644
--- a/src/app/service-graph/services/graph.extender.ts
+++ b/src/app/service-graph/services/graph.extender.ts
@@ -16,7 +16,9 @@
*/
-import {IXosServiceGraph} from '../interfaces';
+
+
+import {Graph} from 'graphlib';
export interface IXosServiceGraphReducers {
coarse: IXosServiceGraphReducer[];
@@ -29,7 +31,7 @@
}
export interface IXosServiceGraphReducerFn {
- (graph: IXosServiceGraph): IXosServiceGraph;
+ (graph: Graph): Graph;
}
export interface IXosServiceGraphExtender {
diff --git a/src/app/service-graph/services/graph.store.spec.ts b/src/app/service-graph/services/graph.store.spec.ts
new file mode 100644
index 0000000..a41809c
--- /dev/null
+++ b/src/app/service-graph/services/graph.store.spec.ts
@@ -0,0 +1,334 @@
+
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 * as _ from 'lodash';
+import * as angular from 'angular';
+import 'angular-mocks';
+import {IXosGraphStore, XosGraphStore} from './graph.store';
+import {Subject} from 'rxjs/Subject';
+import {Graph} from 'graphlib';
+import {XosDebouncer} from '../../core/services/helpers/debounce.helper';
+
+interface ITestXosGraphStore extends IXosGraphStore {
+
+ // state
+ serviceGraph: Graph;
+ serviceInstanceShown: boolean;
+
+ // private methods
+ getNodeId: any;
+ getModelType: any;
+ addNode: any;
+ addEdge: any;
+ nodesFromGraph: any;
+ toggleServiceInstances: any;
+
+ // observables
+ ServiceInstanceSubscription: any;
+ ServiceInstanceLinkSubscription: any;
+}
+
+let service: ITestXosGraphStore;
+
+let scope: ng.IRootScopeService;
+
+const services = [
+ {
+ id: 1,
+ class_names: 'Service,XOSBase',
+ name: 'Service 1'
+ },
+ {
+ id: 2,
+ class_names: 'Service,XOSBase',
+ name: 'Service 2'
+ }
+];
+
+const servicedependencies = [
+ {
+ id: 1,
+ class_names: 'ServiceDependency,XOSBase',
+ provider_service_id: 2,
+ subscriber_service_id: 1
+ }
+];
+
+const serviceInstances = [
+ {
+ id: 1,
+ class_names: 'ServiceInstance,XOSBase',
+ name: 'ServiceInstance 1',
+ owner_id: 1
+ },
+ {
+ id: 2,
+ class_names: 'ServiceInstance,XOSBase',
+ name: 'ServiceInstance 2',
+ owner_id: 2
+ }
+];
+
+const serviceInstanceLinks = [
+ {
+ id: 1,
+ class_names: 'ServiceInstanceLink,XOSBase',
+ provider_service_instance_id: 1,
+ subscriber_service_instance_id: 2,
+ }
+];
+
+const subject_services = new Subject();
+const subject_servicedependency = new Subject();
+const subject_serviceinstances = new Subject();
+const subject_serviceinstancelinks = new Subject();
+
+let MockModelStore = {
+ query: jasmine.createSpy('XosModelStore.query')
+ .and.callFake((model) => {
+ if (model === 'Service') {
+ return subject_services.asObservable();
+ }
+ else if (model === 'ServiceDependency') {
+ return subject_servicedependency.asObservable();
+ }
+ else if (model === 'ServiceInstance') {
+ return subject_serviceinstances.asObservable();
+ }
+ else if (model === 'ServiceInstanceLink') {
+ return subject_serviceinstancelinks.asObservable();
+ }
+ })
+};
+
+
+describe('The XosGraphStore service', () => {
+
+ beforeEach(() => {
+ angular.module('XosGraphStore', [])
+ .service('XosGraphStore', XosGraphStore)
+ .value('XosModelStore', MockModelStore)
+ .service('XosDebouncer', XosDebouncer);
+
+ angular.mock.module('XosGraphStore');
+ });
+
+ beforeEach(angular.mock.inject((XosGraphStore: ITestXosGraphStore,
+ $rootScope: ng.IRootScopeService,
+ _$q_: ng.IQService) => {
+
+ service = XosGraphStore;
+ scope = $rootScope;
+
+ }));
+
+ it('should load services and service-dependency and add nodes to the graph', (done) => {
+ let event = 0;
+ service.get().subscribe(
+ (graph: Graph) => {
+ if (event === 1) {
+ expect(graph.nodes().length).toBe(services.length);
+ expect(graph.nodes()).toEqual(['service~1', 'service~2']);
+ expect(graph.edges().length).toBe(servicedependencies.length);
+ expect(graph.edges()).toEqual([{v: 'service~1', w: 'service~2'}]);
+ done();
+ }
+ else {
+ event = event + 1;
+ }
+ }
+ );
+ subject_services.next(services);
+ subject_servicedependency.next(servicedependencies);
+ scope.$apply();
+ });
+
+ describe(`the getModelType`, () => {
+ it('should return the node type', () => {
+ const res = service.getModelType(services[0]);
+ expect(res).toBe('service');
+ });
+
+ it('should return the node type', () => {
+ const res = service.getModelType(serviceInstances[0]);
+ expect(res).toBe('serviceinstance');
+ });
+ });
+
+ describe('the getNodeId method', () => {
+ it('should return the id for a Service', () => {
+ const res = service.getNodeId(services[0]);
+ expect(res).toBe(`service~1`);
+ });
+
+ it('should return the id for a ServiceInstance', () => {
+ const res = service.getNodeId(serviceInstances[0]);
+ expect(res).toBe(`serviceinstance~1`);
+ });
+ });
+
+ describe('the addNode method', () => {
+
+ beforeEach(() => {
+ spyOn(service.serviceGraph, 'setNode');
+ spyOn(service.serviceGraph, 'setEdge');
+ });
+
+ it(`should a service to the graph`, () => {
+ service.addNode(services[0]);
+ expect(service.serviceGraph.setNode).toHaveBeenCalledWith('service~1', services[0]);
+ });
+
+ it('should add a service instance to the graph', () => {
+ service.addNode(serviceInstances[0]);
+ expect(service.serviceGraph.setNode).toHaveBeenCalledWith('serviceinstance~1', serviceInstances[0]);
+ });
+
+ it('should add an "ownership" edge to the graph', () => {
+ service.addNode(serviceInstances[0]);
+ expect(service.serviceGraph.setEdge).toHaveBeenCalledWith('serviceinstance~1', 'service~1', {service: 1, service_instance: 1, type: 'ownership'});
+ });
+ });
+
+ describe('the addEdge method', () => {
+
+ beforeEach(() => {
+ spyOn(service.serviceGraph, 'setEdge');
+ });
+
+ it('should add a ServiceDependency to the graph', () => {
+ service.addEdge(servicedependencies[0]);
+ expect(service.serviceGraph.setEdge).toHaveBeenCalledWith('service~1', 'service~2', servicedependencies[0]);
+ });
+
+ it('should add a ServiceInstanceLink to the graph', () => {
+ service.addEdge(serviceInstanceLinks[0]);
+ expect(service.serviceGraph.setEdge).toHaveBeenCalledWith('serviceinstance~1', 'serviceinstance~2', serviceInstanceLinks[0]);
+ });
+ });
+
+ describe('the nodesFromGraph and linksFromGraph methods', () => {
+ let graph: Graph;
+
+ beforeEach(() => {
+ graph = new Graph();
+ services.forEach(s => {
+ graph.setNode(`service~${s.id}`, s);
+ });
+
+ servicedependencies.forEach(sd => {
+ graph.setEdge('service~1', 'service~2', sd);
+ });
+ });
+
+ it('should add id and type to the nodes', () => {
+ const nodes = service.nodesFromGraph(graph);
+ expect(nodes[0].id).toBe('service~1');
+ expect(nodes[0].type).toBe('service');
+ expect(nodes[0].data).toBeDefined();
+ });
+
+ it('should add id and type to the links', () => {
+ const links = service.linksFromGraph(graph);
+ expect(links[0].id).toBe('service~1-service~2');
+ expect(links[0].type).toBe('servicedependency');
+ expect(links[0].data).toBeDefined();
+ });
+
+ it('should handle ownership links', () => {
+ graph.setNode(`serviceinstance~1`, serviceInstances[0]);
+ graph.setEdge('service~1', 'serviceinstance~1', {type: 'ownership', service: 1, service_instance: 1});
+ const links = service.linksFromGraph(graph);
+ expect(links[1].source).toBe(0);
+ expect(links[1].target).toBe(2);
+ });
+
+ it('should handle serviceinstancelink links', () => {
+ graph.setNode(`serviceinstance~1`, serviceInstances[0]);
+ graph.setNode(`serviceinstance~2`, serviceInstances[1]);
+ graph.setEdge('serviceinstance~1', 'serviceinstance~2', serviceInstanceLinks[0]);
+ const links = service.linksFromGraph(graph);
+ const targetLink = _.find(links, {id: `serviceinstance~1-serviceinstance~2`});
+ expect(targetLink).toBeDefined();
+ expect(targetLink.source).toBe(3);
+ expect(targetLink.target).toBe(2);
+ });
+ });
+
+ describe(`the toggleServiceInstances method`, () => {
+ describe('when they are disabled', () => {
+
+ beforeEach(() => {
+ MockModelStore.query.calls.reset();
+ });
+
+ it('should fetch them', () => {
+ service.toggleServiceInstances();
+ expect(service.serviceInstanceShown).toBeTruthy();
+ expect(MockModelStore.query).toHaveBeenCalledWith(`ServiceInstance`, '/core/serviceinstances');
+ expect(MockModelStore.query).toHaveBeenCalledWith(`ServiceInstanceLink`, '/core/serviceinstancelinks');
+ expect(service.ServiceInstanceSubscription).toBeDefined();
+ expect(service.ServiceInstanceLinkSubscription).toBeDefined();
+ });
+ });
+
+ describe('when they are enabled', () => {
+ beforeEach(() => {
+ service.ServiceInstanceSubscription = {
+ unsubscribe: jasmine.createSpy('ServiceInstanceSubscription')
+ };
+ service.ServiceInstanceLinkSubscription = {
+ unsubscribe: jasmine.createSpy('ServiceInstanceLinkSubscription')
+ };
+ service.serviceInstanceShown = true;
+ });
+
+ it('should cancel subscriptions', () => {
+ service.toggleServiceInstances();
+ expect(service.serviceInstanceShown).toBeFalsy();
+ expect(service.ServiceInstanceSubscription.unsubscribe).toHaveBeenCalled();
+ expect(service.ServiceInstanceLinkSubscription.unsubscribe).toHaveBeenCalled();
+ });
+
+ describe('and loaded in the graph', () => {
+ beforeEach(() => {
+ service.serviceGraph = new Graph();
+
+ services.forEach(s => {
+ service.addNode(s);
+ });
+
+ serviceInstances.forEach(si => {
+ service.addNode(si);
+ });
+
+ serviceInstanceLinks.forEach(sil => {
+ service.addEdge(sil);
+ });
+ });
+ it('should remove ServiceInstance and related nodes/edges from the graph', () => {
+ let filteredGraph = service.toggleServiceInstances();
+ expect(service.serviceInstanceShown).toBeFalsy();
+ expect(filteredGraph.nodes().length).toBe(2);
+ expect(filteredGraph.edges().length).toBe(0);
+ expect(service.serviceGraph.nodes().length).toBe(2);
+ expect(service.serviceGraph.edges().length).toBe(0);
+ });
+ });
+ });
+ });
+});
diff --git a/src/app/service-graph/services/graph.store.ts b/src/app/service-graph/services/graph.store.ts
new file mode 100644
index 0000000..4d46c88
--- /dev/null
+++ b/src/app/service-graph/services/graph.store.ts
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 * as _ from 'lodash';
+import {Graph} from 'graphlib';
+import {IXosModelStoreService} from '../../datasources/stores/model.store';
+import {IXosDebouncer} from '../../core/services/helpers/debounce.helper';
+import {Subscription} from 'rxjs/Subscription';
+import {BehaviorSubject} from 'rxjs/BehaviorSubject';
+import {Observable} from 'rxjs/Observable';
+import {IXosBaseModel, IXosSgLink, IXosSgNode} from '../interfaces';
+
+
+export interface IXosGraphStore {
+ get(): Observable<Graph>;
+ nodesFromGraph(graph: Graph): IXosSgNode[];
+ linksFromGraph(graph: Graph): IXosSgLink[];
+ toggleServiceInstances(): Graph;
+}
+
+export class XosGraphStore implements IXosGraphStore {
+ static $inject = [
+ '$log',
+ 'XosModelStore',
+ 'XosDebouncer'
+ ];
+
+ // state
+ private serviceInstanceShown: boolean = false;
+
+ // graphs
+ private serviceGraph: any;
+ private ServiceGraphSubject: BehaviorSubject<any>;
+
+ // datastore
+ private ServiceSubscription: Subscription;
+ private ServiceDependencySubscription: Subscription;
+ private ServiceInstanceSubscription: Subscription;
+ private ServiceInstanceLinkSubscription: Subscription;
+
+ // debounced
+ private efficientNext = this.XosDebouncer.debounce(this.callNext, 500, this, false);
+
+ constructor (
+ private $log: ng.ILogService,
+ private XosModelStore: IXosModelStoreService,
+ private XosDebouncer: IXosDebouncer
+ ) {
+ this.$log.info('[XosGraphStore] Setup');
+
+ this.serviceGraph = new Graph();
+ this.ServiceGraphSubject = new BehaviorSubject(this.serviceGraph);
+
+ this.loadData();
+ }
+
+ $onDestroy() {
+ this.ServiceSubscription.unsubscribe();
+ this.ServiceDependencySubscription.unsubscribe();
+ }
+
+ public nodesFromGraph(graph: Graph): IXosSgNode[] {
+ return _.map(graph.nodes(), (n: string) => {
+ const nodeData = graph.node(n);
+ return {
+ id: n,
+ type: this.getModelType(nodeData),
+ data: nodeData
+ };
+ });
+ }
+
+ public linksFromGraph(graph: Graph): IXosSgLink[] {
+ const nodes = this.nodesFromGraph(graph);
+
+ // NOTE we'll need some intelligence here to differentiate between:
+ // - ServiceDependency
+ // - ServiceInstanceLinks
+ // - Owners
+
+ return _.map(graph.edges(), l => {
+ const link = graph.edge(l);
+ const linkType = this.getModelType(link);
+
+ // FIXME consider ownership links
+ let sourceId, targetId;
+
+ switch (linkType) {
+ case 'servicedependency':
+ sourceId = this.getServiceId(link.subscriber_service_id);
+ targetId = this.getServiceId(link.provider_service_id);
+ break;
+ case 'serviceinstancelink':
+ sourceId = this.getServiceInstanceId(link.subscriber_service_instance_id);
+ targetId = this.getServiceInstanceId(link.provider_service_instance_id);
+ break;
+ case 'ownership':
+ sourceId = this.getServiceId(link.service);
+ targetId = this.getServiceInstanceId(link.service_instance);
+ }
+
+ // NOTE help while debugging
+ if (!sourceId || !targetId) {
+ this.$log.warn(`Link ${l.v}-${l.w} has missing source or target:`, l, link);
+ }
+
+ return {
+ id: `${l.v}-${l.w}`,
+ type: this.getModelType(link),
+ source: _.findIndex(nodes, {id: sourceId}),
+ target: _.findIndex(nodes, {id: targetId}),
+ data: link
+ };
+ });
+ }
+
+ public toggleServiceInstances(): Graph {
+ if (this.serviceInstanceShown) {
+ // NOTE remove subscriptions
+ this.ServiceInstanceSubscription.unsubscribe();
+ this.ServiceInstanceLinkSubscription.unsubscribe();
+
+ // remove nodes from the graph
+ this.removeElementsFromGraph('serviceinstance'); // NOTE links are automatically removed by the graph library
+ }
+ else {
+ // NOTE subscribe to ServiceInstance and ServiceInstanceLink observables
+ this.loadServiceInstances();
+ this.loadServiceInstanceLinks();
+ }
+ this.serviceInstanceShown = !this.serviceInstanceShown;
+ return this.serviceGraph;
+ }
+
+ public get(): Observable<Graph> {
+ return this.ServiceGraphSubject.asObservable();
+ }
+
+ private loadData() {
+ this.loadServices();
+ this.loadServiceDependencies();
+ }
+
+ // graph operations
+ private addNode(node: IXosBaseModel) {
+ const nodeId = this.getNodeId(node);
+ this.serviceGraph.setNode(nodeId, node);
+
+ const nodeType = this.getModelType(node);
+ if (nodeType === 'serviceinstance') {
+ // NOTE adding owner link
+ this.addOwnershipEdge({
+ service: node.owner_id,
+ service_instance: node.id,
+ type: 'ownership'
+ });
+ }
+ }
+
+ private addEdge(link: IXosBaseModel) {
+ const linkType = this.getModelType(link);
+ if (linkType === 'servicedependency') {
+ const sourceId = this.getServiceId(link.subscriber_service_id);
+ const targetId = this.getServiceId(link.provider_service_id);
+ this.serviceGraph.setEdge(sourceId, targetId, link);
+ }
+ if (linkType === 'serviceinstancelink') {
+ // NOTE serviceinstancelink can point also to services, networks, ...
+ const sourceId = this.getServiceInstanceId(link.provider_service_instance_id);
+ if (angular.isDefined(link.subscriber_service_instance_id)) {
+ const targetId = this.getServiceInstanceId(link.subscriber_service_instance_id);
+ this.serviceGraph.setEdge(sourceId, targetId, link);
+ }
+ }
+ }
+
+ private addOwnershipEdge(link: any) {
+ const sourceId = this.getServiceInstanceId(link.service_instance);
+ const targetId = this.getServiceId(link.service);
+ this.serviceGraph.setEdge(sourceId, targetId, link);
+ }
+
+ private removeElementsFromGraph(type: string) {
+ _.forEach(this.serviceGraph.nodes(), (n: string) => {
+ const node = this.serviceGraph.node(n);
+ const nodeType = this.getModelType(node);
+ if (nodeType === type) {
+ this.serviceGraph.removeNode(n);
+ }
+ });
+ // NOTE update the observable
+ this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+ }
+
+ // helpers
+ private getModelType(node: IXosBaseModel): string {
+ if (node.type) {
+ // NOTE we'll add "ownership" links
+ return node.type;
+ }
+ return node.class_names.split(',')[0].toLowerCase();
+ }
+
+ private getServiceId(id: number): string {
+ return `service~${id}`;
+ }
+
+ private getServiceInstanceId(id: number): string {
+ return `serviceinstance~${id}`;
+ }
+
+ private getNodeId(node: IXosBaseModel): string {
+
+ const nodeType = this.getModelType(node);
+ switch (nodeType) {
+ case 'service':
+ return this.getServiceId(node.id);
+ case 'serviceinstance':
+ return this.getServiceInstanceId(node.id);
+ }
+ }
+
+ // data loaders
+ private loadServices() {
+ this.ServiceSubscription = this.XosModelStore.query('Service', '/core/services')
+ .subscribe(
+ (res) => {
+ if (res.length > 0) {
+ _.forEach(res, n => {
+ this.addNode(n);
+ });
+ this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+ }
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] Service Observable: `, err);
+ }
+ );
+ }
+
+ private loadServiceDependencies() {
+ this.ServiceDependencySubscription = this.XosModelStore.query('ServiceDependency', '/core/servicedependencys')
+ .subscribe(
+ (res) => {
+ if (res.length > 0) {
+ _.forEach(res, l => {
+ this.addEdge(l);
+ });
+ this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+ }
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] Service Observable: `, err);
+ }
+ );
+ }
+
+ private loadServiceInstances() {
+ this.ServiceInstanceSubscription = this.XosModelStore.query('ServiceInstance', '/core/serviceinstances')
+ .subscribe(
+ (res) => {
+ if (res.length > 0) {
+ _.forEach(res, n => {
+ this.addNode(n);
+ });
+ this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+ }
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] ServiceInstance Observable: `, err);
+ }
+ );
+ }
+
+ private loadServiceInstanceLinks() {
+ this.ServiceInstanceLinkSubscription = this.XosModelStore.query('ServiceInstanceLink', '/core/serviceinstancelinks')
+ .subscribe(
+ (res) => {
+ if (res.length > 0) {
+ _.forEach(res, l => {
+ this.addEdge(l);
+ });
+ this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+ }
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] ServiceInstanceLinks Observable: `, err);
+ }
+ );
+ }
+
+ private callNext(subject: BehaviorSubject<any>, data: any) {
+ subject.next(data);
+ }
+}
diff --git a/src/app/service-graph/services/node-positioner.service.spec.ts b/src/app/service-graph/services/node-positioner.service.spec.ts
new file mode 100644
index 0000000..14fb9c4
--- /dev/null
+++ b/src/app/service-graph/services/node-positioner.service.spec.ts
@@ -0,0 +1,128 @@
+
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-ui-router';
+import {IXosNodePositioner, XosNodePositioner} from './node-positioner.service';
+
+let service: IXosNodePositioner;
+
+let scope: ng.IRootScopeService;
+
+let constraints: string = '';
+
+let mockResource = {
+ query: () => {
+ return;
+ }
+};
+
+const mockModelRest = {
+ getResource: jasmine.createSpy('ModelRest.getResource')
+ .and.returnValue(mockResource)
+};
+
+describe('The XosNodePositioner service', () => {
+
+ beforeEach(() => {
+ angular.module('XosNodePositioner', [])
+ .service('XosNodePositioner', XosNodePositioner)
+ .value('ModelRest', mockModelRest);
+
+ angular.mock.module('XosNodePositioner');
+ });
+
+ beforeEach(angular.mock.inject((
+ XosNodePositioner: IXosNodePositioner,
+ $rootScope: ng.IRootScopeService,
+ _$q_: ng.IQService) => {
+
+ service = XosNodePositioner;
+ scope = $rootScope;
+
+ spyOn(mockResource, 'query').and.callFake(() => {
+ const d = _$q_.defer();
+ d.resolve([{constraints}]);
+ return {$promise: d.promise};
+ });
+ }));
+
+ it('should position the nodes on the svg', (done) => {
+ const svg = {width: 300, height: 100};
+ const nodes = [
+ {data: {name: 'a'}},
+ {data: {name: 'b'}}
+ ];
+ constraints = '["a", "b"]';
+ service.positionNodes(svg, nodes)
+ .then((positioned) => {
+ expect(positioned[0].x).toBe(100);
+ expect(positioned[0].y).toBe(50);
+ expect(positioned[1].x).toBe(200);
+ expect(positioned[1].y).toBe(50);
+ done();
+ });
+
+ scope.$apply();
+ });
+
+ it('should position the nodes on the svg in vertical bundles', (done) => {
+ const svg = {width: 300, height: 90};
+ const nodes = [
+ {data: {name: 'a'}},
+ {data: {name: 'b'}},
+ {data: {name: 'c'}}
+ ];
+ constraints = '["a", ["b", "c"]]';
+ service.positionNodes(svg, nodes)
+ .then((positioned) => {
+ expect(positioned[0].x).toBe(100);
+ expect(positioned[0].y).toBe(45);
+ expect(positioned[1].x).toBe(200);
+ expect(positioned[1].y).toBe(30);
+ expect(positioned[2].x).toBe(200);
+ expect(positioned[2].y).toBe(60);
+ done();
+ });
+
+ scope.$apply();
+ });
+
+ it('should accept null as constraint to leave an empty space', (done) => {
+ const svg = {width: 300, height: 90};
+ const nodes = [
+ {data: {name: 'a'}},
+ {data: {name: 'b'}},
+ {data: {name: 'c'}}
+ ];
+ constraints = '[[null, "a"], ["b", "c"]]';
+ service.positionNodes(svg, nodes)
+ .then((positioned) => {
+ expect(positioned[0].x).toBe(100);
+ expect(positioned[0].y).toBe(60);
+ expect(positioned[1].x).toBe(200);
+ expect(positioned[1].y).toBe(30);
+ expect(positioned[2].x).toBe(200);
+ expect(positioned[2].y).toBe(60);
+ done();
+ });
+
+ scope.$apply();
+ });
+});
diff --git a/src/app/service-graph/services/node-positioner.service.ts b/src/app/service-graph/services/node-positioner.service.ts
new file mode 100644
index 0000000..499afd0
--- /dev/null
+++ b/src/app/service-graph/services/node-positioner.service.ts
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 * as _ from 'lodash';
+import {IXosResourceService} from '../../datasources/rest/model.rest';
+import {IXosSgNode} from '../interfaces';
+
+export interface IXosNodePositioner {
+ positionNodes(svg: {width: number, height: number}, nodes: any[]): ng.IPromise<IXosSgNode[]>;
+}
+
+export class XosNodePositioner implements IXosNodePositioner {
+ static $inject = [
+ '$log',
+ '$q',
+ 'ModelRest'
+ ];
+
+ constructor (
+ private $log: ng.ILogService,
+ private $q: ng.IQService,
+ private ModelRest: IXosResourceService,
+ ) {
+ this.$log.info('[XosNodePositioner] Setup');
+ }
+
+ public positionNodes(svg: {width: number, height: number}, nodes: any[]): ng.IPromise<IXosSgNode[]> {
+
+ // TODO refactor naming in this loop to make it clearer
+ const d = this.$q.defer();
+
+ this.getConstraints()
+ .then(constraints => {
+ const hStep = this.getHorizontalStep(svg.width, constraints);
+ const positionConstraints = _.reduce(constraints, (all: any, c: string | string[], i: number) => {
+ let pos: {x: number, y: number, fixed: boolean} = {
+ x: svg.width / 2,
+ y: svg.height / 2,
+ fixed: true
+ };
+ // NOTE it's a single element, leave it in the middle
+ if (angular.isString(c)) {
+ pos.x = (i + 1) * hStep;
+ all[c] = pos;
+ }
+ else {
+ const verticalConstraints = c;
+ const vStep = this.getVerticalStep(svg.height, verticalConstraints);
+ _.forEach(verticalConstraints, (c: string, v: number) => {
+ if (angular.isString(c)) {
+ let p = angular.copy(pos);
+ p.x = (i + 1) * hStep;
+ p.y = (v + 1) * vStep;
+ all[c] = p;
+ }
+ });
+ }
+ return all;
+ }, {});
+
+ d.resolve(_.map(nodes, n => {
+ return angular.merge(n, positionConstraints[n.data.name]);
+ }));
+ })
+ .catch(e => {
+ this.$log.error(`[XosNodePositioner] Error retrieving constraints`, e);
+ });
+
+ return d.promise;
+ }
+
+ private getConstraints(): ng.IPromise<any[]> {
+ const d = this.$q.defer();
+ this.ModelRest.getResource('/core/servicegraphconstraints').query().$promise
+ .then(res => {
+ d.resolve(JSON.parse(res[0].constraints));
+ })
+ .catch(e => {
+ d.reject(e);
+ });
+ return d.promise;
+ }
+
+ private getHorizontalStep(svgWidth: number, constraints: any[]) {
+ return svgWidth / (constraints.length + 1);
+ }
+
+ private getVerticalStep(svgHeight: number, verticalConstraints: string[]) {
+ // NOTE verticalConstraints represent the vertical part (the nested array)
+ return svgHeight / (verticalConstraints.length + 1);
+ }
+}
diff --git a/src/app/service-graph/services/renderer/node.renderer.ts b/src/app/service-graph/services/renderer/node.renderer.ts
new file mode 100644
index 0000000..17bcf70
--- /dev/null
+++ b/src/app/service-graph/services/renderer/node.renderer.ts
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed 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 * as d3 from 'd3';
+import * as _ from 'lodash';
+import {IXosSgNode} from '../../interfaces';
+import {XosServiceGraphConfig as config} from '../../graph.config';
+import {IXosServiceGraphIcons} from '../d3-helpers/graph-icons.service';
+import {IXosGraphHelpers} from '../d3-helpers/graph-elements.helpers';
+
+export interface IXosNodeRenderer {
+ renderNodes(forceLayout: d3.forceLayout, nodeContainer: d3.Selection, nodes: IXosSgNode[]): void;
+}
+
+export class XosNodeRenderer {
+
+ static $inject = [
+ 'XosServiceGraphIcons',
+ 'XosGraphHelpers'
+ ];
+
+ private drag;
+
+ constructor (
+ private XosServiceGraphIcons: IXosServiceGraphIcons,
+ private XosGraphHelpers: IXosGraphHelpers
+ ) {}
+
+ public renderNodes(forceLayout: any, nodeContainer: any, nodes: IXosSgNode[]): void {
+
+ this.drag = forceLayout.drag()
+ .on('dragstart', (n: IXosSgNode) => {
+ n.fixed = true;
+ });
+
+ const node = nodeContainer
+ .selectAll('g.node')
+ .data(nodes, n => n.id);
+
+ node
+ .call(this.drag);
+
+ const entering = node.enter()
+ .append('g')
+ .attr({
+ id: n => n.id,
+ class: n => `node ${n.type} ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
+ });
+
+ this.renderServiceNodes(entering.filter('.service'));
+ this.renderServiceInstanceNodes(entering.filter('.serviceinstance'));
+
+ node.exit().remove();
+ }
+
+ private renderServiceNodes(nodes: d3.selection) {
+
+ nodes
+ .append('rect')
+ .attr({
+ rx: config.node.radius,
+ ry: config.node.radius
+ });
+
+ nodes
+ .append('path')
+ .attr({
+ d: this.XosServiceGraphIcons.get('service').path,
+ transform: this.XosServiceGraphIcons.get('service').transform,
+ class: 'icon'
+ });
+
+ this.positionServiceNodeGroup(nodes);
+ this.handleLabels(nodes);
+ }
+
+ private renderServiceInstanceNodes(nodes: d3.selection) {
+ nodes.append('rect')
+ .attr({
+ width: 40,
+ height: 40,
+ x: -20,
+ y: -20,
+ transform: `rotate(45)`
+ });
+
+ nodes
+ .append('path')
+ .attr({
+ d: this.XosServiceGraphIcons.get('serviceinstance').path,
+ class: 'icon'
+ });
+
+ this.positionServiceInstanceNodeGroup(nodes);
+ this.handleLabels(nodes); // eventually improve, padding top is wrong
+ }
+
+ private positionServiceNodeGroup(nodes: d3.selection) {
+ const self = this;
+ nodes.each(function (d: IXosSgNode) {
+ const node = d3.select(this);
+ const rect = node.select('rect');
+ const icon = node.select('path');
+ const bbox = self.XosGraphHelpers.getSiblingIconBBox(rect.node());
+
+ rect
+ .attr({
+ width: bbox.width + config.node.padding,
+ height: bbox.height + config.node.padding,
+ x: - (config.node.padding / 2),
+ y: - (config.node.padding / 2),
+ transform: `translate(${-bbox.width / 2}, ${-bbox.height / 2})`
+ });
+
+ icon
+ .attr({
+ transform: `translate(${-bbox.width / 2}, ${-bbox.height / 2})`
+ });
+ });
+ }
+
+ private positionServiceInstanceNodeGroup(nodes: d3.selection) {
+ const self = this;
+ nodes.each(function (d: IXosSgNode) {
+ const node = d3.select(this);
+ const rect = node.select('rect');
+ const icon = node.select('path');
+ const bbox = self.XosGraphHelpers.getSiblingIconBBox(rect.node());
+ const size = _.max([bbox.width, bbox.height]); // NOTE we need it to be a square
+ rect
+ .attr({
+ width: size + config.node.padding,
+ height: size + config.node.padding,
+ x: - (config.node.padding / 2),
+ y: - (config.node.padding / 2),
+ transform: `rotate(45), translate(${-bbox.width / 2}, ${-bbox.height / 2})`
+ });
+
+ icon
+ .attr({
+ transform: `translate(${-bbox.width / 2}, ${-bbox.height / 2})`
+ });
+ });
+ }
+
+ private handleLabels(nodes: d3.selection) {
+ const self = this;
+ // if (this.userConfig.labels) {
+
+ // group to contain label text and wrapper
+ const label = nodes.append('g')
+ .attr({
+ class: 'label'
+ });
+
+ // setting up the wrapper
+ label
+ .append('rect')
+ .attr({
+ class: 'label-wrapper',
+ rx: config.node.radius,
+ ry: config.node.radius
+ });
+
+ // adding text
+ label
+ .append('text')
+ .text(n => this.getNodeLabel(n))
+ .attr({
+ 'opacity': 0,
+ 'text-anchor': 'left',
+ 'alignment-baseline': 'bottom',
+ 'font-size': config.node.text,
+ y: config.node.text * 0.78
+ })
+ .transition()
+ .duration(config.duration)
+ .attr({
+ opacity: 1
+ });
+
+ // resize and position label
+ label.each(function() {
+ const text = d3.select(this).select('text').node();
+ const rect = d3.select(this).select('rect');
+ const iconRect = d3.select(this.parentNode).select('rect').node();
+ const icon = self.XosGraphHelpers.getBBox(iconRect);
+ const bbox = self.XosGraphHelpers.getBBox(text);
+
+ // scale the rectangle around the label to fit the text
+ rect
+ .attr({
+ width: bbox.width + config.node.padding,
+ height: config.node.text - 2 + config.node.padding,
+ x: -(config.node.padding / 2),
+ y: -(config.node.padding / 2),
+ });
+
+ // translate the lable group to the correct position
+ d3.select(this)
+ .attr({
+ transform: function() {
+ const label = self.XosGraphHelpers.getBBox(this);
+ const x = - (label.width - config.node.padding) / 2;
+ const y = (icon.height / 2) + config.node.padding;
+ return `translate(${x}, ${y})`;
+ }
+ });
+ });
+ // }
+ // else {
+ // node.selectAll('text')
+ // .transition()
+ // .duration(this.duration)
+ // .attr({
+ // opacity: 0
+ // })
+ // .remove();
+ // }
+ }
+
+ private getNodeLabel(n: any): string {
+ return n.data.name ? n.data.name.toUpperCase() : n.data.id;
+ // return n.data.name ? n.data.name.toUpperCase() + ` - ${n.data.id}` : n.data.id;
+ }
+}
diff --git a/src/app/service-graph/services/service-graph.store.spec.ts b/src/app/service-graph/services/service-graph.store.spec.ts
deleted file mode 100644
index 91c7dd0..0000000
--- a/src/app/service-graph/services/service-graph.store.spec.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-
-/*
- * Copyright 2017-present Open Networking Foundation
-
- * Licensed 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 * as angular from 'angular';
-import 'angular-mocks';
-import 'angular-ui-router';
-import {IXosServiceGraphStore, XosServiceGraphStore} from './service-graph.store';
-import {Subject} from 'rxjs';
-import {XosDebouncer} from '../../core/services/helpers/debounce.helper';
-import {IXosServiceGraph} from '../interfaces';
-import {XosServiceGraphExtender, IXosServiceGraphExtender} from './graph.extender';
-
-let service: IXosServiceGraphStore, extender: IXosServiceGraphExtender;
-
-const subjects = {
- service: new Subject<any>(),
- tenant: new Subject<any>(),
- subscriber: new Subject<any>(),
- tenantroot: new Subject<any>(),
- network: new Subject<any>(),
- servicedependency: new Subject<any>()
-};
-
-// COARSE data
-const coarseServices = [
- {
- id: 1,
- name: 'Service A',
- class_names: 'Service, XOSBase'
- },
- {
- id: 2,
- name: 'Service B',
- class_names: 'Service, XOSBase'
- }
-];
-
-const coarseTenants = [
- {
- id: 1,
- provider_service_id: 2,
- subscriber_service_id: 1,
- kind: 'coarse',
- class_names: 'Tenant, XOSBase'
- }
-];
-
-const mockModelStore = {
- query: (modelName: string) => {
- return subjects[modelName.toLowerCase()].asObservable();
- }
-};
-
-describe('The XosServiceGraphStore service', () => {
-
- beforeEach(() => {
- angular.module('xosServiceGraphStore', [])
- .service('XosServiceGraphStore', XosServiceGraphStore)
- .value('XosModelStore', mockModelStore)
- .service('XosServiceGraphExtender', XosServiceGraphExtender)
- .service('XosDebouncer', XosDebouncer);
-
- angular.mock.module('xosServiceGraphStore');
- });
-
- beforeEach(angular.mock.inject((
- XosServiceGraphStore: IXosServiceGraphStore,
- XosServiceGraphExtender: IXosServiceGraphExtender
- ) => {
- service = XosServiceGraphStore;
- extender = XosServiceGraphExtender;
- }));
-
- describe('when subscribing for the COARSE service graph', () => {
- beforeEach((done) => {
- subjects.service.next(coarseServices);
- subjects.servicedependency.next(coarseTenants);
- setTimeout(done, 500);
- });
-
- it('should return an observer for the Coarse Service Graph', (done) => {
- service.getCoarse()
- .subscribe(
- (res: IXosServiceGraph) => {
- expect(res.nodes.length).toBe(2);
- expect(res.nodes[0].d3Class).toBeUndefined();
- expect(res.links.length).toBe(1);
- expect(res.links[0].d3Class).toBeUndefined();
- done();
- },
- (err) => {
- done(err);
- }
- );
- });
-
- xdescribe('when a reducer is registered', () => {
- // NOTE the reducer appliance has been moved in the component
- beforeEach((done) => {
- extender.register('coarse', 'test', (graph: IXosServiceGraph) => {
- graph.nodes = graph.nodes.map(n => {
- n.d3Class = `testNode`;
- return n;
- });
-
- graph.links = graph.links.map(n => {
- n.d3Class = `testLink`;
- return n;
- });
-
- return graph;
- });
-
- // triggering another next cycle to apply the reducer
- subjects.service.next(coarseServices);
- subjects.tenant.next(coarseTenants);
- setTimeout(done, 500);
- });
-
- it('should transform the result', (done) => {
- service.getCoarse()
- .subscribe(
- (res: IXosServiceGraph) => {
- expect(res.nodes.length).toBe(2);
- expect(res.nodes[0].d3Class).toEqual('testNode');
- expect(res.links.length).toBe(1);
- expect(res.links[0].d3Class).toEqual('testLink');
- done();
- },
- (err) => {
- done(err);
- }
- );
- });
- });
- });
-
- describe('when subscribing for the Fine-grained service graph', () => {
- xit('should have a test', () => {
- expect(true).toBeTruthy();
- });
- });
-});
diff --git a/src/app/service-graph/services/service-graph.store.ts b/src/app/service-graph/services/service-graph.store.ts
deleted file mode 100644
index 92b9e6d..0000000
--- a/src/app/service-graph/services/service-graph.store.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-
-/*
- * Copyright 2017-present Open Networking Foundation
-
- * Licensed 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 * as _ from 'lodash';
-import {Observable, BehaviorSubject, Subscription} from 'rxjs';
-import {IXosModelStoreService} from '../../datasources/stores/model.store';
-import {
- IXosServiceGraph, IXosServiceModel, IXosTenantModel, IXosCoarseGraphData,
- IXosServiceGraphNode, IXosServiceGraphLink, IXosFineGrainedGraphData
-} from '../interfaces';
-import {IXosDebouncer} from '../../core/services/helpers/debounce.helper';
-export interface IXosServiceGraphStore {
- // TODO remove, moved in a new service
- get(): Observable<IXosServiceGraph>;
- // TODO rename in get()
- getCoarse(): Observable<IXosServiceGraph>;
-}
-
-export class XosServiceGraphStore implements IXosServiceGraphStore {
- static $inject = [
- '$log',
- 'XosModelStore',
- 'XosDebouncer'
- ];
-
- // graph data store
- private graphData: BehaviorSubject<IXosFineGrainedGraphData> = new BehaviorSubject({
- services: [],
- tenants: [],
- networks: [],
- subscribers: [],
- servicedependencies: []
- });
-
- private emptyGraph: IXosServiceGraph = {
- nodes: [],
- links: []
- };
-
- // representation of the graph as D3 requires
- private d3CoarseGraph = new BehaviorSubject(this.emptyGraph);
- private d3FineGrainedGraph = new BehaviorSubject(this.emptyGraph);
-
- // storing locally reference to the data model
- private services;
- private tenants;
- private subscribers;
- private networks;
- private servicedependencys;
-
- // debounced functions
- private handleData;
-
- // datastore
- private ServiceSubscription: Subscription;
- private NetworkSubscription: Subscription;
- private ServiceDependencySubscription: Subscription;
-
- constructor (
- private $log: ng.ILogService,
- private XosModelStore: IXosModelStoreService,
- private XosDebouncer: IXosDebouncer
- ) {
-
- this.$log.info(`[XosServiceGraphStore] Setup`);
-
- // we want to have a quiet period of 500ms from the last event before doing anything
- this.handleData = this.XosDebouncer.debounce(this._handleData, 500, this, false);
-
- // observe models and populate graphData
- this.ServiceSubscription = this.XosModelStore.query('Service', '/core/services')
- .subscribe(
- (res) => {
- this.combineData(res, 'services');
- },
- (err) => {
- this.$log.error(`[XosServiceGraphStore] Service Observable: `, err);
- }
- );
-
- this.ServiceDependencySubscription = this.XosModelStore.query('ServiceDependency', '/core/servicedependencys')
- .subscribe(
- (res) => {
- this.combineData(res, 'servicedependencies');
- },
- (err) => {
- this.$log.error(`[XosServiceGraphStore] Service Observable: `, err);
- }
- );
-
- this.NetworkSubscription = this.XosModelStore.query('Network', '/core/networks')
- .subscribe(
- (res) => {
- this.combineData(res, 'networks');
- },
- (err) => {
- this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
- }
- );
-
- // observe graphData and build Coarse and FineGrained graphs
- this.graphData
- .subscribe(
- (res: IXosFineGrainedGraphData) => {
- this.$log.debug(`[XosServiceGraphStore] New graph data received`, res);
- this.graphDataToCoarseGraph(res);
- // this.graphDataToFineGrainedGraph(res);
- },
- (err) => {
- this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
- }
- );
- }
-
- public get() {
- return this.d3FineGrainedGraph.asObservable();
- }
-
- public getCoarse() {
- return this.d3CoarseGraph.asObservable();
- }
-
- private combineData(data: any, type: 'services'|'tenants'|'subscribers'|'networks'|'servicedependencies') {
- switch (type) {
- case 'services':
- this.services = data;
- break;
- case 'tenants':
- this.tenants = data;
- break;
- case 'subscribers':
- this.subscribers = data;
- break;
- case 'networks':
- this.networks = data;
- break;
- case 'servicedependencies':
- this.servicedependencys = data;
- break;
- }
- this.handleData(this.services, this.tenants);
- }
-
- private _handleData(services: IXosServiceModel[], tenants: IXosTenantModel[]) {
- this.graphData.next({
- services: this.services,
- tenants: this.tenants,
- subscribers: this.subscribers,
- networks: this.networks,
- servicedependencies: this.servicedependencys
- });
- }
-
- private getNodeIndexById(id: number | string, nodes: IXosServiceModel[]) {
- return _.findIndex(nodes, {id: id});
- }
-
- private graphDataToCoarseGraph(data: IXosCoarseGraphData) {
-
- try {
- const links: IXosServiceGraphLink[] = _.chain(data.servicedependencies)
- .map((t: IXosTenantModel) => {
- return {
- id: t.id,
- source: this.getNodeIndexById(t.provider_service_id, data.services),
- target: this.getNodeIndexById(t.subscriber_service_id, data.services),
- model: t
- };
- })
- .value();
-
- const nodes: IXosServiceGraphNode[] = _.map(data.services, (s: IXosServiceModel) => {
- return {
- id: s.id,
- label: s.name,
- model: s
- };
- });
-
- let graph: IXosServiceGraph = {
- nodes,
- links
- };
-
- this.d3CoarseGraph.next(graph);
- } catch (e) {
- this.d3CoarseGraph.error(e);
- }
- }
-}
diff --git a/src/app/service-graph/services/service-instance.graph.store.ts b/src/app/service-graph/services/service-instance.graph.store.ts
deleted file mode 100644
index b8220b9..0000000
--- a/src/app/service-graph/services/service-instance.graph.store.ts
+++ /dev/null
@@ -1,281 +0,0 @@
-
-/*
- * Copyright 2017-present Open Networking Foundation
-
- * Licensed 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 * as _ from 'lodash';
-import {Observable, BehaviorSubject, Subscription} from 'rxjs';
-import {
- IXosServiceGraph, IXosServiceInstanceGraphData, IXosServiceGraphNode
-} from '../interfaces';
-import {IXosDebouncer} from '../../core/services/helpers/debounce.helper';
-import {IXosModelStoreService} from '../../datasources/stores/model.store';
-import {IXosServiceGraphStore} from './service-graph.store';
-
-export interface IXosServiceInstanceGraphStore {
- get(): Observable<IXosServiceGraph>;
-}
-
-export class XosServiceInstanceGraphStore implements IXosServiceInstanceGraphStore {
- static $inject = [
- '$log',
- 'XosServiceGraphStore',
- 'XosModelStore',
- 'XosDebouncer'
- ];
-
- private CoarseGraphSubscription: Subscription;
- private ServiceInstanceSubscription: Subscription;
- private ServiceInstanceLinkSubscription: Subscription;
- private NetworkSubscription: Subscription;
-
- // debounced functions
- private handleData;
-
-
- // FIXME this is declared also in ServiceGraphStore
- private emptyGraph: IXosServiceGraph = {
- nodes: [],
- links: []
- };
-
- // graph data store
- private graphData: BehaviorSubject<IXosServiceInstanceGraphData> = new BehaviorSubject({
- serviceGraph: this.emptyGraph,
- serviceInstances: [],
- serviceInstanceLinks: [],
- networks: []
- });
-
- private d3ServiceInstanceGraph = new BehaviorSubject(this.emptyGraph);
-
- private serviceGraph: IXosServiceGraph = this.emptyGraph;
- private serviceInstances: any[] = [];
- private serviceInstanceLinks: any[] = [];
- private networks: any[] = [];
-
- constructor (
- private $log: ng.ILogService,
- private XosServiceGraphStore: IXosServiceGraphStore,
- private XosModelStore: IXosModelStoreService,
- private XosDebouncer: IXosDebouncer
- ) {
- this.$log.info(`[XosServiceInstanceGraphStore] Setup`);
-
- // we want to have a quiet period of 500ms from the last event before doing anything
- this.handleData = this.XosDebouncer.debounce(this._handleData, 500, this, false);
-
- this.CoarseGraphSubscription = this.XosServiceGraphStore.getCoarse()
- .subscribe(
- (graph: IXosServiceGraph) => {
- this.combineData(graph, 'serviceGraph');
- }
- );
-
- this.ServiceInstanceSubscription = this.XosModelStore.query('ServiceInstance', '/core/serviceinstances')
- .subscribe(
- (res) => {
- this.combineData(res, 'serviceInstance');
- },
- (err) => {
- this.$log.error(`[XosServiceInstanceGraphStore] Service Observable: `, err);
- }
- );
-
- this.ServiceInstanceLinkSubscription = this.XosModelStore.query('ServiceInstanceLink', '/core/serviceinstancelinks')
- .subscribe(
- (res) => {
- this.combineData(res, 'serviceInstanceLink');
- },
- (err) => {
- this.$log.error(`[XosServiceInstanceGraphStore] Service Observable: `, err);
- }
- );
-
- this.NetworkSubscription = this.XosModelStore.query('Network', '/core/networks')
- .subscribe(
- (res) => {
- this.combineData(res, 'networks');
- },
- (err) => {
- this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
- }
- );
-
- // observe graphData and build ServiceInstance graph
- this.graphData
- .subscribe(
- (res: IXosServiceInstanceGraphData) => {
- this.$log.debug(`[XosServiceInstanceGraphStore] New graph data received`, res);
-
- this.graphDataToD3(res);
- },
- (err) => {
- this.$log.error(`[XosServiceInstanceGraphStore] graphData Observable: `, err);
- }
- );
- }
-
- public get(): Observable<IXosServiceGraph> {
- return this.d3ServiceInstanceGraph;
- }
-
- // called by all the observables, combine the data in a globla graph observable
- private combineData(data: any, type: 'serviceGraph' | 'serviceInstance' | 'serviceInstanceLink' | 'serviceInterface' | 'networks') {
- switch (type) {
- case 'serviceGraph':
- this.serviceGraph = angular.copy(data);
- break;
- case 'serviceInstance':
- this.serviceInstances = data;
- break;
- case 'serviceInstanceLink':
- this.serviceInstanceLinks = data;
- break;
- case 'networks':
- this.networks = data;
- break;
- }
- this.handleData();
- }
-
- private _handleData() {
- this.graphData.next({
- serviceGraph: this.serviceGraph,
- serviceInstances: this.serviceInstances,
- serviceInstanceLinks: this.serviceInstanceLinks,
- networks: this.networks
- });
- }
-
- private getNodeType(n: any) {
- return n.class_names.split(',')[0].toLowerCase();
- }
-
- private getNodeLabel(n: any) {
- if (this.getNodeType(n) === 'serviceinstance') {
- return n.name ? n.name : n.id;
- }
- return n.humanReadableName ? n.humanReadableName : n.name;
- }
-
- private d3Id(type: string, id: number) {
- return `${type.toLowerCase()}~${id}`;
- }
-
- private toD3Node(n: any): IXosServiceGraphNode {
- return {
- id: this.d3Id(this.getNodeType(n), n.id),
- label: this.getNodeLabel(n),
- model: n,
- type: this.getNodeType(n)
- };
- }
-
- private getServiceInstanceIndexById(l: any, nodes: any[], where: 'source' | 'target'): string {
- if (where === 'source') {
- return _.find(nodes, {id: `serviceinstance~${l.provider_service_instance_id}`});
- }
- else {
- if (l.subscriber_service_id) {
- return _.find(nodes, {id: `service~${l.subscriber_service_id}`});
- }
- else if (l.subscriber_network_id) {
- return _.find(nodes, {id: `network~${l.subscriber_network_id}`});
- }
- else if (l.subscriber_service_instance_id) {
- return _.find(nodes, {id: `serviceinstance~${l.subscriber_service_instance_id}`});
- }
- }
- }
-
- private getOwnerById(id: number, nodes: any[]): any {
- return _.find(nodes, {id: `service~${id}`});
- }
-
- private graphDataToD3(data: IXosServiceInstanceGraphData) {
- try {
- // get all the nodes
- let nodes = _.chain(data.serviceGraph.nodes)
- .map(n => {
- // HACK we are receiving node as d3 models
- return n.model;
- })
- .map(n => {
- return this.toD3Node(n);
- })
- .value();
-
- data.serviceInstances = _.chain(data.serviceInstances)
- .map(n => {
- return this.toD3Node(n);
- })
- .value();
- nodes = nodes.concat(data.serviceInstances);
-
- data.networks = _.chain(data.networks)
- .filter(n => {
- const subscriber = _.findIndex(data.serviceInstanceLinks, {subscriber_network_id: n.id});
- return subscriber > -1;
- })
- .map(n => {
- return this.toD3Node(n);
- })
- .value();
- nodes = nodes.concat(data.networks);
-
- let links = data.serviceGraph.links;
-
- // create the links starting from the coarse ones
- links = _.reduce(data.serviceInstanceLinks, (links, l) => {
- let link = {
- id: `service_instance_link~${l.id}`,
- source: this.getServiceInstanceIndexById(l, nodes, 'source'),
- target: this.getServiceInstanceIndexById(l, nodes, 'target'),
- model: l,
- d3Class: 'service-instance'
- };
- links.push(link);
- return links;
- }, data.serviceGraph.links);
-
- const linksToService = _.reduce(data.serviceInstances, (links, n) => {
- if (angular.isDefined(n.model.owner_id)) {
- let link = {
- id: `owner~${n.id}`,
- source: n,
- target: this.getOwnerById(n.model.owner_id, nodes),
- model: n,
- d3Class: 'owner'
- };
- links.push(link);
- }
- return links;
- }, []);
-
- links = links.concat(linksToService);
-
- let graph: IXosServiceGraph = {
- nodes,
- links
- };
-
- this.d3ServiceInstanceGraph.next(graph);
- } catch (e) {
- this.d3ServiceInstanceGraph.error(e);
- }
- }
-}
diff --git a/src/app/views/dashboard/dashboard.html b/src/app/views/dashboard/dashboard.html
index 93cf2aa..3aa8505 100644
--- a/src/app/views/dashboard/dashboard.html
+++ b/src/app/views/dashboard/dashboard.html
@@ -19,7 +19,7 @@
<!--<h1>Dashboard</h1>-->
<div class="row">
<div class="col-xs-12">
- <xos-coarse-tenancy-graph></xos-coarse-tenancy-graph>
+ <xos-service-graph></xos-service-graph>
</div>
</div>
<div class="row">