[CORD-1043] Fine-grained service graph first draft
Change-Id: I16566b0c38dda64fa920120ce16ea699ca157279
diff --git a/src/app/core/services/helpers/debounce.helper.ts b/src/app/core/services/helpers/debounce.helper.ts
index 0534106..89f62ee 100644
--- a/src/app/core/services/helpers/debounce.helper.ts
+++ b/src/app/core/services/helpers/debounce.helper.ts
@@ -12,7 +12,7 @@
}
// wait for 'wait' ms without actions to call the function
- // if 'immediate' call it immediatly then wait for 'wait'
+ // if 'immediate' call it immediately then wait for 'wait'
// NOTE that we cannot use $timeout service to debounce functions as it trigger infiniteDigest Exception
public debounce(func: any, wait: number, context: any, immediate?: boolean) {
let timeout;
diff --git a/src/app/core/services/navigation.spec.ts b/src/app/core/services/navigation.spec.ts
index 82a235e..50af9d9 100644
--- a/src/app/core/services/navigation.spec.ts
+++ b/src/app/core/services/navigation.spec.ts
@@ -49,6 +49,10 @@
{
label: 'Core',
state: 'xos.core'
+ },
+ {
+ label: 'Service Graph',
+ state: 'xos.fine-grained-graph'
}
].concat(mockRoutes);
}));
diff --git a/src/app/core/services/navigation.ts b/src/app/core/services/navigation.ts
index 9869b5a..e41333c 100644
--- a/src/app/core/services/navigation.ts
+++ b/src/app/core/services/navigation.ts
@@ -36,10 +36,10 @@
label: 'Core',
state: 'xos.core'
},
- // {
- // label: 'Service',
- // state: 'xos.services'
- // },
+ {
+ label: 'Service Graph',
+ state: 'xos.fine-grained-graph'
+ },
];
// adding configuration defined routes
// this.routes = StyleConfig.routes.concat(defaultRoutes).reverse();
diff --git a/src/app/service-graph/components/coarse/coarse.component.ts b/src/app/service-graph/components/coarse/coarse.component.ts
index 566698c..d610aa6 100644
--- a/src/app/service-graph/components/coarse/coarse.component.ts
+++ b/src/app/service-graph/components/coarse/coarse.component.ts
@@ -23,7 +23,7 @@
private linkGroup;
private nodeGroup;
- // debounce functions
+ // debounced functions
private renderGraph;
constructor (
@@ -40,6 +40,7 @@
this.CoarseGraphSubscription = this.XosServiceGraphStore.getCoarse()
.subscribe(
(res: IXosServiceGraph) => {
+
// id there are no data, do nothing
if (!res.nodes || res.nodes.length === 0 || !res.links || res.links.length === 0) {
return;
@@ -63,7 +64,6 @@
$onDestroy() {
this.CoarseGraphSubscription.unsubscribe();
- this.XosServiceGraphStore.dispose();
}
private _renderGraph() {
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.html b/src/app/service-graph/components/fine-grained/fine-grained.component.html
new file mode 100644
index 0000000..10e4887
--- /dev/null
+++ b/src/app/service-graph/components/fine-grained/fine-grained.component.html
@@ -0,0 +1,4 @@
+<h1>Fine Grained Tenancy Graph</h1>
+
+<svg>
+</svg>
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.scss b/src/app/service-graph/components/fine-grained/fine-grained.component.scss
new file mode 100644
index 0000000..425b1bf
--- /dev/null
+++ b/src/app/service-graph/components/fine-grained/fine-grained.component.scss
@@ -0,0 +1,55 @@
+@import './../../../style/vars.scss';
+@import '../../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/variables';
+
+xos-fine-grained-tenancy-graph {
+ display: block;
+ // background: $color-accent;
+
+ svg {
+ height: 800px;
+ width: 100%;
+ background-color: $panel-filled-bg;
+ border-radius: 3px;
+ }
+
+ .node-group {
+
+ .node {
+ cursor: pointer;
+ }
+
+ .node {
+ rect, circle {
+ stroke: $color-accent;
+ fill: $background-color;
+ }
+
+ &.network > circle{
+ stroke: blue;
+ }
+
+ &.tenant > rect{
+ stroke: green;
+ }
+
+ &.subscriber > rect{
+ stroke: red;
+ }
+ }
+
+ .node > text {
+ fill: #fff;
+ font-size: 20px;
+ }
+ }
+
+ .link-group {
+ line {
+ stroke: $color-accent;
+ }
+ }
+ .arrow-marker {
+ stroke: $color-accent;
+ fill: $color-accent;
+ }
+}
\ No newline at end of file
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.ts b/src/app/service-graph/components/fine-grained/fine-grained.component.ts
new file mode 100644
index 0000000..b0f71a6
--- /dev/null
+++ b/src/app/service-graph/components/fine-grained/fine-grained.component.ts
@@ -0,0 +1,270 @@
+import {IXosServiceGraphStore} from '../../services/graph.store';
+import './fine-grained.component.scss';
+import * as d3 from 'd3';
+import * as $ from 'jquery';
+import {Subscription} from 'rxjs';
+import {XosServiceGraphConfig as config} from '../../graph.config';
+import {IXosDebouncer} from '../../../core/services/helpers/debounce.helper';
+import {IXosServiceGraph, IXosServiceGraphLink, IXosServiceGraphNode} from '../../interfaces';
+
+class XosFineGrainedTenancyGraphCtrl {
+ static $inject = [
+ '$log',
+ 'XosServiceGraphStore',
+ 'XosDebouncer'
+ ];
+
+ public graph: IXosServiceGraph;
+
+ private GraphSubscription: Subscription;
+ private svg;
+ private forceLayout;
+ private linkGroup;
+ private nodeGroup;
+
+ // debounced functions
+ private renderGraph;
+
+ constructor(
+ private $log: ng.ILogService,
+ private XosServiceGraphStore: IXosServiceGraphStore,
+ private XosDebouncer: IXosDebouncer
+ ) {
+ this.handleSvg();
+ this.setupForceLayout();
+ this.renderGraph = this.XosDebouncer.debounce(this._renderGraph, 500, this);
+
+ $(window).on('resize', () => {
+ this.setupForceLayout();
+ this.renderGraph();
+ });
+
+ this.GraphSubscription = this.XosServiceGraphStore.get()
+ .subscribe(
+ (graph) => {
+
+ if (!graph.nodes || graph.nodes.length === 0 || !graph.links || graph.links.length === 0) {
+ return;
+ }
+
+ this.$log.debug(`[XosFineGrainedTenancyGraphCtrl] Coarse Event and render`, graph);
+ this.graph = graph;
+ this.renderGraph();
+ },
+ (err) => {
+ this.$log.error(`[XosFineGrainedTenancyGraphCtrl] Error: `, err);
+ }
+ );
+ }
+
+ $onDestroy() {
+ this.GraphSubscription.unsubscribe();
+ }
+
+ private _renderGraph() {
+ this.addNodeLinksToForceLayout(this.graph);
+ this.renderNodes(this.graph.nodes);
+ this.renderLinks(this.graph.links);
+ }
+
+ private getSvgDimensions(): {width: number, heigth: number} {
+ return {
+ width: $('xos-fine-grained-tenancy-graph svg').width(),
+ heigth: $('xos-fine-grained-tenancy-graph svg').height()
+ };
+ }
+
+ private handleSvg() {
+ this.svg = d3.select('svg');
+
+ this.linkGroup = this.svg.append('g')
+ .attr({
+ class: 'link-group'
+ });
+
+ this.nodeGroup = this.svg.append('g')
+ .attr({
+ class: 'node-group'
+ });
+ }
+
+ private setupForceLayout() {
+
+ const tick = () => {
+ this.nodeGroup.selectAll('g.node')
+ .attr({
+ transform: d => `translate(${d.x}, ${d.y})`
+ });
+
+ this.linkGroup.selectAll('line')
+ .attr({
+ x1: l => l.source.x || 0,
+ y1: l => l.source.y || 0,
+ x2: l => l.target.x || 0,
+ y2: l => l.target.y || 0,
+ });
+ };
+ const svgDim = this.getSvgDimensions();
+ this.forceLayout = d3.layout.force()
+ .size([svgDim.width, svgDim.heigth])
+ .linkDistance(config.force.linkDistance)
+ .charge(config.force.charge)
+ .gravity(config.force.gravity)
+ .on('tick', tick);
+ }
+
+ private addNodeLinksToForceLayout(data: IXosServiceGraph) {
+ this.forceLayout
+ .nodes(data.nodes)
+ .links(data.links)
+ .start();
+ }
+
+ private getSiblingTextBBox(contex: any /* D3 this */) {
+ return d3.select(contex.parentNode).select('text').node().getBBox();
+ }
+
+ private renderServiceNodes(nodes: any) {
+
+ const self = this;
+ nodes.append('rect')
+ .attr({
+ rx: config.node.radius,
+ ry: config.node.radius
+ });
+
+ nodes.append('text')
+ .attr({
+ 'text-anchor': 'middle'
+ })
+ .text(n => n.label);
+ // .text(n => `${n.id} - ${n.label}`);
+
+ const existing = nodes.selectAll('rect');
+
+ // resize node > rect as contained text
+ existing.each(function() {
+ const textBBox = self.getSiblingTextBBox(this);
+ const rect = d3.select(this);
+ rect.attr({
+ width: textBBox.width + config.node.padding,
+ height: textBBox.height + config.node.padding,
+ x: textBBox.x - (config.node.padding / 2),
+ y: textBBox.y - (config.node.padding / 2)
+ });
+ });
+ }
+
+ private renderTenantNodes(nodes: any) {
+ nodes.append('rect')
+ .attr({
+ width: 40,
+ height: 40,
+ x: -25,
+ y: -25,
+ transform: `rotate(45)`
+ });
+
+ nodes.append('text')
+ .attr({
+ 'text-anchor': 'middle'
+ })
+ .text(n => n.label);
+ }
+
+ private renderNetworkNodes(nodes: any) {
+ const self = this;
+ nodes.append('circle');
+
+ nodes.append('text')
+ .attr({
+ 'text-anchor': 'middle'
+ })
+ .text(n => n.label);
+
+ const existing = nodes.selectAll('circle');
+
+ // resize node > rect as contained text
+ existing.each(function() {
+ const textBBox = self.getSiblingTextBBox(this);
+ const rect = d3.select(this);
+ rect.attr({
+ r: (textBBox.width / 2) + config.node.padding,
+ cy: - (textBBox.height / 4)
+ });
+ });
+ }
+
+ private renderSubscriberNodes(nodes: any) {
+ const self = this;
+ nodes.append('rect');
+
+ nodes.append('text')
+ .attr({
+ 'text-anchor': 'middle'
+ })
+ .text(n => n.label);
+
+ const existing = nodes.selectAll('rect');
+
+ // resize node > rect as contained text
+ existing.each(function() {
+ const textBBox = self.getSiblingTextBBox(this);
+ const rect = d3.select(this);
+ rect.attr({
+ width: textBBox.width + config.node.padding,
+ height: textBBox.height + config.node.padding,
+ x: textBBox.x - (config.node.padding / 2),
+ y: textBBox.y - (config.node.padding / 2)
+ });
+ });
+ }
+
+ private renderNodes(nodes: IXosServiceGraphNode[]) {
+ const node = this.nodeGroup
+ .selectAll('g.node')
+ .data(nodes, n => n.id);
+
+ const svgDim = this.getSvgDimensions();
+ const hStep = svgDim.width / (nodes.length - 1);
+ const vStep = svgDim.heigth / (nodes.length - 1);
+ const entering = node.enter()
+ .append('g')
+ .attr({
+ class: n => `node ${n.type}`,
+ transform: (n, i) => `translate(${hStep * i}, ${vStep * i})`
+ })
+ .call(this.forceLayout.drag)
+ .on('mousedown', () => {
+ d3.event.stopPropagation();
+ })
+ .on('mouseup', (d) => {
+ d.fixed = true;
+ });
+
+ this.renderServiceNodes(entering.filter('.service'));
+ this.renderTenantNodes(entering.filter('.tenant'));
+ this.renderNetworkNodes(entering.filter('.network'));
+ this.renderSubscriberNodes(entering.filter('.subscriber'));
+ }
+
+ private renderLinks(links: IXosServiceGraphLink[]) {
+ const link = this.linkGroup
+ .selectAll('line')
+ .data(links, l => l.id);
+
+ const entering = link.enter();
+
+ entering.append('line')
+ .attr({
+ class: 'link',
+ 'marker-start': 'url(#arrow)'
+ });
+ }
+}
+
+export const XosFineGrainedTenancyGraph: angular.IComponentOptions = {
+ template: require('./fine-grained.component.html'),
+ controllerAs: 'vm',
+ controller: XosFineGrainedTenancyGraphCtrl,
+};
diff --git a/src/app/service-graph/index.ts b/src/app/service-graph/index.ts
index ab9207b..70b255e 100644
--- a/src/app/service-graph/index.ts
+++ b/src/app/service-graph/index.ts
@@ -2,12 +2,21 @@
import {XosServiceGraphStore} from './services/graph.store';
import {xosCore} from '../core/index';
import {XosCoarseTenancyGraph} from './components/coarse/coarse.component';
+import {XosFineGrainedTenancyGraph} from './components/fine-grained/fine-grained.component';
export const xosServiceGraph = 'xosServiceGraph';
angular
.module(xosServiceGraph, [xosDataSources, xosCore])
.service('XosServiceGraphStore', XosServiceGraphStore)
.component('xosCoarseTenancyGraph', XosCoarseTenancyGraph)
+ .component('xosFineGrainedTenancyGraph', XosFineGrainedTenancyGraph)
+ .config(($stateProvider) => {
+ $stateProvider
+ .state('xos.fine-grained-graph', {
+ url: 'tenancy-graph',
+ component: 'xosFineGrainedTenancyGraph',
+ });
+ })
.run(($log: ng.ILogService) => {
$log.info(`[${xosServiceGraph}] Module Setup`);
});
diff --git a/src/app/service-graph/interfaces.ts b/src/app/service-graph/interfaces.ts
index ab80c79..e08bc78 100644
--- a/src/app/service-graph/interfaces.ts
+++ b/src/app/service-graph/interfaces.ts
@@ -1,5 +1,6 @@
export interface IXosServiceModel {
id: number;
+ d3Id?: string;
backend_status: string;
kind: string;
name: string;
@@ -8,6 +9,7 @@
export interface IXosTenantModel {
id: number;
+ d3Id?: string;
backend_status: string;
kind: string;
@@ -17,10 +19,11 @@
// destination
subscriber_service_id: number;
subscriber_tenant_id: number;
- subscriber_user_id: number;
subscriber_root_id: number;
subscriber_network_id: number;
+ subscriber_user_id: number;
+
// extra informations
service_specific_id: string;
service_specific_attribute: string;
@@ -35,13 +38,18 @@
tenants: IXosTenantModel[];
}
+export interface IXosFineGrainedGraphData extends IXosCoarseGraphData {
+ subscribers: IXosServiceModel[];
+ networks: IXosTenantModel[];
+}
+
export interface IXosServiceGraphNodeBadge {
type: 'info'|'success'|'warning'|'danger';
text: string;
}
export interface IXosServiceGraphNode {
- id: number;
+ id: number | string;
label: string;
x?: number;
y?: number;
@@ -49,10 +57,11 @@
py?: number;
badge?: IXosServiceGraphNodeBadge;
model: IXosServiceModel;
+ type: 'service' | 'tenant' | 'network' | 'subscriber';
}
export interface IXosServiceGraphLink {
- id: number;
+ id: number | string;
source: number;
target: number;
model: IXosTenantModel;
diff --git a/src/app/service-graph/services/graph.store.ts b/src/app/service-graph/services/graph.store.ts
index 821da41..0ed0960 100644
--- a/src/app/service-graph/services/graph.store.ts
+++ b/src/app/service-graph/services/graph.store.ts
@@ -3,13 +3,12 @@
import {IXosModelStoreService} from '../../datasources/stores/model.store';
import {
IXosServiceGraph, IXosServiceModel, IXosTenantModel, IXosCoarseGraphData,
- IXosServiceGraphNode, IXosServiceGraphLink
+ IXosServiceGraphNode, IXosServiceGraphLink, IXosFineGrainedGraphData
} from '../interfaces';
import {IXosDebouncer} from '../../core/services/helpers/debounce.helper';
export interface IXosServiceGraphStore {
get(): Observable<IXosServiceGraph>;
getCoarse(): Observable<IXosServiceGraph>;
- dispose(): void;
}
export class XosServiceGraphStore implements IXosServiceGraphStore {
@@ -20,18 +19,22 @@
];
// graph data store
- private graphData: BehaviorSubject<IXosCoarseGraphData> = new BehaviorSubject({
+ private graphData: BehaviorSubject<IXosFineGrainedGraphData> = new BehaviorSubject({
services: [],
- tenants: []
+ tenants: [],
+ networks: [],
+ subscribers: []
});
- // reprentations of the graph as D3 requires
+ // representation of the graph as D3 requires
private d3CoarseGraph = new BehaviorSubject({});
private d3FineGrainedGraph = new BehaviorSubject({});
// storing locally reference to the data model
private services;
private tenants;
+ private subscribers;
+ private networks;
// debounced functions
private handleData;
@@ -39,6 +42,8 @@
// datastore
private ServiceSubscription: Subscription;
private TenantSubscription: Subscription;
+ private SubscriberSubscription: Subscription;
+ private NetworkSubscription: Subscription;
constructor (
private $log: ng.ILogService,
@@ -51,15 +56,16 @@
// 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
+ // TODO get Nodes (model that represent compute nodes in a pod)
+ // TODO get Instances (model that represent deployed VMs)
this.ServiceSubscription = this.XosModelStore.query('Service', '/core/services')
.subscribe(
(res) => {
this.combineData(res, 'services');
},
(err) => {
- this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
+ this.$log.error(`[XosServiceGraphStore] Service Observable: `, err);
}
);
@@ -69,31 +75,41 @@
this.combineData(res, 'tenants');
},
(err) => {
- this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
+ this.$log.error(`[XosServiceGraphStore] Tenant Observable: `, err);
}
);
- // observe graphData and build Coarse or FineGrained graphs (based on who's subscribed)
- this.graphData
+ this.SubscriberSubscription = this.XosModelStore.query('Subscriber', '/core/subscribers')
.subscribe(
- (res: IXosCoarseGraphData) => {
- if (this.d3CoarseGraph.observers.length > 0) {
- this.graphDataToCoarseGraph(res);
- }
- if (this.d3FineGrainedGraph.observers.length > 0) {
- // TODO graphDataToFineGrainedGraph
- }
+ (res) => {
+ this.combineData(res, 'subscribers');
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] Subscriber Observable: `, err);
+ }
+ );
+
+ this.NetworkSubscription = this.XosModelStore.query('Network', '/core/networks')
+ .subscribe(
+ (res) => {
+ this.combineData(res, 'networks');
},
(err) => {
this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
}
);
- }
- public dispose() {
- // cancel subscriptions from observables
- this.ServiceSubscription.unsubscribe();
- this.TenantSubscription.unsubscribe();
+ // observe graphData and build Coarse and FineGrained graphs
+ this.graphData
+ .subscribe(
+ (res: IXosFineGrainedGraphData) => {
+ this.graphDataToCoarseGraph(res);
+ this.graphDataToFineGrainedGraph(res);
+ },
+ (err) => {
+ this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
+ }
+ );
}
public get() {
@@ -104,7 +120,7 @@
return this.d3CoarseGraph.asObservable();
}
- private combineData(data: any, type: 'services'|'tenants') {
+ private combineData(data: any, type: 'services'|'tenants'|'subscribers'|'networks') {
switch (type) {
case 'services':
this.services = data;
@@ -112,6 +128,12 @@
case 'tenants':
this.tenants = data;
break;
+ case 'subscribers':
+ this.subscribers = data;
+ break;
+ case 'networks':
+ this.networks = data;
+ break;
}
this.handleData(this.services, this.tenants);
}
@@ -119,23 +141,71 @@
private _handleData(services: IXosServiceModel[], tenants: IXosTenantModel[]) {
this.graphData.next({
services: this.services,
- tenants: this.tenants
+ tenants: this.tenants,
+ subscribers: this.subscribers,
+ networks: this.networks
});
}
- private getCoarseNodeIndexById(id: number, nodes: IXosServiceModel[]) {
+ private getNodeIndexById(id: number | string, nodes: IXosServiceModel[]) {
return _.findIndex(nodes, {id: id});
}
+ private d3Id(type: string, id: number) {
+ return `${type.toLowerCase()}~${id}`;
+ }
+
+ private getTargetId(tenant: IXosTenantModel) {
+
+ let targetId;
+ if (tenant.subscriber_service_id) {
+ targetId = this.d3Id('service', tenant.subscriber_service_id);
+ }
+ else if (tenant.subscriber_tenant_id) {
+ targetId = this.d3Id('tenant', tenant.subscriber_tenant_id);
+ }
+ else if (tenant.subscriber_network_id) {
+ targetId = this.d3Id('network', tenant.subscriber_network_id);
+ }
+ else if (tenant.subscriber_root_id) {
+ targetId = this.d3Id('subscriber', tenant.subscriber_root_id);
+ }
+ return targetId;
+ }
+
+ private getSourceId(tenant: IXosTenantModel) {
+ return this.d3Id('service', tenant.provider_service_id);
+ }
+
+ private getNodeType(n: any) {
+ return n.class_names.split(',')[0].toLowerCase();
+ }
+
+ private getNodeLabel(n: any) {
+ if (this.getNodeType(n) === 'tenant') {
+ return n.id;
+ }
+ return n.humanReadableName ? n.humanReadableName : n.name;
+ }
+
+ private removeUnwantedFineGrainedData(data: IXosFineGrainedGraphData): IXosFineGrainedGraphData {
+ data.tenants = _.filter(data.tenants, t => t.kind !== 'coarse');
+ data.networks = _.filter(data.networks, n => {
+ const subscriber = _.findIndex(data.tenants, {subscriber_network_id: n.id});
+ return subscriber > -1;
+ });
+ return data;
+ }
+
private graphDataToCoarseGraph(data: IXosCoarseGraphData) {
- // TODO find how to bind source/target by node ID and not by position in array (ask Simon?)
+
const links: IXosServiceGraphLink[] = _.chain(data.tenants)
.filter((t: IXosTenantModel) => t.kind === 'coarse')
.map((t: IXosTenantModel) => {
return {
id: t.id,
- source: this.getCoarseNodeIndexById(t.provider_service_id, data.services),
- target: this.getCoarseNodeIndexById(t.subscriber_service_id, data.services),
+ source: this.getNodeIndexById(t.provider_service_id, data.services),
+ target: this.getNodeIndexById(t.subscriber_service_id, data.services),
model: t
};
})
@@ -155,4 +225,61 @@
});
}
+ private graphDataToFineGrainedGraph(data: IXosFineGrainedGraphData) {
+
+ data = this.removeUnwantedFineGrainedData(data);
+
+ let nodes = _.reduce(Object.keys(data), (list: any[], k: string) => {
+ return list.concat(data[k]);
+ }, []);
+
+ nodes = _.chain(nodes)
+ .map(n => {
+ n.d3Id = this.d3Id(this.getNodeType(n), n.id);
+ return n;
+ })
+ .map(n => {
+ let node: IXosServiceGraphNode = {
+ id: n.d3Id,
+ label: this.getNodeLabel(n),
+ model: n,
+ type: this.getNodeType(n)
+ };
+ return node;
+ })
+ .value();
+
+ const links = _.reduce(data.tenants, (links: IXosServiceGraphLink[], tenant: IXosTenantModel) => {
+ const sourceId = this.getSourceId(tenant);
+ const targetId = this.getTargetId(tenant);
+
+ const tenantToProvider = {
+ id: `${sourceId}_${tenant.d3Id}`,
+ source: this.getNodeIndexById(sourceId, nodes),
+ target: this.getNodeIndexById(tenant.d3Id, nodes),
+ model: tenant
+ };
+
+ const tenantToSubscriber = {
+ id: `${tenant.d3Id}_${targetId}`,
+ source: this.getNodeIndexById(tenant.d3Id, nodes),
+ target: this.getNodeIndexById(targetId, nodes),
+ model: tenant
+ };
+
+ links.push(tenantToProvider);
+ links.push(tenantToSubscriber);
+ return links;
+ }, []);
+
+ if (nodes.length === 0 || links.length === 0) {
+ return;
+ }
+
+ this.d3FineGrainedGraph.next({
+ nodes: nodes,
+ links: links
+ });
+ }
+
}
diff --git a/src/index.scss b/src/index.scss
index 08318a1..6a1f8e3 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -10,6 +10,15 @@
min-height: 100%;
}
+body {
+ min-height: 90%;
+}
+
+ui-view, xos, .wrapper, .content {
+ display: block;
+ min-height: 100%;
+}
+
.content > div {
opacity: 1 !important;
}