Added d3 force graph to GUI2 topology
Change-Id: I6860472efaf51ea27fad74e630e687f0c6abad3d
diff --git a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
index d159f06..97c9e1e 100644
--- a/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
+++ b/web/gui2/src/main/webapp/app/view/topology/layer/forcesvg/forcesvg.component.ts
@@ -13,143 +13,189 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {Component, Input, OnInit} from '@angular/core';
-import { LocationType } from '../backgroundsvg/backgroundsvg.component';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ HostListener,
+ Input,
+ OnChanges,
+ OnInit,
+ Output, QueryList, SimpleChange,
+ SimpleChanges, ViewChildren
+} from '@angular/core';
+import {IconService, LogService} from 'gui2-fw-lib';
+import {
+ Device,
+ ForceDirectedGraph,
+ Host,
+ LabelToggle,
+ LayerType,
+ Region,
+ RegionLink,
+ SubRegion
+} from './models';
+import {DeviceNodeSvgComponent} from './visuals';
-/**
- * Enum of the topo2CurrentRegion node type from SubRegion below
- */
-export enum NodeType {
- REGION = 'region',
- DEVICE = 'device'
-}
-
-/**
- * Enum of the topo2CurrentRegion layerOrder from Region below
- */
-export enum LayerOrder {
- LAYER_OPTICAL = 'opt',
- LAYER_PACKET = 'pkt',
- LAYER_DEFAULT = 'def'
-}
-
-/**
- * model of the topo2CurrentRegion location from SubRegion below
- */
-export interface Location {
- locType: LocationType;
- latOrY: number;
- longOrX: number;
-}
-
-/**
- * model of the topo2CurrentRegion props from SubRegion below
- */
-export interface RegionProps {
- latitude: number;
- longitude: number;
- name: string;
- peerLocations: string;
-}
-
-/**
- * model of the topo2CurrentRegion subregion from Region below
- */
-export interface SubRegion {
- id: string;
- location: Location;
- nDevs: number;
- nHosts: number;
- name: string;
- nodeType: NodeType;
- props: RegionProps;
-}
-
-export enum LinkType {
- UiRegionLink,
- UiDeviceLink
-}
-
-/**
- * model of the topo2CurrentRegion region rollup from Region below
- */
-export interface RegionRollup {
- id: string;
- epA: string;
- epB: string;
- portA: string;
- portB: string;
- type: LinkType;
-}
-
-/**
- * model of the topo2CurrentRegion region link from Region below
- */
-export interface RegionLink {
- id: string;
- epA: string;
- epB: string;
- rollup: RegionRollup[];
- type: LinkType;
-}
-
-/**
- * model of the topo2CurrentRegion device props from Device below
- */
-export interface DeviceProps {
- latitude: number;
- longitude: number;
- name: string;
- locType: LocationType;
-}
-
-export interface Device {
- id: string;
- layer: LayerOrder;
- location: LocationType;
- master: string;
- nodeType: NodeType;
- online: boolean;
- props: DeviceProps;
- type: string;
-}
-
-/**
- * model of the topo2CurrentRegion WebSocket response
- */
-export interface Region {
- note?: string;
- id: string;
- devices: Device[][];
- hosts: Object[];
- links: RegionLink[];
- layerOrder: LayerOrder[];
- peerLocations?: Location[];
- subregions: SubRegion[];
-}
-
-/**
- * model of the topo2PeerRegions WebSocket response
- */
-export interface Peer {
- peers: SubRegion[];
-}
/**
* ONOS GUI -- Topology Forces Graph Layer View.
+ *
+ * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
+ * This drives the whole Force graph
*/
@Component({
selector: '[onos-forcesvg]',
templateUrl: './forcesvg.component.html',
- styleUrls: ['./forcesvg.component.css']
+ styleUrls: ['./forcesvg.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class ForceSvgComponent implements OnInit {
+export class ForceSvgComponent implements OnInit, OnChanges {
@Input() onosInstMastership: string = '';
- regionData: Region;
+ @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
+ @Output() linkSelected = new EventEmitter<RegionLink>();
+ @Output() selectedNodeEvent = new EventEmitter<Device>();
+ @Input() selectedLink: RegionLink = null;
+ private graph: ForceDirectedGraph;
- constructor() { }
+ @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
+ private _options: { width, height } = { width: 800, height: 600 };
- ngOnInit() {
+ @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
+
+ @HostListener('window:resize', ['$event'])
+ onResize(event) {
+ this.graph.initSimulation(this.options);
+ this.log.debug('Simulation reinit after resize', event);
}
+ constructor(
+ protected log: LogService,
+ protected is: IconService,
+ private ref: ChangeDetectorRef
+ ) {
+ this.selectedLink = null;
+ this.log.debug('ForceSvgComponent constructed');
+ }
+
+ /**
+ * After the component is initialized create the Force simulation
+ */
+ ngOnInit() {
+ // Receiving an initialized simulated graph from our custom d3 service
+ this.graph = new ForceDirectedGraph(this.options);
+
+ /** Binding change detection check on each tick
+ * This along with an onPush change detection strategy should enforce checking only when relevant!
+ * This improves scripting computation duration in a couple of tests I've made, consistently.
+ * Also, it makes sense to avoid unnecessary checks when we are dealing only with simulations data binding.
+ */
+ this.graph.ticker.subscribe((simulation) => {
+ // this.log.debug("Force simulation has ticked", simulation);
+ this.ref.markForCheck();
+ });
+ this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
+
+ this.is.loadIconDef('m_switch');
+ }
+
+ /**
+ * When any one of the inputs get changed by a containing component, this gets called automatically
+ * In addition this is called manually by topology.service when a response
+ * is received from the WebSocket from the server
+ *
+ * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
+ * The Links are added to the Link list of the simulation.
+ * Before they are added the Links are associated with Nodes based on their endPt
+ *
+ * @param changes - a list of changed @Input(s)
+ */
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['regionData']) {
+ const devices: Device[] =
+ changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
+ const hosts: Host[] =
+ changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
+ const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
+ this.graph.nodes = [];
+ if (devices) {
+ this.graph.nodes = devices;
+ }
+ if (hosts) {
+ this.graph.nodes = this.graph.nodes.concat(hosts);
+ }
+ if (subRegions) {
+ this.graph.nodes = this.graph.nodes.concat(subRegions);
+ }
+
+ // Associate the endpoints of each link with a real node
+ this.graph.links = [];
+ for (const linkIdx of Object.keys(this.regionData.links)) {
+ this.regionData.links[linkIdx].source =
+ this.graph.nodes.find((node) =>
+ node.id === this.regionData.links[linkIdx].epA);
+ this.regionData.links[linkIdx].target =
+ this.graph.nodes.find((node) =>
+ node.id === this.regionData.links[linkIdx].epB);
+ this.regionData.links[linkIdx].index = Number(linkIdx);
+ }
+
+ this.graph.links = this.regionData.links;
+
+ this.graph.initSimulation(this.options);
+ this.graph.initNodes();
+ this.log.debug('ForceSvgComponent input changed',
+ this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
+ }
+ }
+
+ /**
+ * Get the index of LayerType so it can drive the visibility of nodes and
+ * hosts on layers
+ */
+ visibleLayerIdx(): number {
+ const layerKeys: string[] = Object.keys(LayerType);
+ for (const idx in layerKeys) {
+ if (LayerType[layerKeys[idx]] === this.visibleLayer) {
+ return Number(idx);
+ }
+ }
+ return -1;
+ }
+
+ selectLink(link: RegionLink): void {
+ this.selectedLink = link;
+ this.linkSelected.emit(link);
+ }
+
+ get options() {
+ return this._options = {
+ width: window.innerWidth,
+ height: window.innerHeight
+ };
+ }
+
+ updateDeviceLabelToggle() {
+ this.devices.forEach((d) => {
+ const old: LabelToggle = d.labelToggle;
+ const next = LabelToggle.next(old);
+ d.ngOnChanges({'labelToggle': new SimpleChange(old, next, false)});
+ });
+ }
+
+ updateSelected(selectedNodeId: string): void {
+ this.log.debug('Device selected', selectedNodeId);
+ this.devices.filter((d) => d.device.id !== selectedNodeId).forEach((d) => {
+ d.deselect();
+ });
+ const selectedDevice: DeviceNodeSvgComponent =
+ (this.devices.find((d) => d.device.id === selectedNodeId));
+ if (selectedDevice) {
+ this.selectedNodeEvent.emit(selectedDevice.device);
+ } else {
+ this.selectedNodeEvent.emit();
+ }
+ }
}
+