[CORD-2030] Subscriber dashboard

Change-Id: Ie12cc13537975fd5e5ca8628d4ad14ce4810a925
diff --git a/xos/gui/.gitignore b/xos/gui/.gitignore
index 789431e..9006b95 100644
--- a/xos/gui/.gitignore
+++ b/xos/gui/.gitignore
@@ -2,3 +2,4 @@
 typings/
 .idea/
 .tmp/
+dist/
diff --git a/xos/gui/conf/browsersync.conf.js b/xos/gui/conf/browsersync.conf.js
index b5bf434..9402505 100755
--- a/xos/gui/conf/browsersync.conf.js
+++ b/xos/gui/conf/browsersync.conf.js
@@ -1,13 +1,10 @@
 
 /*
  * Copyright 2017-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.
@@ -27,11 +24,8 @@
         conf.paths.src
       ],
       middleware: function (req, res, next) {
-        if (req.url.indexOf('xosapi') !== -1) {
-          proxy.api.web(req, res);
-        }
-        else if (req.url.indexOf('spa') !== -1 || req.url.indexOf('socket') !== -1) {
-          proxy.static.web(req, res);
+        if (req.url.indexOf('xosapi') !== -1 || req.url.indexOf('xos') !== -1 || req.url.indexOf('socket') !== -1) {
+          proxy.web(req, res);
         }
         else {
           next();
@@ -40,4 +34,4 @@
     },
     open: false
   };
-};
+};
\ No newline at end of file
diff --git a/xos/gui/conf/proxy.js b/xos/gui/conf/proxy.js
index b6e5e83..41908c0 100644
--- a/xos/gui/conf/proxy.js
+++ b/xos/gui/conf/proxy.js
@@ -1,13 +1,9 @@
-
 /*
  * Copyright 2017-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.
@@ -15,34 +11,21 @@
  * limitations under the License.
  */
 
-
 const httpProxy = require('http-proxy');
 
 const target = process.env.PROXY || '192.168.46.100';
 
