[CORD-2424] Adding Instances and Networks to the graph
Change-Id: Ib30081f4995930d979447af59124896f1308f54d
diff --git a/src/app/service-graph/components/graph/graph.component.scss b/src/app/service-graph/components/graph/graph.component.scss
index 40b6f11..5d773a1 100644
--- a/src/app/service-graph/components/graph/graph.component.scss
+++ b/src/app/service-graph/components/graph/graph.component.scss
@@ -73,12 +73,12 @@
.node {
cursor: pointer;
+ fill: $background-color;
}
.node.service {
> rect {
stroke: $color-accent;
- fill: $background-color;
}
> .icon {
fill: $color-accent;
@@ -87,11 +87,28 @@
.node.serviceinstance {
> rect {
- stroke: green;
- fill: $background-color;
+ stroke: $serviceinstances-fg;
}
> .icon {
- fill: green;
+ fill: $serviceinstances-fg;
+ }
+ }
+
+ .node.instance {
+ > rect {
+ stroke: $instances-fg;
+ }
+ > .icon {
+ fill: $instances-fg;
+ }
+ }
+
+ .node.network {
+ > rect {
+ stroke: $networks-fg;
+ }
+ > .icon {
+ fill: $networks-fg;
}
}
@@ -114,12 +131,21 @@
}
line.ownership {
- stroke: green;
+ stroke: $serviceinstances-fg;
stroke-dasharray: 5;
}
line.serviceinstancelink {
- stroke: green;
+ stroke: $serviceinstances-fg;
+ }
+
+ line.instance_ownership {
+ stroke: $instances-fg;
+ stroke-dasharray: 5;
+ }
+
+ line.port {
+ stroke: $networks-fg;
}
}
.arrow-marker {
diff --git a/src/app/service-graph/components/graph/graph.component.ts b/src/app/service-graph/components/graph/graph.component.ts
index 5f84384..ed36654 100644
--- a/src/app/service-graph/components/graph/graph.component.ts
+++ b/src/app/service-graph/components/graph/graph.component.ts
@@ -26,7 +26,7 @@
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 {IXosSgLink, IXosSgNode} from '../../interfaces';
import {IXosGraphConfig} from '../../services/graph.config';
class XosServiceGraphCtrl {
@@ -74,7 +74,6 @@
graph => {
this.graph = graph;
if (this.graph.nodes().length > 0) {
- this.loader = false;
this.renderGraph(this.graph);
}
},
@@ -154,6 +153,7 @@
this.XosNodePositioner.positionNodes(svgDim, nodes)
.then((nodes: IXosSgNode[]) => {
+ this.loader = false;
this.forceLayout
.nodes(nodes)
@@ -162,6 +162,15 @@
.linkDistance(config.force.linkDistance)
.charge(config.force.charge)
.gravity(config.force.gravity)
+ .linkStrength((link: IXosSgLink) => {
+ switch (link.type) {
+ case 'ownership':
+ case 'instance_ownership':
+ // NOTE make "ownsership" links stronger than other for positioning
+ return 1;
+ }
+ return 0.1;
+ })
.start();
// render nodes
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
index 9baeb57..de03a62 100644
--- a/src/app/service-graph/services/d3-helpers/graph-icons.service.ts
+++ b/src/app/service-graph/services/d3-helpers/graph-icons.service.ts
@@ -34,6 +34,18 @@
'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: ''
+ },
+ {
+ name: 'instance',
+ 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: ''
+ },
+ {
+ name: 'network',
+ path: 'M15.62,1.56a4.69,4.69,0,0,1,4.69,4.63,2.83,2.83,0,0,0,0,.29l0,1.15L21.33,8a3.11,3.11,0,0,1-1,6.06H4.69a3.13,3.13,0,0,1-.05-6.25l.22,0,1.19.07.39-1.12a3.12,3.12,0,0,1,2.94-2.1,3,3,0,0,1,.54.06l1.1.19.54-1a4.7,4.7,0,0,1,4.06-2.41m0-1.56A6.2,6.2,0,0,0,10.2,3.21a4.48,4.48,0,0,0-.82-.09A4.67,4.67,0,0,0,5,6.28l-.28,0a4.69,4.69,0,1,0,0,9.37H20.31a4.67,4.67,0,0,0,1.54-9.09c0-.1,0-.19,0-.28A6.25,6.25,0,0,0,15.62,0Z',
+ transform: ''
}
];
diff --git a/src/app/service-graph/services/graph.config.ts b/src/app/service-graph/services/graph.config.ts
index 7db2d97..e6ddfbd 100644
--- a/src/app/service-graph/services/graph.config.ts
+++ b/src/app/service-graph/services/graph.config.ts
@@ -34,6 +34,33 @@
'XosGraphStore'
];
+ 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,
@@ -64,13 +91,13 @@
key: 's',
modifiers: ['shift'],
cb: () => {
- // NOTE anytime the graph change the observable is updated,
- // no need to manually retrigger here
this.XosGraphStore.toggleServiceInstances();
+ this.toggleInstanceShortcuts();
},
label: 's',
description: 'Toggle ServiceInstances'
});
+
}
public toggleFullscreen() {
@@ -80,4 +107,26 @@
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.ts b/src/app/service-graph/services/graph.store.ts
index 4d46c88..52100e7 100644
--- a/src/app/service-graph/services/graph.store.ts
+++ b/src/app/service-graph/services/graph.store.ts
@@ -30,6 +30,8 @@
nodesFromGraph(graph: Graph): IXosSgNode[];
linksFromGraph(graph: Graph): IXosSgLink[];
toggleServiceInstances(): Graph;
+ toggleInstances(): Graph;
+ toggleNetwork(): Graph;
}
export class XosGraphStore implements IXosGraphStore {
@@ -41,16 +43,22 @@
// state
private serviceInstanceShown: boolean = false;
+ private instanceShown: boolean = false;
+ private networkShown: boolean = false;
// graphs
private serviceGraph: any;
private ServiceGraphSubject: BehaviorSubject<any>;
// datastore
+ private InstanceSubscription: Subscription;
+ private NetworkSubscription: Subscription;
+ private PortSubscription: Subscription;
private ServiceSubscription: Subscription;
private ServiceDependencySubscription: Subscription;
private ServiceInstanceSubscription: Subscription;
private ServiceInstanceLinkSubscription: Subscription;
+ private TenantWithContainerSubscription: Subscription;
// debounced
private efficientNext = this.XosDebouncer.debounce(this.callNext, 500, this, false);
@@ -76,6 +84,7 @@
public nodesFromGraph(graph: Graph): IXosSgNode[] {
return _.map(graph.nodes(), (n: string) => {
const nodeData = graph.node(n);
+
return {
id: n,
type: this.getModelType(nodeData),
@@ -87,16 +96,9 @@
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) {
@@ -105,17 +107,28 @@
targetId = this.getServiceId(link.provider_service_id);
break;
case 'serviceinstancelink':
+ // NOTE ServiceInstanceLink can actually also connect to a service and a network
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);
+ break;
+ case 'instance_ownership':
+ sourceId = this.getServiceInstanceId(link.id);
+ targetId = this.getInstanceId(link.instance_id);
+ break;
+ case 'port':
+ sourceId = this.getInstanceId(link.instance_id);
+ targetId = this.getNetworkId(link.network_id);
+ break;
}
// NOTE help while debugging
if (!sourceId || !targetId) {
this.$log.warn(`Link ${l.v}-${l.w} has missing source or target:`, l, link);
+ // TODO return null and then filter out so that we don't break the rendering
}
return {
@@ -136,6 +149,18 @@
// 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
@@ -146,6 +171,43 @@
return this.serviceGraph;
}
+ public toggleInstances(): Graph {
+ if (this.instanceShown) {
+
+ 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;
+ 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
+ }
+
+ this.networkShown = !this.networkShown;
+ return this.serviceGraph;
+ }
+
public get(): Observable<Graph> {
return this.ServiceGraphSubject.asObservable();
}
@@ -194,6 +256,22 @@
this.serviceGraph.setEdge(sourceId, targetId, link);
}
+ private addInstanceOwner(tenantWithContainer: any) {
+ // NOTE some TenantWithContainer don't have an instance
+ if (tenantWithContainer.instance_id) {
+ const sourceId = this.getServiceInstanceId(tenantWithContainer.id);
+ const targetId = this.getInstanceId(tenantWithContainer.instance_id);
+ this.serviceGraph.setEdge(sourceId, targetId, angular.merge(tenantWithContainer, {type: 'instance_ownership'}));
+ }
+ }
+
+ private addNetworkLink(port: any) {
+ // ports are connected to 1 Instance and 1 Network
+ const sourceId = this.getInstanceId(port.instance_id);
+ const targetId = this.getNetworkId(port.network_id);
+ this.serviceGraph.setEdge(sourceId, targetId, angular.merge(port, {type: 'port'}));
+ }
+
private removeElementsFromGraph(type: string) {
_.forEach(this.serviceGraph.nodes(), (n: string) => {
const node = this.serviceGraph.node(n);
@@ -209,7 +287,7 @@
// helpers
private getModelType(node: IXosBaseModel): string {
if (node.type) {
- // NOTE we'll add "ownership" links
+ // NOTE handling "ownership" links
return node.type;
}
return node.class_names.split(',')[0].toLowerCase();
@@ -223,6 +301,14 @@
return `serviceinstance~${id}`;
}
+ private getInstanceId(id: number): string {
+ return `instance~${id}`;
+ }
+
+ private getNetworkId(id: number): string {
+ return `network~${id}`;
+ }
+
private getNodeId(node: IXosBaseModel): string {
const nodeType = this.getModelType(node);
@@ -231,6 +317,10 @@
return this.getServiceId(node.id);
case 'serviceinstance':
return this.getServiceInstanceId(node.id);
+ case 'instance':
+ return this.getInstanceId(node.id);
+ case 'network':
+ return this.getNetworkId(node.id);
}
}
@@ -303,6 +393,74 @@
);
}
+ private loadInstances() {
+ this.InstanceSubscription = this.XosModelStore.query('Instance', '/core/instances')
+ .subscribe(
+ (res) => {
+ if (res.length > 0) {
+ _.forEach(res, n => {
+ this.addNode(n);
+ });
+ this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+ }
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] Instance Observable: `, err);
+ }
+ );
+ }
+
+ private loadInstanceLinks() {
+ this.TenantWithContainerSubscription = this.XosModelStore.query('TnantWithContainer', '/core/tenantwithcontainers')
+ .subscribe(
+ (res) => {
+ if (res.length > 0) {
+ _.forEach(res, n => {
+ this.addInstanceOwner(n);
+ });
+ this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+ }
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] Instance Observable: `, err);
+ }
+ );
+ }
+
+ private loadNetworks() {
+ this.NetworkSubscription = this.XosModelStore.query('Network', '/core/networks')
+ .subscribe(
+ (res) => {
+ if (res.length > 0) {
+ _.forEach(res, n => {
+ this.addNode(n);
+ });
+ this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+ }
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] Network Observable: `, err);
+ }
+ );
+ }
+
+ private loadPorts() {
+ this.PortSubscription = this.XosModelStore.query('Port', '/core/ports')
+ .subscribe(
+ (res) => {
+ if (res.length > 0) {
+ _.forEach(res, n => {
+ this.addNetworkLink(n);
+ });
+ this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+ }
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] Network Observable: `, err);
+ }
+ );
+ }
+
private callNext(subject: BehaviorSubject<any>, data: any) {
subject.next(data);
}
diff --git a/src/app/service-graph/services/renderer/node.renderer.ts b/src/app/service-graph/services/renderer/node.renderer.ts
index fb29046..18719f1 100644
--- a/src/app/service-graph/services/renderer/node.renderer.ts
+++ b/src/app/service-graph/services/renderer/node.renderer.ts
@@ -60,6 +60,8 @@
this.renderServiceNodes(entering.filter('.service'));
this.renderServiceInstanceNodes(entering.filter('.serviceinstance'));
+ this.renderInstanceNodes(entering.filter('.instance'));
+ this.renderNetworkNodes(entering.filter('.network'));
node.exit().remove();
}
@@ -106,6 +108,46 @@
this.handleLabels(nodes); // eventually improve, padding top is wrong
}
+ private renderInstanceNodes(nodes: d3.selection) {
+ nodes
+ .append('rect')
+ .attr({
+ rx: config.node.radius,
+ ry: config.node.radius
+ });
+
+ nodes
+ .append('path')
+ .attr({
+ d: this.XosServiceGraphIcons.get('instance').path,
+ transform: this.XosServiceGraphIcons.get('instance').transform,
+ class: 'icon'
+ });
+
+ this.positionServiceNodeGroup(nodes);
+ this.handleLabels(nodes);
+ }
+
+ private renderNetworkNodes(nodes: d3.selection) {
+ nodes
+ .append('rect')
+ .attr({
+ rx: config.node.radius,
+ ry: config.node.radius
+ });
+
+ nodes
+ .append('path')
+ .attr({
+ d: this.XosServiceGraphIcons.get('network').path,
+ transform: this.XosServiceGraphIcons.get('network').transform,
+ class: 'icon'
+ });
+
+ this.positionServiceNodeGroup(nodes);
+ this.handleLabels(nodes);
+ }
+
private positionServiceNodeGroup(nodes: d3.selection) {
const self = this;
nodes.each(function (d: IXosSgNode) {
@@ -231,6 +273,7 @@
}
private getNodeLabel(n: any): string {
+ // NOTE for 'instances' display instance_name instead of name?
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;
}