[CORD-2719] Refactoring the service graph to use a proper state machine
Change-Id: I5d92aa876c9769701c93b2f5e7d47bdc311b6eb1
diff --git a/src/app/service-graph/components/graph/graph.component.html b/src/app/service-graph/components/graph/graph.component.html
index 6510a9f..ad3d0da 100644
--- a/src/app/service-graph/components/graph/graph.component.html
+++ b/src/app/service-graph/components/graph/graph.component.html
@@ -22,4 +22,26 @@
</div>
<a ng-click="vm.closeFullscreen()" class="close-btn"><i class="fa fa-times"></i></a>
<svg></svg>
+ <div class="row">
+ <div class="col-md-3">
+ <a ng-click="vm.toggleModel('services')" ng-class="{active: vm.currentState >= 0}" class="btn btn-block btn-accent">
+ <span>Services</span>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a ng-click="vm.toggleModel('serviceinstances')" ng-class="{active: vm.currentState >= 1}" class="btn btn-block btn-accent">
+ <span>Service Instances</span>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a ng-click="vm.toggleModel('instances')" ng-class="{active: vm.currentState >= 2}" class="btn btn-block btn-accent">
+ <span>Instances</span>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a ng-click="vm.toggleModel('networks')" ng-class="{active: vm.currentState >= 3}" class="btn btn-block btn-accent">
+ <span>Networks</span>
+ </a>
+ </div>
+ </div>
</div>
diff --git a/src/app/service-graph/components/graph/graph.component.ts b/src/app/service-graph/components/graph/graph.component.ts
index ed36654..b0cabb9 100644
--- a/src/app/service-graph/components/graph/graph.component.ts
+++ b/src/app/service-graph/components/graph/graph.component.ts
@@ -28,6 +28,7 @@
import {IXosNodeRenderer} from '../../services/renderer/node.renderer';
import {IXosSgLink, IXosSgNode} from '../../interfaces';
import {IXosGraphConfig} from '../../services/graph.config';
+import {GraphStates, IXosGraphStateMachine} from '../../services/graph-state-machine';
class XosServiceGraphCtrl {
static $inject = [
@@ -38,14 +39,18 @@
'XosServiceGraphIcons',
'XosNodePositioner',
'XosNodeRenderer',
- 'XosGraphConfig'
+ 'XosGraphConfig',
+ 'XosGraphStateMachine'
];
+ // graph status
+ public currentState: number;
public loader: boolean = true;
private GraphSubscription: Subscription;
private graph: any; // this is the Graph instance
+
// graph element
private svg;
private linkGroup;
@@ -60,10 +65,13 @@
private XosServiceGraphIcons: IXosServiceGraphIcons,
private XosNodePositioner: IXosNodePositioner,
private XosNodeRenderer: IXosNodeRenderer,
- private XosGraphConfig: IXosGraphConfig
+ private XosGraphConfig: IXosGraphConfig,
+ public XosGraphStateMachine: IXosGraphStateMachine
) {
this.$log.info('[XosServiceGraph] Component setup');
+ this.currentState = this.XosGraphStateMachine.getCurrentState();
+
this.XosGraphConfig.setupKeyboardShortcuts();
this.setupSvg();
@@ -74,6 +82,7 @@
graph => {
this.graph = graph;
if (this.graph.nodes().length > 0) {
+ this.$log.info('[XosServiceGraph] Rendering graph: ', this.graph.nodes(), this.graph.edges());
this.renderGraph(this.graph);
}
},
@@ -87,6 +96,13 @@
this.renderGraph(this.graph);
});
+ this.$scope.$on('xos.sg.stateChange', (event, state: number) => {
+ this.$log.info(`[XosServiceGraph] Received event: xos.sg.stateChange. New state is ${state}`);
+ this.$scope.$applyAsync(() => {
+ this.currentState = state;
+ });
+ });
+
$(window).resize(() => {
this.renderGraph(this.graph);
});
@@ -100,17 +116,34 @@
this.XosGraphConfig.toggleFullscreen();
}
+ public toggleModel(modelName: string): void {
+ switch (modelName) {
+ case 'services':
+ this.XosGraphStateMachine.go(GraphStates.Services);
+ break;
+ case 'serviceinstances':
+ this.XosGraphStateMachine.go(GraphStates.ServiceInstances);
+ break;
+ case 'instances':
+ this.XosGraphStateMachine.go(GraphStates.Instances);
+ break;
+ case 'networks':
+ this.XosGraphStateMachine.go(GraphStates.Networks);
+ break;
+ }
+ }
+
private setupSvg() {
this.svg = d3.select('xos-service-graph svg');
this.linkGroup = this.svg.append('g')
.attr({
- class: 'link-group'
+ 'class': 'link-group'
});
this.nodeGroup = this.svg.append('g')
.attr({
- class: 'node-group'
+ 'class': 'node-group'
});
}
@@ -147,6 +180,7 @@
}
private renderGraph(graph: any) {
+
let nodes: IXosSgNode[] = this.XosGraphStore.nodesFromGraph(graph);
let links = this.XosGraphStore.linksFromGraph(graph);
const svgDim = this.getSvgDimensions();
@@ -190,7 +224,7 @@
entering.append('line')
.attr({
id: n => n.id,
- class: n => n.type
+ 'class': n => n.type
});
link.exit().remove();
diff --git a/src/app/service-graph/index.ts b/src/app/service-graph/index.ts
index 0bed24b..334f90a 100644
--- a/src/app/service-graph/index.ts
+++ b/src/app/service-graph/index.ts
@@ -23,6 +23,7 @@
import {XosGraphConfig} from './services/graph.config';
import {XosNodeRenderer} from './services/renderer/node.renderer';
import {XosServiceGraphLegend} from './components/graph-legend/graph-legend.component';
+import {XosGraphStateMachine} from './services/graph-state-machine';
export const xosServiceGraph = 'xosServiceGraph';
@@ -34,6 +35,7 @@
.service('XosServiceGraphIcons', XosServiceGraphIcons)
.service('XosNodePositioner', XosNodePositioner)
.service('XosGraphConfig', XosGraphConfig)
+ .service('XosGraphStateMachine', XosGraphStateMachine)
.service('XosNodeRenderer', XosNodeRenderer)
.component('xosServiceGraph', XosServiceGraph)
.component('xosServiceGraphLegend', XosServiceGraphLegend)
diff --git a/src/app/service-graph/services/graph-state-machine.ts b/src/app/service-graph/services/graph-state-machine.ts
new file mode 100644
index 0000000..6f406af
--- /dev/null
+++ b/src/app/service-graph/services/graph-state-machine.ts
@@ -0,0 +1,144 @@
+/*
+ * 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 {TypeState} from 'typestate';
+import {IXosGraphStore} from './graph.store';
+
+export interface IXosGraphStateMachine {
+ states: GraphStates;
+ go(state: GraphStates): void;
+ getCurrentState(): number;
+}
+
+export enum GraphStates {
+ Services,
+ ServiceInstances,
+ Instances,
+ Networks
+}
+
+export class XosGraphStateMachine {
+ static $inject = [
+ '$log',
+ '$rootScope',
+ 'XosGraphStore'
+ ];
+
+ private graphStateMachine: TypeState.FiniteStateMachine<GraphStates>;
+
+ constructor(
+ private $log: ng.ILogService,
+ private $scope: ng.IRootScopeService,
+ private XosGraphStore: IXosGraphStore
+ ) {
+ this.$log.info(`[XosGraphStateMachine] Creating Graph StateMachine`);
+
+ this.graphStateMachine = new TypeState.FiniteStateMachine<GraphStates>(GraphStates.Services);
+
+ // I want to be able to move between any state in the state machine
+ this.graphStateMachine.fromAny(GraphStates).toAny(GraphStates);
+
+ this.graphStateMachine.onTransition = (from: GraphStates, to: GraphStates) => {
+ this.$log.info(`[XosGraphStateMachine] From ${GraphStates[from]} to ${GraphStates[to]}`);
+
+ const toName = GraphStates[to];
+
+ switch (toName) {
+ case 'Services':
+ if (from > to) {
+ this.removeNetworks();
+ this.removeInstances();
+ this.removeServiceInstances();
+ }
+ break;
+ case 'ServiceInstances':
+ if (from > to) {
+ this.removeNetworks();
+ this.removeInstances();
+ }
+ else {
+ this.addServiceInstances();
+ }
+ break;
+ case 'Instances':
+ if (from > to) {
+ this.removeNetworks();
+ }
+ else {
+ this.addServiceInstances();
+ this.addInstances();
+ }
+ break;
+ case 'Networks':
+ if (from > to) {
+ // this will never happen
+ }
+ else {
+ this.addServiceInstances();
+ this.addInstances();
+ this.addNetworks();
+ }
+ break;
+ }
+ };
+ }
+
+ public go(state: GraphStates) {
+ this.graphStateMachine.go(state);
+
+ this.$scope.$broadcast('xos.sg.stateChange', this.getCurrentState());
+ }
+
+ public getCurrentState(): number {
+ return this.graphStateMachine.currentState;
+ }
+
+ private addServiceInstances() {
+ // add service instances to the graph
+ this.$log.debug(`[XosGraphStateMachine] Add ServiceInstances`);
+ this.XosGraphStore.addServiceInstances();
+ }
+
+ private addInstances () {
+ // add instances to the graph
+ this.$log.debug(`[XosGraphStateMachine] Add Instances`);
+ this.XosGraphStore.addInstances();
+ }
+
+ private addNetworks () {
+ // add networks to the graph
+ this.$log.debug(`[XosGraphStateMachine] Add Networks`);
+ this.XosGraphStore.addNetworks();
+ }
+
+ private removeServiceInstances() {
+ // remove service instances to the graph
+ this.$log.debug(`[XosGraphStateMachine] Remove ServiceInstances`);
+ this.XosGraphStore.removeServiceInstances();
+ }
+
+ private removeInstances () {
+ // remove instances to the graph
+ this.$log.debug(`[XosGraphStateMachine] Remove Instances`);
+ this.XosGraphStore.removeInstances();
+ }
+
+ private removeNetworks () {
+ // remove networks to the graph
+ this.$log.debug(`[XosGraphStateMachine] Remove Networks`);
+ this.XosGraphStore.removeNetworks();
+ }
+}
diff --git a/src/app/service-graph/services/graph.config.ts b/src/app/service-graph/services/graph.config.ts
index cc316a2..8907952 100644
--- a/src/app/service-graph/services/graph.config.ts
+++ b/src/app/service-graph/services/graph.config.ts
@@ -16,8 +16,8 @@
import * as $ from 'jquery';
import {IXosKeyboardShortcutService} from '../../core/services/keyboard-shortcut';
-import {IXosGraphStore} from './graph.store';
import {IXosSidePanelService} from '../../core/side-panel/side-panel.service';
+import {GraphStates, IXosGraphStateMachine} from './graph-state-machine';
export interface IXosGraphConfig {
setupKeyboardShortcuts(): void;
@@ -33,36 +33,9 @@
'$timeout',
'XosSidePanel',
'XosKeyboardShortcut',
- 'XosGraphStore'
+ 'XosGraphStateMachine'
];
- private instanceEnabled = false;
- private instanceBinding = {
- key: 'i',
- modifiers: ['shift'],
- cb: () => {
- // NOTE anytime the graph change the observable is updated,
- // no need to manually retrigger here
- this.XosGraphStore.toggleInstances();
- this.toggleNetworkShortcuts();
- },
- label: 'i',
- description: 'Toggle Instances'
- };
-
- private networkEnabled = false;
- private networkBinding = {
- key: 'n',
- modifiers: ['shift'],
- cb: () => {
- // NOTE anytime the graph change the observable is updated,
- // no need to manually retrigger here
- this.XosGraphStore.toggleNetwork();
- },
- label: 'n',
- description: 'Toggle Networks'
- };
-
constructor (
private $log: ng.ILogService,
private $cookies: ng.cookies.ICookiesService,
@@ -70,7 +43,7 @@
private $timeout: ng.ITimeoutService,
private XosSidePanel: IXosSidePanelService,
private XosKeyboardShortcut: IXosKeyboardShortcutService,
- private XosGraphStore: IXosGraphStore
+ private XosGraphStateMachine: IXosGraphStateMachine
) {
}
@@ -101,14 +74,43 @@
});
this.XosKeyboardShortcut.registerKeyBinding({
+ key: 'c',
+ modifiers: ['shift'],
+ cb: () => {
+ this.XosGraphStateMachine.go(GraphStates.Services);
+ },
+ label: 'c',
+ description: 'Clean the Service graph'
+ });
+
+ this.XosKeyboardShortcut.registerKeyBinding({
key: 's',
modifiers: ['shift'],
cb: () => {
- this.XosGraphStore.toggleServiceInstances();
- this.toggleInstanceShortcuts();
+ this.XosGraphStateMachine.go(GraphStates.ServiceInstances);
},
label: 's',
- description: 'Toggle ServiceInstances'
+ description: 'Show ServiceInstances'
+ });
+
+ this.XosKeyboardShortcut.registerKeyBinding({
+ key: 'i',
+ modifiers: ['shift'],
+ cb: () => {
+ this.XosGraphStateMachine.go(GraphStates.Instances);
+ },
+ label: 'i',
+ description: 'Show Instances'
+ });
+
+ this.XosKeyboardShortcut.registerKeyBinding({
+ key: 'n',
+ modifiers: ['shift'],
+ cb: () => {
+ this.XosGraphStateMachine.go(GraphStates.Networks);
+ },
+ label: 'n',
+ description: 'Show Networks'
});
}
@@ -120,26 +122,4 @@
this.$rootScope.$broadcast('xos.sg.update');
}, 500);
}
-
- private toggleInstanceShortcuts(): void {
- if (!this.instanceEnabled) {
- this.XosKeyboardShortcut.registerKeyBinding(this.instanceBinding);
- }
- else {
- this.XosKeyboardShortcut.removeKeyBinding(this.instanceBinding);
- this.XosKeyboardShortcut.removeKeyBinding(this.networkBinding);
- }
- this.instanceEnabled = !this.instanceEnabled;
- }
-
- private toggleNetworkShortcuts(): void {
- if (!this.networkEnabled) {
- this.XosKeyboardShortcut.registerKeyBinding(this.networkBinding);
- }
- else {
- this.XosKeyboardShortcut.removeKeyBinding(this.networkBinding);
- }
-
- this.networkEnabled = !this.networkEnabled;
- }
}
diff --git a/src/app/service-graph/services/graph.store.spec.ts b/src/app/service-graph/services/graph.store.spec.ts
index a41809c..53f24fc 100644
--- a/src/app/service-graph/services/graph.store.spec.ts
+++ b/src/app/service-graph/services/graph.store.spec.ts
@@ -27,7 +27,6 @@
// state
serviceGraph: Graph;
- serviceInstanceShown: boolean;
// private methods
getNodeId: any;
@@ -35,7 +34,6 @@
addNode: any;
addEdge: any;
nodesFromGraph: any;
- toggleServiceInstances: any;
// observables
ServiceInstanceSubscription: any;
@@ -269,66 +267,57 @@
});
});
- describe(`the toggleServiceInstances method`, () => {
- describe('when they are disabled', () => {
+ describe('the addServiceInstance method', () => {
+ beforeEach(() => {
+ MockModelStore.query.calls.reset();
+ });
- beforeEach(() => {
- MockModelStore.query.calls.reset();
+ it('should add them to the graph', () => {
+ service.addServiceInstances();
+ expect(MockModelStore.query).toHaveBeenCalledWith(`ServiceInstance`, '/core/serviceinstances');
+ expect(MockModelStore.query).toHaveBeenCalledWith(`ServiceInstanceLink`, '/core/serviceinstancelinks');
+ expect(service.ServiceInstanceSubscription).toBeDefined();
+ expect(service.ServiceInstanceLinkSubscription).toBeDefined();
+ // TODO wait for the Observable to return and check the graph
+ });
+ });
+
+ describe('the removeServiceInstance method', () => {
+ beforeEach(() => {
+ service.ServiceInstanceSubscription = {
+ unsubscribe: jasmine.createSpy('ServiceInstanceSubscription')
+ };
+ service.ServiceInstanceLinkSubscription = {
+ unsubscribe: jasmine.createSpy('ServiceInstanceLinkSubscription')
+ };
+
+ service.serviceGraph = new Graph();
+
+ services.forEach(s => {
+ service.addNode(s);
});
- 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();
+ serviceInstances.forEach(si => {
+ service.addNode(si);
+ });
+
+ serviceInstanceLinks.forEach(sil => {
+ service.addEdge(sil);
});
});
- 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.removeServiceInstances();
+ expect(service.ServiceInstanceSubscription.unsubscribe).toHaveBeenCalled();
+ expect(service.ServiceInstanceLinkSubscription.unsubscribe).toHaveBeenCalled();
+ });
- 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);
- });
- });
+ it('should remove ServiceInstance and related nodes/edges from the graph', () => {
+ let filteredGraph = service.removeServiceInstances();
+ 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
index 52100e7..cf438e6 100644
--- a/src/app/service-graph/services/graph.store.ts
+++ b/src/app/service-graph/services/graph.store.ts
@@ -29,9 +29,12 @@
get(): Observable<Graph>;
nodesFromGraph(graph: Graph): IXosSgNode[];
linksFromGraph(graph: Graph): IXosSgLink[];
- toggleServiceInstances(): Graph;
- toggleInstances(): Graph;
- toggleNetwork(): Graph;
+ addServiceInstances(): Graph;
+ removeServiceInstances(): Graph;
+ addInstances(): Graph;
+ removeInstances(): Graph;
+ addNetworks(): Graph;
+ removeNetworks(): Graph;
}
export class XosGraphStore implements IXosGraphStore {
@@ -41,11 +44,6 @@
'XosDebouncer'
];
- // state
- private serviceInstanceShown: boolean = false;
- private instanceShown: boolean = false;
- private networkShown: boolean = false;
-
// graphs
private serviceGraph: any;
private ServiceGraphSubject: BehaviorSubject<any>;
@@ -141,70 +139,47 @@
});
}
- 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
-
- if (this.instanceShown) {
- // NOTE if we remove ServiceInstances we also need to remove Instances
- this.removeElementsFromGraph('instance');
- this.instanceShown = false;
- }
-
- if (this.networkShown) {
- // NOTE if we remove ServiceInstances we also need to remove Networks
- this.removeElementsFromGraph('network');
- this.networkShown = false;
- }
- }
- else {
- // NOTE subscribe to ServiceInstance and ServiceInstanceLink observables
- this.loadServiceInstances();
- this.loadServiceInstanceLinks();
- }
- this.serviceInstanceShown = !this.serviceInstanceShown;
+ public addServiceInstances(): Graph {
+ this.loadServiceInstances();
+ this.loadServiceInstanceLinks();
return this.serviceGraph;
}
- public toggleInstances(): Graph {
- if (this.instanceShown) {
+ public removeServiceInstances(): Graph {
+ // NOTE remove subscriptions
+ this.ServiceInstanceSubscription.unsubscribe();
+ this.ServiceInstanceLinkSubscription.unsubscribe();
- this.InstanceSubscription.unsubscribe();
- this.TenantWithContainerSubscription.unsubscribe();
-
- this.removeElementsFromGraph('instance'); // NOTE links are automatically removed by the graph library
-
- if (this.networkShown) {
- // NOTE if we remove Instances we also need to remove Networks
- this.removeElementsFromGraph('network');
- this.networkShown = false;
- }
- }
- else {
- this.loadInstances();
- this.loadInstanceLinks();
- }
- this.instanceShown = !this.instanceShown;
+ // remove nodes from the graph
+ this.removeElementsFromGraph('serviceinstance');
return this.serviceGraph;
}
- public toggleNetwork() {
- if (this.networkShown) {
- this.NetworkSubscription.unsubscribe();
- this.PortSubscription.unsubscribe();
- this.removeElementsFromGraph('network');
- }
- else {
- this.loadNetworks();
- this.loadPorts(); // Ports define the connection of an Instance to a Network
- }
+ public addInstances(): Graph {
+ this.loadInstances();
+ this.loadInstanceLinks();
+ return this.serviceGraph;
+ }
- this.networkShown = !this.networkShown;
+ public removeInstances(): Graph {
+ this.InstanceSubscription.unsubscribe();
+ this.TenantWithContainerSubscription.unsubscribe();
+
+ this.removeElementsFromGraph('instance');
+
+ return this.serviceGraph;
+ }
+
+ public addNetworks(): Graph {
+ this.loadNetworks();
+ this.loadPorts();
+ return this.serviceGraph;
+ }
+
+ public removeNetworks(): Graph {
+ this.NetworkSubscription.unsubscribe();
+ this.PortSubscription.unsubscribe();
+ this.removeElementsFromGraph('network');
return this.serviceGraph;
}