GUI2 Topo Allow selection of multiple nodes at once

Change-Id: I0bb226d4697e3df49da0a049d440a70aed172263
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.ts
index 85282e1..21ad66e 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.ts
@@ -13,22 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {
-    Component,
-    Input,
-    OnChanges,
-    OnDestroy,
-    OnInit,
-    SimpleChanges
-} from '@angular/core';
+import {Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges} from '@angular/core';
 import {animate, state, style, transition, trigger} from '@angular/animations';
-import {
-    DetailsPanelBaseImpl,
-    FnService, LionService,
-    LogService,
-    WebSocketService
-} from 'gui2-fw-lib';
-import {Host, Link, LinkType, UiElement} from '../../layer/forcesvg/models';
+import {DetailsPanelBaseImpl, FnService, LionService, LogService, WebSocketService} from 'gui2-fw-lib';
+import {Host, Link, LinkType, NodeType, UiElement} from '../../layer/forcesvg/models';
 import {Params, Router} from '@angular/router';
 
 
@@ -68,6 +56,22 @@
     tt: 'tt_ctl_show_pipeconf',
     path: 'pipeconf',
 };
+const RELATEDINTENTS: ButtonAttrs = {
+    gid: 'm_relatedIntents',
+    tt: 'tr_btn_show_related_traffic',
+    path: 'relatedIntents',
+};
+const CREATEHOSTTOHOSTFLOW: ButtonAttrs = {
+    gid: 'm_endstation',
+    tt: 'tr_btn_create_h2h_flow',
+    path: 'create_h2h_flow',
+};
+const CREATEMULTISOURCEFLOW: ButtonAttrs = {
+    gid: 'm_flows',
+    tt: 'tr_btn_create_msrc_flow',
+    path: 'create_msrc_flow',
+};
+
 
 interface ShowDetails {
     buttons: string[];
@@ -83,6 +87,12 @@
  * ONOS GUI -- Topology Details Panel.
  * Displays details of selected device. When no device is selected the panel slides
  * off to the side and disappears
+ *
+ * This Panel is a child of the Topology component and it gets the 'selectedNodes'
+ * from there as an input component. See TopologyComponent.nodeSelected()
+ * The topology component gets these by listening to events from ForceSvgComponent
+ * which gets them in turn from Device, Host, SubRegion and Link components. This
+ * is so that each component respects the hierarchy
  */
 @Component({
     selector: 'onos-details',
@@ -108,11 +118,12 @@
     ]
 })
 export class DetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
-    @Input() selectedNode: UiElement = undefined; // Populated when user selects node or link
+    @Input() selectedNodes: UiElement[] = []; // Populated when user selects node or link
     @Input() on: boolean = false; // Override the parent class attribute
 
     // deferred localization strings