-const apiProxy = httpProxy.createProxyServer({
-  target: `http://${target}:9101`
+console.log(`proxy setup to: ${target}`);
+
+const proxy = httpProxy.createProxyServer({
+  target: `http://${target}`
 });
 
-const staticFilesProxy = httpProxy.createProxyServer({
-  target: `http://${target}/spa`
-});
-
-apiProxy.on('error', (error, req, res) => {
+proxy.on('error', (error, req, res) => {
   res.writeHead(500, {
     'Content-Type': 'text/plain'
   });
   console.error('[Proxy]', error);
 });
 
-staticFilesProxy.on('error', (error, req, res) => {
-  res.writeHead(500, {
-    'Content-Type': 'text/plain'
-  });
-  console.error('[Proxy]', error);
-});
-
-module.exports = {
-  api: apiProxy,
-  static: staticFilesProxy
-};
+module.exports = proxy;
diff --git a/xos/gui/conf/webpack-dist.conf.js b/xos/gui/conf/webpack-dist.conf.js
index 36fbd97..5c66822 100755
--- a/xos/gui/conf/webpack-dist.conf.js
+++ b/xos/gui/conf/webpack-dist.conf.js
@@ -82,7 +82,7 @@
       compress: {unused: true, dead_code: true, warnings: false}, // eslint-disable-line camelcase
       mangle: false // NOTE mangling was breaking the build
     }),
-    new ExtractTextPlugin('index-[contenthash].css'),
+    new ExtractTextPlugin('index.css'),
     new webpack.optimize.CommonsChunkPlugin({name: 'vendor'}),
     new webpack.ProvidePlugin({
       $: "jquery",
diff --git a/xos/gui/package.json b/xos/gui/package.json
index f9d893b..6859bed 100644
--- a/xos/gui/package.json
+++ b/xos/gui/package.json
@@ -8,6 +8,7 @@
     "angular-toastr": "^2.1.1",
     "angular-ui-bootstrap": "^2.3.1",
     "angular-ui-router": "1.0.0-beta.1",
+    "angularjs-slider": "^6.4.0",
     "bootstrap": "^3.3.7",
     "http-proxy": "^1.16.2",
     "jquery": "^3.1.1",
diff --git a/xos/gui/src/app/services/graph.extension.ts b/xos/gui/src/app/services/graph.extension.ts
index 56310f2..0d90963 100644
--- a/xos/gui/src/app/services/graph.extension.ts
+++ b/xos/gui/src/app/services/graph.extension.ts
@@ -36,11 +36,6 @@
   }
 
   public setup() {
-    this.XosServiceGraphExtender.register('finegrained', 'ecord-local', (graph): any => {
-      graph = this.positionFineGrainedNodes(graph);
-      return graph;
-    });
-
     this.XosServiceGraphExtender.register('coarse', 'ecord-local', (graph: any): any => {
       graph.nodes = this.positionCoarseNodes(graph.nodes);
       return {
@@ -60,7 +55,7 @@
   private positionCoarseNodes(nodes: any[]): any[] {
     // getting distance between nodes
     const hStep = this.getSvgDimensions('xos-coarse-tenancy-graph').width / 4;
-    const vStep = this.getSvgDimensions('xos-coarse-tenancy-graph').height / 4;
+    const vStep = this.getSvgDimensions('xos-coarse-tenancy-graph').height / 5;
 
     const vtr = _.find(nodes, {label: 'vtr'});
     if (vtr) {
@@ -69,6 +64,13 @@
       vtr.fixed = true;
     }
 
+    const rcord = _.find(nodes, {label: 'rcord'});
+    if (rcord) {
+      rcord.x = hStep * 0.5;
+      rcord.y = vStep * 2;
+      rcord.fixed = true;
+    }
+
     const volt = _.find(nodes, {label: 'volt'});
     if (volt) {
       volt.x = hStep * 1;
@@ -90,6 +92,13 @@
       vrouter.fixed = true;
     }
 
+    const addressmanager = _.find(nodes, {label: 'addressmanager'});
+    if (addressmanager) {
+      addressmanager.x = hStep * 2.5;
+      addressmanager.y = vStep * 1.5;
+      addressmanager.fixed = true;
+    }
+
     const oc = _.find(nodes, {label: 'ONOS_CORD'});
     if (oc) {
       oc.x = hStep + (hStep / 2);
@@ -97,117 +106,34 @@
       oc.fixed = true;
     }
 
+    const vtn = _.find(nodes, {label: 'vtn'});
+    if (vtn) {
+      vtn.x = hStep * 1.5;
+      vtn.y = vStep * 4;
+      vtn.fixed = true;
+    }
+
     const of = _.find(nodes, {label: 'ONOS_Fabric'});
     if (of) {
-      of.x = (hStep * 2) + (hStep / 2);
+      of.x = hStep * 2.5;
       of.y = vStep * 3;
       of.fixed = true;
     }
 
+    const fabric = _.find(nodes, {label: 'fabric'});
+    if (fabric) {
+      fabric.x = hStep * 2.5;
+      fabric.y = vStep * 4;
+      fabric.fixed = true;
+    }
+
+    const exampleservice = _.find(nodes, {label: 'exampleservice'});
+    if (exampleservice) {
+      exampleservice.x = hStep * 3.5;
+      exampleservice.y = vStep * 4.5;
+      exampleservice.fixed = true;
+    }
+
     return nodes;
   }
-
-  private positionFineGrainedNodes(graph: any): any[] {
-
-    let subscriberPosition = 0;
-    let networkPosition = 0;
-
-    const positionSubscriberNode = (node: any, hStep: number, vStep: number): any => {
-      subscriberPosition = subscriberPosition + 1;
-      node.x = hStep;
-      node.y = vStep * (3 + subscriberPosition );
-      node.fixed = true;
-      return node;
-    };
-
-    const positionServiceNode = (node: any, hStep: number, vStep: number, vLength: number): any => {
-      if (node.label === 'ONOS_Fabric') {
-        node.x = hStep * 4;
-        node.y = vStep;
-      }
-      if (node.label === 'volt' || node.label === 'vsg' || node.label === 'vrouter') {
-        node.y = vStep * 3;
-      }
-      if (node.label === 'volt') {
-        node.x = hStep * 2;
-      }
-      if (node.label === 'vsg') {
-        node.x = hStep * 3;
-      }
-      if (node.label === 'vrouter') {
-        node.x = hStep * 4;
-      }
-      if (node.label === 'ONOS_CORD' || node.label === 'vtr') {
-        node.y = vStep * (vLength -1);
-      }
-      if (node.label === 'ONOS_CORD') {
-        node.x = hStep * 2;
-      }
-      if (node.label === 'vtr') {
-        node.x = hStep * 3;
-      }
-
-      node.fixed = true;
-      return node;
-    };
-
-    const positionNetworkNode = (node: any, hStep: number, vStep: number): any => {
-      networkPosition = networkPosition + 1;
-      node.x = hStep * 5;
-      node.y = vStep * (3 + networkPosition );
-      node.fixed = true;
-      return node;
-    };
-
-    const findSubscriberElementY = (nodes: any[], node: any): any => {
-      if (node.model.subscriber_root_id) {
-        console.log(node.model.subscriber_root_id);
-        const subscriber = _.find(nodes, n => {
-          return n.id === `tenantroot~${node.model.subscriber_root_id}`
-        });
-        debugger;
-        // console.log(subscriber.y);
-        return subscriber.y;
-      }
-    };
-
-    const positionTenantNode = (nodes: any[], node: any, hStep: number, vStep: number): any => {
-      if (node.model.kind === 'vOLT') {
-        node.x = hStep * 2;
-      }
-      if (node.model.kind === 'vCPE') {
-        node.x = hStep * 3;
-      }
-      if (node.model.kind === 'vROUTER') {
-        node.x = hStep * 4;
-      }
-
-      return node;
-    };
-
-    let subscribers = _.filter(graph.nodes, n => n.type === 'subscriber' || n.type === 'tenantroot');
-
-    const vLength = 5 + subscribers.length;
-
-    const hStep = this.getSvgDimensions('xos-fine-grained-tenancy-graph').width / 6;
-    const vStep = this.getSvgDimensions('xos-fine-grained-tenancy-graph').height / vLength;
-
-    graph.nodes = _.map(graph.nodes, n => {
-      if (n.type === 'subscriber' || n.type === 'tenantroot') {
-        n = positionSubscriberNode(n, hStep, vStep);
-      }
-      if (n.type === 'service') {
-        n = positionServiceNode(n, hStep, vStep, vLength);
-      }
-      if (n.type === 'network') {
-        n = positionNetworkNode(n, hStep, vStep);
-      }
-      if (n.type === 'tenant') {
-        n = positionTenantNode(graph.nodes, n, hStep, vStep);
-      }
-      // n.fixed = true;
-      return n;
-    });
-    return graph;
-  }
 }
diff --git a/xos/gui/src/app/subscriber-dashboard/subscriber-dashboard.component.ts b/xos/gui/src/app/subscriber-dashboard/subscriber-dashboard.component.ts
new file mode 100644
index 0000000..fb98e4c
--- /dev/null
+++ b/xos/gui/src/app/subscriber-dashboard/subscriber-dashboard.component.ts
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017-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 * as _ from 'lodash';
+import './subscriber-dashboard.scss';
+
+class rcordSubscriberDashboardCtrl {
+
+  static $inject = [
+    'toastr',
+    'XosModelStore',
+    'XosModeldefsCache'
+  ];
+
+  public levelOptions = [];
+  public subscribers = [];
+  public statusFieldOptions = [];
+  public slider = {
+    floor: 0,
+    ceil: 1000000000,
+    translate: (value) => {
+      return Math.floor(value / 1000000);
+    }
+  };
+
+  private subscriberDef;
+
+  constructor (
+    private toastr: any,
+    private XosModelStore: any,
+    private XosModeldefsCache: any
+  ) {
+
+  }
+
+  $onInit() {
+    this.XosModelStore.query('CordSubscriberRoot', '/rcord/cordsubscriberroots')
+      .subscribe(
+        res => {
+          this.subscribers = this.parseSubscribers(res);
+        }
+      );
+
+    this.levelOptions = [
+      `G`,
+      `PG`,
+      `PG_13`,
+      `R`,
+      `X`
+    ];
+
+    this.subscriberDef = this.XosModeldefsCache.get('CordSubscriberRoot');
+    this.statusFieldOptions = _.find(this.subscriberDef.fields, {name: 'status'}).options;
+  }
+
+  public addDevice(subscriber) {
+    subscriber.service_specific_attribute.devices.push({
+      name: '',
+      mac: '',
+      level: 'PG_13'
+    })
+  }
+
+  public removeDevice(subscriber, device) {
+    _.remove(subscriber.service_specific_attribute.devices, {name:device.name})
+  }
+
+  public save(subscriber) {
+    console.log(subscriber);
+    const item: any = angular.copy(subscriber);
+    item.service_specific_attribute = JSON.stringify(item.service_specific_attribute);
+    item.$save()
+      .then(() => {
+        this.toastr.success(`Subscriber successfully saved`);
+      });
+  }
+
+  private parseSubscribers(subscribers) {
+    return _.map(subscribers, (s) => {
+      if (angular.isString(s.service_specific_attribute)) {
+        s.service_specific_attribute = JSON.parse(s.service_specific_attribute);
+      }
+      return s;
+    })
+  }
+}
+
+export const rcordSubscriberDashboard: angular.IComponentOptions = {
+  template: require('./subscriber-dashboard.html'),
+  controllerAs: 'vm',
+  controller: rcordSubscriberDashboardCtrl
+};
\ No newline at end of file
diff --git a/xos/gui/src/app/subscriber-dashboard/subscriber-dashboard.html b/xos/gui/src/app/subscriber-dashboard/subscriber-dashboard.html
new file mode 100644
index 0000000..0b49316
--- /dev/null
+++ b/xos/gui/src/app/subscriber-dashboard/subscriber-dashboard.html
@@ -0,0 +1,106 @@
+<!--
+Copyright 2017-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.
+-->
+
+<div class="row" ng-if="!vm.selectedSubscriber.status">
+    <div class="col-xs-12">
+        <h2>Select Subscriber:</h2>
+        <input
+          type="text"
+          ng-model="vm.selectedSubscriber"
+          uib-typeahead="s as s.name for s in vm.subscribers | filter:$viewValue | limitTo:8"
+          class="form-control">
+    </div>
+</div>
+
+<div class="row" ng-if="vm.selectedSubscriber.status">
+    <div class="col-xs-12">
+        <form>
+            <div class="form-group">
+                <h2>Status:</h2>
+            </div>
+            <div class="form-group">
+                <div class="row">
+                    <div class="col-xs-3" ng-repeat="s in vm.statusFieldOptions">
+                        <a
+                          class="btn btn-default btn-block"
+                          ng-click="vm.selectedSubscriber.status = s.id"
+                          ng-class="{'active': s.id === vm.selectedSubscriber.status}">
+                            {{s.label}}
+                        </a>
+                    </div>
+                </div>
+            </div>
+            <div class="form-group">
+                <h2>Bandwith:</h2>
+            </div>
+            <div class="form-group">
+                <div class="row">
+                    <div class="col-xs-6">
+                        <label>Uplink speed:</label>
+                        <rzslider
+                          rz-slider-model="vm.selectedSubscriber.uplink_speed"
+                          rz-slider-options="vm.slider"></rzslider>
+                    </div>
+                    <div class="col-xs-6">
+                        <label>Downlink speed:</label>
+                        <rzslider
+                          rz-slider-model="vm.selectedSubscriber.downlink_speed"
+                          rz-slider-options="vm.slider"></rzslider>
+                    </div>
+                </div>
+            </div>
+            <div class="form-group">
+                <h2>
+                    Devices:
+                    <a ng-click="vm.addDevice(vm.selectedSubscriber)">
+                        <small class="label label-default">
+                            <i class="fa fa-plus"></i>
+                        </small>
+                    </a>
+                </h2>
+            </div>
+            <div class="row">
+                <div class="col-md-3 col-sm-6" ng-repeat="d in vm.selectedSubscriber.service_specific_attribute.devices">
+                    <div class="panel panel-filled">
+                        <div class="panel-body">
+                            <div class="form-group">
+                                <a ng-click="vm.removeDevice(vm.selectedSubscriber, d)">
+                                    <i class="fa fa-remove pull-right"></i>
+                                </a>
+                            </div>
+                            <div class="form-group">
+                                <label>Name:</label>
+                                <input class="form-control" type="text" ng-model="d.name"/>
+                            </div>
+                            <div class="form-group">
+                                <label>MAC Address:</label>
+                                <input class="form-control" type="text" ng-model="d.mac"/>
+                            </div>
+                            <div class="form-group">
+                                <label>Filter level:</label>
+                                <select class="form-control" ng-model="d.level" ng-options="l for l in vm.levelOptions"></select>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="form-group">
+                <button class="btn btn-success" ng-click="vm.save(vm.selectedSubscriber)">Save</button>
+                <button class="btn btn-info" ng-click="vm.selectedSubscriber = null">Reset</button>
+            </div>
+        </form>
+    </div>
+</div>
\ No newline at end of file
diff --git a/xos/gui/src/app/subscriber-dashboard/subscriber-dashboard.scss b/xos/gui/src/app/subscriber-dashboard/subscriber-dashboard.scss
new file mode 100644
index 0000000..e65add3
--- /dev/null
+++ b/xos/gui/src/app/subscriber-dashboard/subscriber-dashboard.scss
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017-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.
+ */
+
+.rzslider .rz-pointer {
+  background-color: #f6a821;
+  width: 20px;
+  height: 20px;
+  top: -8px;
+
+  &:after {
+    top: 6px;
+    left: 6px;
+  }
+}
\ No newline at end of file
diff --git a/xos/gui/src/index.html b/xos/gui/src/index.html
index 57bd4f5..fb32573 100644
--- a/xos/gui/src/index.html
+++ b/xos/gui/src/index.html
@@ -21,15 +21,16 @@
 <head>
   <meta charset="UTF-8">
   <title>Document</title>
-  <link href="/spa/loader.css" rel="stylesheet">
-  <link href="/spa/app.css" rel="stylesheet">
+  <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
+  <link href="/xos/loader.css" rel="stylesheet">
+  <link href="/xos/app.css" rel="stylesheet">
 </head>
 <body>
   <div ui-view></div>
-  <script src="/spa/vendor.js"></script>
-  <script src="/spa/app.js"></script>
-  <script src="/spa/loader.js"></script>
-  <script src="/spa/app.config.js"></script>
-  <script src="/spa/style.config.js"></script>
+  <script src="/xos/vendor.js"></script>
+  <script src="/xos/app.js"></script>
+  <script src="/xos/loader.js"></script>
+  <script src="/xos/app.config.js"></script>
+  <script src="/xos/style.config.js"></script>
 </body>
 </html>
\ No newline at end of file
diff --git a/xos/gui/src/index.ts b/xos/gui/src/index.ts
index 85d3a34..511a69a 100644
--- a/xos/gui/src/index.ts
+++ b/xos/gui/src/index.ts
@@ -22,14 +22,41 @@
 import 'angular-ui-router';
 import 'angular-resource';
 import 'angular-cookies';
+import 'angularjs-slider';
+import '../node_modules/angularjs-slider/dist/rzslider.scss';
+
 import {RCordGraphReducer, IRCordGraphReducer} from './app/services/graph.extension';
+import {rcordSubscriberDashboard} from './app/subscriber-dashboard/subscriber-dashboard.component';
 
 angular.module('xos-rcord-gui-extension', [
     'ui.router',
-    'app'
+    'app',
+    'rzModule'
   ])
   .service('RCordGraphReducer', RCordGraphReducer)
-  .run(function($log: ng.ILogService, RCordGraphReducer: IRCordGraphReducer) {
+  .component('rcordSubscriberDashboard', rcordSubscriberDashboard)
+  .run(function($log: ng.ILogService, RCordGraphReducer: IRCordGraphReducer, XosNavigationService: any, XosRuntimeStates: any, $state: ng.ui.IStateService) {
     $log.info('[xos-rcord-graph-gui-extension] App is running');
     RCordGraphReducer.setup();
+
+    XosRuntimeStates.addState(`xos.rcord`, {
+      url: 'rcord',
+      parent: 'xos',
+      abstract: true,
+      template: '<div ui-view></div>'
+    });
+
+    XosRuntimeStates.addState(`xos.rcord.dashboard`, {
+      url: '/dashboard',
+      parent: 'xos.rcord',
+      component: 'rcordSubscriberDashboard'
+    });
+
+    window.setTimeout(() => {
+      XosNavigationService.add({
+        label: 'Dashboard',
+        state: 'xos.rcord.dashboard',
+        parent: 'xos.rcord'
+      });
+    }, 5000);
   });