GUI2-Cluster View

Change-Id: I812439fae68d18756c707c1021ce6e070ae6afc3
diff --git a/web/gui2/src/main/webapp/app/view/cluster/cluster-details.directive.ts b/web/gui2/src/main/webapp/app/view/cluster/cluster-details.directive.ts
new file mode 100644
index 0000000..9aa2eb7
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/view/cluster/cluster-details.directive.ts
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2015-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 {Directive, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output} from '@angular/core';
+import {FnService} from '../../fw/util/fn.service';
+import {LogService} from '../../log.service';
+import {MastService} from '../../fw/mast/mast.service';
+import {DetailsPanelBaseImpl} from '../../fw/widget/detailspanel.base';
+import {LoadingService} from '../../fw/layer/loading.service';
+import {IconService} from '../../fw/svg/icon.service';
+import {LionService} from '../../fw/util/lion.service';
+import {WebSocketService} from '../../fw/remote/websocket.service';
+import * as d3 from 'd3';
+import {PanelService} from '../../fw/layer/panel.service';
+import {HostListener} from '@angular/core';
+
+// internal state
+let detailsPanel,
+    pStartY,
+    pHeight,
+    top,
+    topTable,
+    bottom,
+    iconDiv,
+    wSize;
+
+
+// constants
+const topPdg = 28,
+    ctnrPdg = 24,
+    scrollSize = 17,
+    portsTblPdg = 100,
+    pName = 'details-panel',
+    propOrder = [
+        'id', 'ip',
+    ],
+    deviceCols = [
+        'id', 'type', 'chassisid', 'mfr',
+        'hw', 'sw', 'protocol', 'serial',
+    ];
+
+function addProp(tbody, label, value) {
+    const tr = tbody.append('tr');
+
+    function addCell(cls, txt) {
+        tr.append('td').attr('class', cls).text(txt);
+    }
+
+    addCell('label', label + ' :');
+    addCell('value', value);
+}
+
+function addDeviceRow(tbody, device) {
+    const tr = tbody.append('tr');
+
+    deviceCols.forEach(function (col) {
+        tr.append('td').text(device[col]);
+    });
+}
+
+@Directive({
+    selector: '[onosClusterDetails]',
+})
+
+export class ClusterDetailsDirective extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
+    @Input() id: string;
+    @Output() closeEvent = new EventEmitter<string>();
+
+    lionFn; // Function
+
+    constructor(protected fs: FnService,
+                protected ls: LoadingService,
+                protected is: IconService,
+                protected lion: LionService,
+                protected wss: WebSocketService,
+                protected log: LogService,
+                protected mast: MastService,
+                protected ps: PanelService,
+                protected el: ElementRef,
+                @Inject('Window') private w: Window) {
+        super(fs, ls, log, wss, 'cluster');
+
+        if (this.lion.ubercache.length === 0) {
+            this.lionFn = this.dummyLion;
+            this.lion.loadCbs.set('clusterdetails', () => this.doLion());
+        } else {
+            this.doLion();
+        }
+        this.log.debug('ClusterDetailsDirective constructed');
+    }
+
+    ngOnInit() {
+        this.init();
+        this.initPanel();
+        this.log.debug('Cluster Details Component initialized');
+    }
+
+    /**
+     * Stop listening to clusterDetailsResponse on WebSocket
+     */
+    ngOnDestroy() {
+        this.lion.loadCbs.delete('clusterdetails');
+        this.destroy();
+        this.ps.destroyPanel(pName);
+        this.log.debug('Cluster Details Component destroyed');
+    }
+
+    @HostListener('window:resize', ['event'])
+    onResize(event: any) {
+        this.heightCalc();
+        this.populateDetails(this.detailsData);
+        return {
+            h: this.w.innerHeight,
+            w: this.w.innerWidth
+        };
+    }
+
+    @HostListener('document:click', ['$event'])
+    onClick(event) {
+        if (event.path !== undefined) {
+            for (let i = 0; i < event.path.length; i++) {
+                if (event.path[i].className === 'close-btn') {
+                    this.close();
+                    break;
+                }
+            }
+        } else if (event.target.href === undefined) {
+            if (event.target.parentNode.className === 'close-btn') {
+                this.close();
+            }
+        } else if (event.target.href.baseVal === '#xClose') {
+            this.close();
+        }
+    }
+
+    /**
+     * Details Panel Data Request on row selection changes
+     * Should be called whenever id changes
+     * If id is empty, no request is made
+     */
+    ngOnChanges() {
+        if (this.id === '') {
+            if (detailsPanel) {
+                detailsPanel.hide();
+            }
+            return '';
+        } else {
+            const query = {
+                'id': this.id
+            };
+            this.requestDetailsPanelData(query);
+            this.heightCalc();
+
+            /*
+             * Details data takes around 2ms to come up on web-socket
+             * putting a timeout interval of 5ms
+             */
+            setTimeout(() => {
+                this.populateDetails(this.detailsData);
+                detailsPanel.show();
+            }, 500);
+
+
+        }
+    }
+
+    doLion() {
+        this.lionFn = this.lion.bundle('core.view.Cluster');
+    }
+
+    heightCalc() {
+        pStartY = this.fs.noPxStyle(d3.select('.tabular-header'), 'height')
+            + this.mast.mastHeight + topPdg;
+        wSize = this.fs.windowSize(this.fs.noPxStyle(d3.select('.tabular-header'), 'height'));
+        pHeight = wSize.height;
+    }
+
+    createDetailsPane() {
+        detailsPanel = this.ps.createPanel(pName, {
+            width: wSize.width,
+            margin: 0,
+            hideMargin: 0,
+        });
+        detailsPanel.el().style('top', pStartY + 'px');
+        detailsPanel.el().style('position', 'absolute');
+        this.hidePanel = function () {
+            detailsPanel.hide();
+        };
+        detailsPanel.hide();
+    }
+
+    initPanel() {
+        this.heightCalc();
+        this.createDetailsPane();
+    }
+
+    populateDetails(details) {
+        this.setUpPanel();
+        this.populateTop(details);
+        this.populateBottom(details.devices);
+        detailsPanel.height(pHeight);
+    }
+
+    setUpPanel() {
+        let container, closeBtn;
+        detailsPanel.empty();
+
+        container = detailsPanel.append('div').classed('container', true);
+
+        top = container.append('div').classed('top', true);
+        closeBtn = top.append('div').classed('close-btn', true);
+        this.addCloseBtn(closeBtn);
+        iconDiv = top.append('div').classed('dev-icon', true);
+        top.append('h2');
+        topTable = top.append('div').classed('top-content', true)
+            .append('table');
+        top.append('hr');
+
+        bottom = container.append('div').classed('bottom', true);
+        bottom.append('h2').classed('devices-title', true).text('Devices');
+        bottom.append('table');
+    }
+
+    addCloseBtn(div) {
+        this.is.loadEmbeddedIcon(div, 'close', 20);
+        div.on('click', this.closePanel);
+    }
+
+    closePanel(): boolean {
+        if (detailsPanel.isVisible()) {
+            detailsPanel.hide();
+            return true;
+        }
+        return false;
+    }
+
+    populateTop(details) {
+        const propLabels = this.getLionProps();
+
+        this.is.loadEmbeddedIcon(iconDiv, 'node', 40);
+        top.select('h2').text(details.id);
+
+        const tbody = topTable.append('tbody');
+
+        propOrder.forEach(function (prop, i) {
+            addProp(tbody, propLabels[i], details[prop]);
+        });
+    }
+
+    getLionDeviceCols() {
+        return [
+            this.lionFn('uri'),
+            this.lionFn('type'),
+            this.lionFn('chassis_id'),
+            this.lionFn('vendor'),
+            this.lionFn('hw_version'),
+            this.lionFn('sw_version'),
+            this.lionFn('protocol'),
+            this.lionFn('serial_number'),
+        ];
+    }
+
+    populateBottom(devices) {
+        const table = bottom.select('table'),
+            theader = table.append('thead').append('tr'),
+            tbody = table.append('tbody');
+
+        let tbWidth, tbHeight;
+
+        this.getLionDeviceCols().forEach(function (col) {
+            theader.append('th').text(col);
+        });
+        if (devices !== undefined) {
+            devices.forEach(function (device) {
+                addDeviceRow(tbody, device);
+            });
+        }
+
+        tbWidth = this.fs.noPxStyle(tbody, 'width') + scrollSize;
+        tbHeight = pHeight
+            - (this.fs.noPxStyle(detailsPanel.el()
+                    .select('.top'), 'height')
+                + this.fs.noPxStyle(detailsPanel.el()
+                    .select('.devices-title'), 'height')
+                + portsTblPdg);
+
+        table.style('zIndex', '0');
+        table.style('height', tbHeight + 'px');
+        table.style('width', tbWidth + 'px');
+        table.style('overflow', 'auto');
+        table.style('display', 'block');
+
+        detailsPanel.width(tbWidth + ctnrPdg);
+    }
+
+    getLionProps() {
+        return [
+            this.lionFn('node_id'),
+            this.lionFn('ip_address'),
+        ];
+    }
+}