[CORD-1943] New service graph
- labels
- enforcing service position
- started documentation
- toggling service instances
- toggle fullscreen
Change-Id: I01b71fb2607fb58711d4624f6b5b6479609b2f4f
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);
- }
- }
-}