-    lionFn; // Function
+    lionFnTopo; // Function
+    lionFnFlow; // Function for flow bundle
     showDetails: ShowDetails; // Will be populated on callback. Cleared if nothing is selected
 
     constructor(
@@ -125,8 +136,9 @@
         super(fs, log, wss, 'topo');
 
         if (this.lion.ubercache.length === 0) {
-            this.lionFn = this.dummyLion;
-            this.lion.loadCbs.set('flow', () => this.doLion());
+            this.lionFnTopo = this.dummyLion;
+            this.lionFnFlow = this.dummyLion;
+            this.lion.loadCbs.set('detailscore', () => this.doLion());
         } else {
             this.doLion();
         }
@@ -160,32 +172,75 @@
 
     /**
      * If changes are detected on the Input param selectedNode, call on WSS sendEvent
+     * and expect ShowDetails to be updated from data sent back from server.
+     *
      * Note the difference in call to the WSS with requestDetails between a node
      * and a link - the handling is done in TopologyViewMessageHandler#RequestDetails.process()
      *
+     * When multiple items are selected fabricate the ShowDetails here, and
+     * present buttons that allow custom actions
+     *
      * The WSS will call back asynchronously (see fn in ngOnInit())
      *
      * @param changes Simple Changes set of updates
      */
     ngOnChanges(changes: SimpleChanges): void {
-        if (changes['selectedNode']) {
-            this.selectedNode = changes['selectedNode'].currentValue;
+        if (changes['selectedNodes']) {
+            this.selectedNodes = changes['selectedNodes'].currentValue;
             let type: any;
-
-            if (this.selectedNode === undefined) {
+            if (this.selectedNodes.length === 0) {
                 // Selection has been cleared
                 this.showDetails = <ShowDetails>{};
                 return;
+            } else if (this.selectedNodes.length > 1) {
+                // Don't send message to WSS just form dialog here
+                const propOrder: string[] = [];
+                const propValues: Object = {};
+                const propLabels: Object = {};
+                let numHosts: number = 0;
+                for (let i = 0; i < this.selectedNodes.length; i++) {
+                    propOrder.push(i.toString());
+                    propLabels[i.toString()] = i.toString();
+                    propValues[i.toString()] = this.selectedNodes[i].id;
+                    if (this.selectedNodes[i].hasOwnProperty('nodeType') &&
+                        (<Host>this.selectedNodes[i]).nodeType === NodeType.HOST) {
+                        numHosts++;
+                    } else {
+                        numHosts = -128; // Negate the whole thing so other buttons will not be shown
+                    }
+                }
+                const buttons: string[] = [];
+                if (numHosts === 2) {
+                    buttons.push('createHostToHostFlow');
+                } else if (numHosts > 2) {
+                    buttons.push('createMultiSourceFlow');
+                }
+                buttons.push('relatedIntents');
+
+                this.showDetails = <ShowDetails>{
+                    buttons: buttons,
+                    glyphId: undefined,
+                    id: 'multiple',
+                    navPath: undefined,
+                    propLabels: propLabels,
+                    propOrder: propOrder,
+                    propValues: propValues,
+                    title: this.lionFnTopo('title_selected_items')
+                };
+                this.log.debug('Details panel generated from multiple devices', this.showDetails);
+                return;
             }
 
-            if (this.selectedNode.hasOwnProperty('nodeType')) { // For Device, Host, SubRegion
-                type = (<Host>this.selectedNode).nodeType;
+            // If only one thing has been selected then request details of that from the server
+            const selectedNode = this.selectedNodes[0];
+            if (selectedNode.hasOwnProperty('nodeType')) { // For Device, Host, SubRegion
+                type = (<Host>selectedNode).nodeType;
                 this.wss.sendEvent('requestDetails', {
-                    id: this.selectedNode.id,
+                    id: selectedNode.id,
                     class: type,
                 });
-            } else if (this.selectedNode.hasOwnProperty('type')) { // Must be link
-                const link: Link = <Link>this.selectedNode;
+            } else if (selectedNode.hasOwnProperty('type')) { // Must be link
+                const link: Link = <Link>selectedNode;
                 if (<LinkType><unknown>LinkType[link.type] === LinkType.UiEdgeLink) { // Number based enum
                     this.wss.sendEvent('requestDetails', {
                         key: link.id,
@@ -207,7 +262,7 @@
                     });
                 }
             } else {
-                this.log.warn('Unexpected type for selected element', this.selectedNode);
+                this.log.warn('Unexpected type for selected element', selectedNode);
             }
         }
     }
@@ -231,6 +286,12 @@
                 return SHOWMETERVIEW;
             case 'showPipeConfView':
                 return SHOWPIPECONFVIEW;
+            case 'relatedIntents':
+                return RELATEDINTENTS;
+            case 'createHostToHostFlow':
+                return CREATEHOSTTOHOSTFLOW;
+            case 'createMultiSourceFlow':
+                return CREATEMULTISOURCEFLOW;
             default:
                 return <ButtonAttrs>{
                     gid: btnName,
@@ -244,19 +305,46 @@
      * e.g. if params are 'meter', 'device' and 'null:0000000000001' then the
      * navigation URL will become "http://localhost:4200/#/meter?devId=null:0000000000000002"
      *
+     * When multiple hosts are selected other actions have to be accommodated
+     *
      * @param path The path to navigate to
      * @param navPath The parameter name to use
      * @param selId the parameter value to use
      */
-    navto(path: string, navPath: string, selId: string): void {
-        this.log.debug('navigate to', path, 'for', navPath, '=', selId);
-        // Special case until it's fixed
-        if (selId) {
+    navto(path: string): void {
+        this.log.debug('navigate to', path, 'for',
+            this.showDetails.navPath, '=', this.showDetails.id);
+
+        const ids: string[] = [];
+        Object.values(this.showDetails.propValues).forEach((v) => ids.push(v));
+        if (path === 'relatedIntents' && this.showDetails.id === 'multiple') {
+            this.wss.sendEvent('requestRelatedIntents', {
+                'ids': ids,
+                'hover': ''
+            });
+
+        } else if (path === 'create_h2h_flow' && this.showDetails.id === 'multiple') {
+            this.wss.sendEvent('addHostIntent', {
+                'one': ids[0],
+                'two': ids[1],
+                'ids': ids
+            });
+
+        } else if (path === 'create_msrc_flow' && this.showDetails.id === 'multiple') {
+            // Should only happen when there are 3 or more ids
+            this.wss.sendEvent('addMultiSourceIntent', {
+                'src': ids.slice(0, ids.length - 1),
+                'dst': ids[ids.length - 1],
+                'ids': ids
+            });
+
+        } else if (this.showDetails.id) {
+            let navPath = this.showDetails.navPath;
             if (navPath === 'device') {
                 navPath = 'devId';
             }
             const queryPar: Params = {};
-            queryPar[navPath] = selId;
+            queryPar[navPath] = this.showDetails.id;
             this.router.navigate([path], { queryParams: queryPar });
         }
     }
@@ -265,8 +353,8 @@
      * Read the LION bundle for Details panel and set up the lionFn
      */
     doLion() {
-        this.lionFn = this.lion.bundle('core.view.Flow');
-
+        this.lionFnTopo = this.lion.bundle('core.view.Topo');
+        this.lionFnFlow = this.lion.bundle('core.view.Flow');
     }
 
 }