aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--demo-ui/README.md149
-rw-r--r--demo-ui/app/config.json32
-rw-r--r--demo-ui/app/controllers/CosController.js381
-rw-r--r--demo-ui/app/controllers/EplController.js528
-rw-r--r--demo-ui/app/controllers/MainController.js31
-rw-r--r--demo-ui/app/controllers/MefController.js117
-rw-r--r--demo-ui/app/services/cosServices.js64
-rw-r--r--demo-ui/app/services/dbg.js54
-rw-r--r--demo-ui/app/services/eplServices.js64
-rw-r--r--demo-ui/app/services/mefServices.js71
-rw-r--r--demo-ui/app/services/model.js61
-rw-r--r--demo-ui/app/views/cos-panel.html171
-rw-r--r--demo-ui/app/views/css/vcpe.css340
-rw-r--r--demo-ui/app/views/epl-panel.html187
-rw-r--r--demo-ui/app/views/less/vcpe.less365
-rw-r--r--demo-ui/app/views/mef-panel.html370
-rw-r--r--demo-ui/app/views/vcpe-portal.html71
-rw-r--r--demo-ui/bower.json11
-rw-r--r--demo-ui/gruntfile.js25
-rw-r--r--demo-ui/package.json18
-rw-r--r--demo-ui/vcpeUiServer.js19
21 files changed, 3129 insertions, 0 deletions
diff --git a/demo-ui/README.md b/demo-ui/README.md
new file mode 100644
index 0000000..5382eef
--- /dev/null
+++ b/demo-ui/README.md
@@ -0,0 +1,149 @@
+===================================
+Virtual Business CPE Demo UI Portal
+===================================
+* This portal communicates with the vcpe service manager defined in vcpe-services repo (cosmgr, evcmgr, svcmgr)
+* It enbles the creation of Service Levels (Cos) and Eth Private Lines (EPL)
+* As part of the EPL creation, EVCs and UNIs are also created
+* Currently the code is housed in private VCPE gerrit repo:
+ http://euca-10-5-5-106.cirrus.cloud.cablelabs.com:8080/#/admin/projects/vcpe-ui
+* All development and testing have been on OSX
+* The portal currently must be run within Chrome
+
+Environment Requirements:
+=========================
+* node.js / npm
+* bower
+* grunt
+* less
+
+If you need help with environement set-up instrucitons, see "Environment Set Up" notes at the bottom of this document
+
+Building the runtime environement:
+==================================
+* Once your environemnt set up (per Environment Set Up instructions at the bottom of this document)
+
+ $ git clone http://yourusername@euca-10-5-5-106.cirrus.cloud.cablelabs.com:8080/vcpe-ui
+ $ cd vcpe-ui
+ $ npm install
+ - reads package.json to install dependencies for the vcpeUiServer.js node app
+ - dependencies are installed in ./node_modules directory
+ $ bower install
+ - reads bower.json to install dependencies for the webapp (angular,etc)
+ - dependencies are installed in ./bower_components directory
+ $ grunt
+ - reads gruntfile.js and carries out specified "build" steps
+ - in our case, we compile the .less file to .css
+
+Starting the web app
+===================
+* Start the vcpe server managers (so the app has someting to talk to)
+ - see README.md in vcpe-services repo
+* The webapp can be served by any web server. vcpeUiServer.js is a nodejs app that can be used as an easy way to serve the webapp.
+* Start the server (within the vcpe-ui directory)
+
+ $ node vcpeUiServer.js [portnum] &
+
+ NOTES:
+ - portnum is optional, and if not provided the app will be served on port 4000
+ - An issue that you may run into when you start the server is a port conflict if the server is already running from a previous session (Error: listen EADDRINUSE). To solve the issue run 'pkill node', and then start the server.
+
+* Bring up the web app
+ - To render properly, Chrome must be used (safari has an html flexbox issue, to be fixed in the future)
+ - In the chrome address bar enter the following
+
+ http://localhost:4000/app/views/vcpe-portal.html
+
+Using the web app
+===================
+* Config the app via the vcpe-ui/app/config.json file. This allows configuration of:
+ - Communications with the service managers (cosmgr, evcmgr, eplmgr)
+ - UNI's to be used for Eth Private Line creation
+* You will need to start the VCPE services (cosmgr, evcmgr, eplmgr) in order for the web app to run. Please refer to vcpe-services/README.md for instrutcioons on buiding and running these services
+* In order for the web app to work, either the vcpe ODL plugin, or the ODL emulator must be running, to handle ODL rest call sent by the UI.
+ - To run the ODL Uni Manager Emulator, please see the section "Running the ODL Uni Manager Emulator" section the vcpe-services/README.md
+ - To run the ODL plugin please see the vcpe-odl repsoitory
+* Start the app: http://localhost:4000/app/views/vcpe-portal.html
+* Two panels will be visible
+ - Service Levels (CoS in MEF Speak)
+ - Ethernet Private Lines
+* w/in each panel you can CRUD Service Levels (CoS) and Eth Private Lines (EPL)
+ - creation (+), modification (/) and delete (-)
+* Notes
+ - Service levels must be created before creating Eth Private Lines
+ - Eth private lines present drop down boxes with info as configured in config.json and created service levels
+
+Debugging the Web App
+=====================
+* The webapp provides farily extensive logging of events and issues
+* To see the logging and reported errors
+ - within the chrome menu: view/developer/developer tools
+
+To Look Behind the Scenes
+=========================
+* The web portal sends REST messages to CoS MGR, evc MGR, and epl MGR
+* Using postman (or similar tool), you can easily see a list of MEF entities that have been created w/in each of the service layers:
+ - NOTES:
+ header: Accept application/json
+ body: no body required
+ - To get list of created MEF entities (no request body required)
+ cos: GET http://localhost:9090/cosmgr/webapi/cos/list
+ evc: GET http://localhost:9090/evcmgr/webapi/evc/list
+ epl: GET http://localhost:9090/svcmgr/webapi/svc/epl/list
+
+==================
+Environment Set Up
+==================
+
+Install node/npm
+================
+**OSX:**
+* Download and execute the Nodejs Mac OS X Installer (.pkg) from
+ https://nodejs.org/download/
+
+**Windows/Cygwin:**
+TBD
+
+**Linux:**
+TBD
+
+Install Bower
+=============
+**OSX:**
+ * Bower is a web front end dependency mangament tool
+ * Once you have installed nodejs/npm, install bower as follows
+
+ $ sudo npm install -g bower
+
+**Windows/Cygwin:**
+TBD
+
+**Linux:**
+TBD
+
+Install Grunt Client
+====================
+**OSX:**
+ * Grunt is a web front end build utility
+ * Once you have installed nodejs/npm, install grunt client as follows
+
+ $ sudo npm install -g grunt-cli
+
+**Windows/Cygwin:**
+TBD
+
+**Linux:**
+TBD
+
+Install Less
+====================
+**OSX:**
+ * Less is a css enhancment environement
+ * Once you have installed nodejs/npm, install less as follows
+
+ $ sudo npm install -g less
+
+**Windows/Cygwin:**
+TBD
+
+**Linux:**
+TBD
diff --git a/demo-ui/app/config.json b/demo-ui/app/config.json
new file mode 100644
index 0000000..56c74c6
--- /dev/null
+++ b/demo-ui/app/config.json
@@ -0,0 +1,32 @@
+{
+ "cosMgr" : { "ip": "localhost", "port": "9090" },
+ "evcMgr" : { "ip": "localhost", "port": "9090" },
+ "eplMgr" : { "ip": "localhost", "port": "9090" },
+ "evcMgr" : { "ip": "localhost", "port": "9090" },
+ "uniMgr" : { "ip": "localhost", "port": "8181" },
+ "evcPathMgr": { "ip": "localhost", "port": "8181" },
+
+
+ "uniList" : [
+ {
+ "ip" : "10.1.1.11",
+ "mac" : "00:11:11:11:11:11",
+ "address" : "Denver"
+ },
+ {
+ "ip" : "10.1.1.12",
+ "mac" : "00:22:22:22:22:22",
+ "address" : "Paris"
+ },
+ {
+ "ip" : "10.1.1.13",
+ "mac" : "00:33:33:33:33:33",
+ "address" : "Tokyo"
+ },
+ {
+ "ip" : "10.1.1.14",
+ "mac" : "00:44:44:44:44:44",
+ "address" : "Quebec"
+ }
+ ]
+} \ No newline at end of file
diff --git a/demo-ui/app/controllers/CosController.js b/demo-ui/app/controllers/CosController.js
new file mode 100644
index 0000000..523f3c1
--- /dev/null
+++ b/demo-ui/app/controllers/CosController.js
@@ -0,0 +1,381 @@
+(function() {
+
+var CosController = function($scope, $http, $interval, $log, cosServices, model, dbg ) {
+
+ //
+ // Controller Set Up
+ //
+
+ dbg.p("---> in CosController()");
+
+ actionState = {
+ IDLE : 1, ADD : 2, UPDATE : 3, DEL : 4
+ }
+
+ // $scope variables
+ $scope.cosList = null;
+ $scope.selectedCosIdx = -1;
+ $scope.cosToEdit = null;
+ $scope.cosActionButtonText = "unset";
+
+ $scope.availableBWs = [
+ {
+ name : "10M",
+ cir : 10000
+ },
+ {
+ name : "100M",
+ cir : 100000
+ },
+ {
+ name : "1G",
+ cir : 1000000
+ },
+ {
+ name : "10G",
+ cir : 10000000
+ },
+ ];
+
+ dbg.p("bandwidths avaialble:")
+ dbg.pj($scope.availableBWs);
+
+ // congroller variables
+ var cosActionState = actionState.IDLE;
+ var newCos = null;
+ var uniqueId = true;
+ var nameNum = 1;
+
+
+ // cos constructor
+ var Cos = function(){
+ if ( uniqueId )
+ this.id = "new"+ nameNum;
+ else
+ this.id = "new";
+ this.commitedInfoRate = 50;
+ this.availbility = 95.5;
+ this.frameDelay = 2.2;
+ this.jitter = 3.3;
+ this.frameLoss = 4.4 ;
+ }
+
+ //
+ // utility fxns
+ //
+
+ var validCosIdx = function(idx) {
+ return ( idx >= 0 && idx < $scope.cosList.length )
+ }
+
+ var cosIdxReset = function() {
+ return ( $scope.cosList.length > 0 ? 0 : -1);
+ }
+
+ var cosIdxAfter = function( action ) {
+ switch ( action ) {
+ case actionState.DEL:
+ if ( validCosIdx($scope.selectedCosIdx) )
+ return $scope.selectedCosIdx;
+ else if ( validCosIdx($scope.selectedCosIdx-1) )
+ return $scope.selectedCosIdx-1;
+ else
+ return cosIdxReset();
+ break;
+ case actionState.ADD:
+ return ( $scope.cosList.length >= 0 ?
+ $scope.cosList.length-1 : cosIdxReset() );
+ break;
+ case actionState.UPDATE:
+ return ( validCosIdx($scope.selectedCosIdx) ?
+ $scope.selectedCosIdx : cosIdxReset() );
+ break;
+ default:
+ dbg.e("invalid action state");
+ break;
+ }
+ }
+
+ $scope.cosExists = function(cosId) {
+ var nameCheck = function (cosElm) { return cosElm.id == cosId; }
+ return ( $scope.cosList &&
+ $scope.cosList.filter(nameCheck).length > 0 );
+ }
+
+ $scope.bwText = function(cos) {
+ if (cos) {
+ switch ( cos.commitedInfoRate ) {
+ case 10000 : return "10M";
+ case 100000 : return "100M";
+ case 1000000 : return "1G";
+ case 10000000 : return "10G";
+ }
+ return "invalid BW";
+ }
+ else {
+ return "null cos";
+ }
+ }
+
+ // return index of avilable BW based cir (-1 if not found)
+ var availableBwIdx = function (cir) {
+ for ( var i = 0; i < $scope.availableBWs.length; i++ ) {
+ if ($scope.availableBWs[i].cir == cir) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ // return index of cos in current list (-1 if not found)
+ var cosNameToIdx = function ( cosName ) {
+ if ( !cosName || !$scope.cosList )
+ return -1;
+
+ for ( var i = 0; i < $scope.cosList.length; i++ ) {
+ if ($scope.cosList[i].id == cosName ) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ //
+ // HTTP Repsponse Handlers
+ //
+
+ var onRequestError = function(reason){
+ dbg.e("HTTP Request Problem\n" + JSON.stringify(reason));
+ };
+ var onCosListResp = function(data) {
+ dbg.p("in onCosListResp()");
+ $scope.cosList = data;
+
+ if ( $scope.cosList && $scope.cosList.length > 0 )
+ $scope.selectedCosIdx = 0;
+ else
+ $scope.selectedCosIdx = -1;
+
+ dbg.p("selectedCosIdx = " + $scope.selectedCosIdx,1);
+ };
+ var onCosCreateResp = function(data) {
+ dbg.p("in onCosCreateResp()");
+ dbg.p("following cos returned from create cmd", 1);
+ dbg.pj(data);
+ $scope.cosList.push(newCos);
+ $scope.selectedCosIdx = cosIdxAfter(actionState.ADD);
+ nameNum++;
+ newCos = null;
+ $scope.cosToEdit = null;
+ dbg.p("selectedCosIdx = " + $scope.selectedCosIdx,1);
+ };
+ var onCosUpdateResp = function(data) {
+ dbg.p("in onCosUpdateResp()");
+ dbg.p("following cos returned after update", 1);
+ dbg.pj(data);
+ $scope.selectedCosIdx = cosIdxAfter(actionState.UPDATE);
+ $scope.cosToEdit = null;
+ dbg.p("selectedCosIdx = " + $scope.selectedCosIdx,1);
+ };
+ var onCosDeleteResp = function(data) {
+ dbg.p("in onCosDeleteResp()");
+ dbg.p("following returned after delete", 1);
+ dbg.pj(data);
+ if ( validCosIdx($scope.selectedCosIdx) )
+ $scope.cosList.splice($scope.selectedCosIdx,1);
+ $scope.selectedCosIdx = cosIdxAfter(actionState.DEL);
+ dbg.p("selectedCosIdx = " + $scope.selectedCosIdx,1);
+ };
+
+ //
+ // CoS Event Handlers & Utils
+ //
+
+ $scope.onCosClick = function (i) {
+ dbg.p("in onCosClick(): clicked ("+i+")");
+ $scope.selectedCosIdx = i;
+ cosActionState = actionState.IDLE;
+ dbg.p("selectedCosIdx = "+ $scope.selectedCosIdx,1);
+ }
+ $scope.onAddCos = function () {
+ dbg.p("in onAddCos()");
+ cosActionState = actionState.ADD;
+ $scope.cosActionButtonText = "Create";
+ newCos = new Cos();
+ $scope.cosToEdit = newCos;
+
+ if ($scope.availableBWs.length >=3 )
+ $scope.cosToEdit.bwSelected = $scope.availableBWs[2];
+ else
+ $scope.cosToEdit.bwSelected = $scope.availableBWs[0];
+
+ dbg.pj(newCos);
+ }
+ $scope.onUpdateCos = function () {
+ dbg.p("in onUpdateCos()");
+ if ( $scope.cosList.length <= 0 )
+ {
+ dbg.p("nos CoS's exist, no action being taken");
+ return;
+ }
+ cosActionState = actionState.UPDATE;
+ $scope.cosActionButtonText = "Update";
+ $scope.cosToEdit = $scope.cosList[$scope.selectedCosIdx];
+
+ dbg.p("looking for dd list bw: " + $scope.cosToEdit.commitedInfoRate);
+ var availBwIdx = availableBwIdx($scope.cosToEdit.commitedInfoRate);
+ if ( availBwIdx < 0 || availBwIdx > $scope.availableBWs.length-1 ) {
+ dbg.e("invalid selected BW idx: "+availBwIdx+"(setting to 0)");
+ $scope.cosToEdit.bwSelected = $scope.availableBWs[0];
+ }
+ else {
+ $scope.cosToEdit.bwSelected = $scope.availableBWs[availBwIdx];
+ }
+
+ dbg.p("about to edit cos:");
+ dbg.pj($scope.cosToEdit);
+
+
+ }
+ $scope.onDelCos = function () {
+ dbg.p("in onDelCos()");
+ if ( $scope.cosList.length <= 0 )
+ {
+ dbg.p("nos CoS's exist, no action being taken");
+ return;
+ }
+ cosActionState = actionState.DEL;
+ $scope.cosActionButtonText = "Delete";
+ }
+
+ $scope.onCosInputSubmit = function () {
+ dbg.p("in onCosInputSubmit()");
+
+ var cosToOperateOn = null;
+ if ( cosActionState === actionState.ADD)
+ cosToOperateOn = newCos;
+ else
+ cosToOperateOn = $scope.cosList[$scope.selectedCosIdx];
+
+ switch(cosActionState) {
+
+ case actionState.DEL:
+ dbg.p("about to delete " + cosToOperateOn.id,1);
+ cosServices.deleteCos(cosToOperateOn)
+ .then(onCosDeleteResp, onRequestError);
+ break;
+
+ case actionState.ADD:
+ cosToOperateOn.commitedInfoRate = cosToOperateOn.bwSelected.cir;
+ delete cosToOperateOn.bwSelected;
+ dbg.p("about to add " + cosToOperateOn.id, 1);
+ cosServices.createCos(cosToOperateOn)
+ .then(onCosCreateResp, onRequestError);
+ break;
+
+ case actionState.UPDATE:
+ cosToOperateOn.commitedInfoRate = cosToOperateOn.bwSelected.cir;
+ delete cosToOperateOn.bwSelected;
+ dbg.p("about to update " + cosToOperateOn.id, 1);
+ cosServices.updateCos(cosToOperateOn)
+ .then(onCosUpdateResp, onRequestError);
+ break;
+
+ default:
+ dbg.e("invalid action state");
+ break;
+ }
+
+ cosActionState = actionState.IDLE;
+ }
+
+ //
+ // State query fxns
+ //
+
+ $scope.cosActionInProgress = function () {
+ return ( cosActionState != actionState.IDLE )
+ }
+ $scope.cosEditInProgress = function () {
+ return ( cosActionState === actionState.ADD ||
+ cosActionState === actionState.UPDATE );
+ }
+ $scope.cosUpdateInProgress = function () {
+ return ( cosActionState === actionState.UPDATE );
+ }
+ $scope.cosAddInProgress = function () {
+ return ( cosActionState === actionState.ADD );
+ }
+ $scope.cosDelInProgress = function () {
+ return ( cosActionState === actionState.DEL );
+ }
+ $scope.cosNameConflict = function () {
+ return ( $scope.cosAddInProgress() &&
+ $scope.cosExists($scope.cosToEdit.id));
+ }
+ $scope.showCosInputs = function () {
+ return $scope.cosEditInProgress();
+ }
+ $scope.showCosValues = function () {
+ return (!$scope.showCosInputs() &&
+ validCosIdx($scope.selectedCosIdx));
+ }
+ $scope.showCosNameInput = function () {
+ return $scope.cosAddInProgress();
+ }
+ $scope.showCosName = function () {
+ return (! $scope.showCosNameInput());
+ }
+ $scope.showCosActionButton = function () {
+ return ( $scope.cosAddInProgress() ||
+ ($scope.cosActionInProgress() &&
+ $scope.cosList.length > 0 ));
+ }
+ $scope.activateCosActionButton = function () {
+ return ( $scope.cosActionInProgress() &&
+ !$scope.cosNameConflict() )
+ }
+
+ //
+ // Watchers
+ //
+
+ // we can't get our initial CoS list until
+ // the URL has been set by config file read in
+ cosUrlWatcher = function () { return cosServices.getCosUrl(); }
+ onCosUrlChange = function (newCosUrl, oldCosUrl) {
+ if (newCosUrl !== oldCosUrl) {
+ dbg.p("detected cos url change, getting cos list");
+ cosServices.getCosList()
+ .then(onCosListResp, onRequestError);
+ }
+ }
+ $scope.$watch( cosUrlWatcher, onCosUrlChange );
+
+ // Watch for change in selected EPL so tht
+ // we can set the appropraite CoS as selected
+ eplWatcher = function () { return model.getCurrentEpl(); }
+ onEplChange = function (newEpl, oldEpl) {
+ dbg.p("in CosController:onEplChange");
+ //dbg.p("oldEpl:"); dbg.pj(oldEpl);
+ //dbg.p("newEpl:"); dbg.pj(newEpl);
+
+ // only make a chance if we have an
+ if ( newEpl ) {
+ var cosIdx = cosNameToIdx(newEpl.cos);
+ dbg.p("changing selected CoS to: " + newEpl.cos +
+ " [idx="+cosIdx+"]", 1);
+ if ( validCosIdx(cosIdx) ) {
+ cosActionState = actionState.IDLE
+ $scope.selectedCosIdx = cosIdx;
+ }
+ }
+ }
+ $scope.$watch( eplWatcher, onEplChange );
+
+};
+
+// register controller in the module
+app.controller("CosController", CosController);
+
+}()); \ No newline at end of file
diff --git a/demo-ui/app/controllers/EplController.js b/demo-ui/app/controllers/EplController.js
new file mode 100644
index 0000000..8dbdc90
--- /dev/null
+++ b/demo-ui/app/controllers/EplController.js
@@ -0,0 +1,528 @@
+(function() {
+
+var EplController = function($scope, $http, $interval, $log,
+ eplServices, cosServices, model, dbg ) {
+
+ //
+ // Controller Set Up
+ //
+ //
+
+ dbg.p("---> in EplController()");
+
+ actionState = {
+ IDLE : 1, ADD : 2, UPDATE : 3, DEL : 4
+ };
+
+ // $scope variables
+ $scope.eplList = [];
+ $scope.selectedEplIdx = -1;
+ $scope.eplToEdit = null;
+ $scope.eplActionButtonText = "unset";
+ $scope.availableUnis = [];
+ $scope.availableCosIds = [];
+
+ // for trouble shooting
+ $scope.availableCosIds.push("unset cos 0");
+ $scope.availableCosIds.push("unset cos 1");
+ $scope.availableCosIds.push("unset cos 2");
+
+ // controller variables
+ var _uniqueId = true;
+ var _nameNum = 1;
+ var _eplActionState = actionState.IDLE;
+ var _newEpl = null;
+
+ // start out with null until we do list query)
+ model.setCurrentEpl(null);
+
+ //
+ // utility fxns
+ //
+
+ var validEplIdx = function(idx) {
+ return ( idx >= 0 && idx < $scope.eplList.length )
+ }
+ var eplIdxReset = function() {
+ return ( $scope.eplList.length > 0 ? 0 : -1);
+ }
+ var cosIdxReset = function() {
+ return ( $scope.eplList.length > 0 ? 0 : -1);
+ }
+ var eplIdxAfter = function( action ) {
+ switch ( action ) {
+ case actionState.DEL:
+ if ( validEplIdx($scope.selectedEplIdx) )
+ return $scope.selectedEplIdx;
+ else if ( validEplIdx($scope.selectedEplIdx-1) )
+ return $scope.selectedEplIdx-1;
+ else
+ return eplIdxReset();
+ break;
+ case actionState.ADD:
+ return ( $scope.eplList.length >= 0 ?
+ $scope.eplList.length-1 : eplIdxReset() );
+ break;
+ case actionState.UPDATE:
+ return ( validEplIdx($scope.selectedEplIdx) ?
+ $scope.selectedEplIdx : eplIdxReset() );
+ break;
+ default:
+ dbg.e("invalid action state");
+ return -1;
+ break;
+ }
+ }
+ $scope.eplExists = function(eplId) {
+ var nameCheck = function (eplElm) { return eplElm.id == eplId; }
+ return ( $scope.eplList &&
+ $scope.eplList.filter(nameCheck).length > 0 );
+ }
+ var setEplUniInfo = function(epl, uniIdx1, uniIdx2) {
+
+ if ( $scope.availableUnis.length < 2 ) {
+ dbg.e("Can't set unis in constructor, avaulable uni list too short")
+ return;
+ }
+
+ epl.numCustLocations = 2;
+
+ if ( uniIdx1 < 0 || uniIdx1 > 1) {
+ epl.uniHostIpList.push($scope.availableUnis[0].ip);
+ epl.uniHostMacList.push($scope.availableUnis[0].mac);
+ epl.custAddressList.push($scope.availableUnis[0].address);
+ }
+ else {
+ epl.uniHostIpList.push($scope.availableUnis[uniIdx1].ip);
+ epl.uniHostMacList.push($scope.availableUnis[uniIdx1].mac);
+ epl.custAddressList.push($scope.availableUnis[uniIdx1].address);
+ }
+
+ if ( uniIdx2 < 0 || uniIdx2 > 1) {
+ epl.uniHostIpList.push($scope.availableUnis[0].ip);
+ epl.uniHostMacList.push($scope.availableUnis[0].mac);
+ epl.custAddressList.push($scope.availableUnis[0].address);
+ }
+ else {
+ epl.uniHostIpList.push($scope.availableUnis[uniIdx2].ip);
+ epl.uniHostMacList.push($scope.availableUnis[uniIdx2].mac);
+ epl.custAddressList.push($scope.availableUnis[uniIdx2].address);
+ }
+ }
+
+ // return index of avilable Uni based on IP (-1 if not found)
+ var availableUniIdx = function (uniIp) {
+ for ( var i = 0; i < $scope.availableUnis.length; i++ ) {
+ if ($scope.availableUnis[i].ip == uniIp) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ var updateEplWithSelectedUnis = function(epl) {
+
+ if ( epl == null || $scope.eplToEdit == null ) {
+ dbg.e("can't update EPL with UNIS, missing object");
+ return
+ }
+
+ epl.uniHostIpList = [];
+ epl.uniHostIpList.push($scope.eplToEdit.uni1Selected.ip);
+ epl.uniHostIpList.push($scope.eplToEdit.uni2Selected.ip);
+
+ epl.uniHostMacList = [];
+ epl.uniHostMacList.push($scope.eplToEdit.uni1Selected.mac);
+ epl.uniHostMacList.push($scope.eplToEdit.uni2Selected.mac);
+
+ epl.custAddressList = [];
+ epl.custAddressList.push($scope.eplToEdit.uni1Selected.address);
+ epl.custAddressList.push($scope.eplToEdit.uni2Selected.address);
+ }
+
+ //
+ // epl constructor
+ //
+
+ var Epl = function(){
+ dbg.pj("constructing EPL");
+ if ( _uniqueId )
+ this.id = "new"+ _nameNum;
+ else
+ this.id = "new";
+
+ // Don't set COS here, we have to wait until we get the latest cos list
+ this.cos = "unset";
+
+ // EVC ID set by lower layers
+ // TODO : how does evc ID cat capture at EPL layer??
+ this.evcId = "unset";
+
+ this.uniHostIpList = [];
+ this.uniHostMacList = [];
+ this.custAddressList = [];
+
+ if ( $scope.availableUnis.length >= 2 )
+ setEplUniInfo(this, 0, 1);
+ else {
+ dbg.e("can't construct EPL, not enough uni's in the available uni list");
+ return;
+ }
+
+ dbg.pj(this);
+ }
+
+ var copyReturnedEplParams = function(srcEpl, destEpl ) {
+ destEpl.evcId = srcEpl.evcId;
+ }
+
+ var setModalActiveEpl =function () {
+ if ( $scope.selectedEplIdx >= 0 &&
+ $scope.selectedEplIdx < $scope.eplList.length )
+ {
+ model.setCurrentEpl($scope.eplList[$scope.selectedEplIdx]);
+ }
+ else
+ {
+ model.setCurrentEpl(null);
+ }
+ }
+
+ //
+ // EPL HTTP Response Handlers
+ //
+
+ var onRequestError = function(reason){
+ dbg.e("HTTP Request Problem\n" + JSON.stringify(reason));
+ };
+
+ var onEplListResp = function(data) {
+ dbg.p("in onEplListResp()");
+ $scope.eplList = data;
+
+ if ( $scope.eplList && $scope.eplList.length > 0 ) {
+ $scope.selectedEplIdx = 0;
+ model.setCurrentEpl($scope.eplList[$scope.selectedEplIdx]);
+ dbg.p("selectedEplIdx/Id = "+ $scope.selectedEplIdx + "/" +
+ $scope.eplList[$scope.selectedEplIdx].id, 1);
+ }
+ else {
+ $scope.selectedEplIdx = -1;
+ model.setCurrentEpl(null);
+ dbg.p("selectedEplIdx = "+ $scope.selectedEplIdx, 1);
+ }
+ _eplActionState = actionState.IDLE;
+ };
+ var onEplCreateResp = function(returnedEpl) {
+ dbg.p("in onEplCreateResp()");
+ dbg.p("following epl returned from create cmd", 1);
+ dbg.pj(returnedEpl);
+
+ // grab entire returned Epl, since we don't know what
+ // the svcmgr may have changed on creation
+ $scope.eplList.push(returnedEpl);
+
+ // a safer alternative?
+ //copyReturnedEplParams(returnedEpl, _newEpl );
+ //$scope.eplList.push(_newEpl);
+
+ $scope.selectedEplIdx = eplIdxAfter(actionState.ADD);
+ setModalActiveEpl();
+
+ _nameNum++;
+ _newEpl = null;
+ $scope.eplToEdit = null;
+ dbg.p("selectedEplIdx = " + $scope.selectedEplIdx,1);
+ };
+ var onEplUpdateResp = function(returnedEpl) {
+ dbg.p("in onEplUpdateResp()");
+ dbg.p("following epl returned after update", 1);
+ dbg.pj(returnedEpl);
+
+ // splice in entire returned Epl, since we don't know what
+ // the svcmgr may have changed on update (esp evc)
+ $scope.eplList[$scope.selectedEplIdx] = returnedEpl;
+
+ // a safer alternative?
+ // copyReturnedEplParams(returnedEpl, eplToEdit );
+
+
+
+ $scope.selectedEplIdx = eplIdxAfter(actionState.UPDATE);
+
+ // To force a refresh
+ model.setCurrentEpl(null);
+ setModalActiveEpl();
+
+ $scope.eplToEdit = null;
+ dbg.p("selectedEplIdx = " + $scope.selectedEplIdx,1);
+ };
+ var onEplDeleteResp = function(data) {
+ dbg.p("in onEplDeleteResp()");
+ dbg.p("following returned after delete", 1);
+ dbg.pj(data);
+ if ( validEplIdx($scope.selectedEplIdx) )
+ $scope.eplList.splice($scope.selectedEplIdx,1);
+ $scope.selectedEplIdx = eplIdxAfter(actionState.DEL);
+ setModalActiveEpl();
+ dbg.p("selectedEplIdx = " + $scope.selectedEplIdx,1);
+ };
+
+ //
+ // Epl Event Handlers
+ //
+
+ $scope.onEplClick = function (i) {
+ dbg.p("in onEplClick(): clicked ("+i+")");
+ $scope.selectedEplIdx = i;
+ _eplActionState = actionState.IDLE;
+ eplToOperateOn = $scope.eplList[$scope.selectedEplIdx]
+ model.setCurrentEpl(eplToOperateOn);
+ dbg.p("selectedEplIdx/Id = "+ $scope.selectedEplIdx + "/" +
+ eplToOperateOn.id, 1);
+ }
+ $scope.onAddEpl = function () {
+ dbg.p("in onAddEpl()");
+ _eplActionState = actionState.ADD;
+ $scope.eplActionButtonText = "Create";
+
+ var updateEplToAddOnCosListResponse = function(data) {
+ dbg.p("in updateNewEplCosOnCosListResponse()", 1);
+ var cosList = data;
+
+ $scope.availableCosIds = [];
+ for ( var i = 0; i < cosList.length; i++ )
+ $scope.availableCosIds.push(cosList[i].id);
+
+ dbg.p("Avilable CoS List");
+ dbg.pj($scope.availableCosIds);
+
+ // defaults for drop down lists
+ if ( $scope.availableCosIds.length > 0 )
+ $scope.eplToEdit.cos = $scope.availableCosIds[0];
+
+ $scope.eplToEdit.uni1Selected = $scope.availableUnis[0];
+ $scope.eplToEdit.uni2Selected = $scope.availableUnis[1];
+
+ dbg.p("Newly created EPL to add after CoS list retrieved");
+ dbg.pj($scope.eplToEdit);
+ };
+
+ _newEpl = new Epl();
+ $scope.eplToEdit = _newEpl;
+
+ // need to get the current cos list before adding new EPL
+ cosServices.getCosList()
+ .then(updateEplToAddOnCosListResponse,
+ onRequestError);
+ }
+ $scope.onUpdateEpl = function () {
+ dbg.p("in onUpdateEpl()");
+ if ( $scope.eplList.length <= 0 )
+ {
+ dbg.p("nos Epl's exist, no action being taken");
+ return;
+ }
+ _eplActionState = actionState.UPDATE;
+ $scope.eplActionButtonText = "Update";
+
+ var updateEplToEditOnCosListResponse = function(data) {
+ dbg.p("in onCosListResp()");
+ var cosList = data;
+ $scope.availableCosIds = [];
+ for ( var i = 0; i < cosList.length; i++ )
+ $scope.availableCosIds.push(cosList[i].id);
+ dbg.p("Avilable CoS List");
+ dbg.pj($scope.availableCosIds);
+
+ $scope.eplToEdit = $scope.eplList[$scope.selectedEplIdx];
+
+ // TODO (if time): put below stuff in fxn since duplicated twice
+ dbg.p("looking for dd list ip: " + $scope.eplToEdit.uniHostIpList[0] );
+ var availUniIdx = availableUniIdx($scope.eplToEdit.uniHostIpList[0]);
+ if ( availUniIdx < 0 || availUniIdx > $scope.availableUnis.length-1 ) {
+ dbg.e("invalid selected uni-1 idx: "+availUniIdx+"(setting to 0)");
+ $scope.eplToEdit.uni1Selected = $scope.availableUnis[0];
+ }
+ else {
+ $scope.eplToEdit.uni1Selected = $scope.availableUnis[availUniIdx];
+ }
+
+ dbg.p("looking for dd list ip: " + $scope.eplToEdit.uniHostIpList[1] );
+ availUniIdx = availableUniIdx($scope.eplToEdit.uniHostIpList[1]);
+ if ( availUniIdx < 0 || availUniIdx > $scope.availableUnis.length-1 ) {
+ dbg.e("invalid selected uni-2 idx: "+availUniIdx+"(setting to 0)");
+ $scope.eplToEdit.uni2Selected = $scope.availableUnis[0]
+ }
+ else {
+ $scope.eplToEdit.uni2Selected = $scope.availableUnis[availUniIdx];
+ }
+
+ dbg.p("about to edit epl:");
+ dbg.pj($scope.eplToEdit);
+ };
+
+ // need to get the current cos list before editing
+ cosServices.getCosList()
+ .then(updateEplToEditOnCosListResponse,
+ onRequestError);
+ }
+
+ $scope.onDelEpl = function () {
+ dbg.p("in onDelEpl()");
+ if ( $scope.eplList.length <= 0 )
+ {
+ dbg.p("nos Epl's exist, no action being taken");
+ return;
+ }
+ _eplActionState = actionState.DEL;
+ $scope.eplActionButtonText = "Delete";
+ }
+
+ $scope.onEplInputSubmit = function () {
+ dbg.p("in onEplInputSubmit()");
+
+ var eplToOperateOn = null;
+ if ( _eplActionState === actionState.ADD)
+ eplToOperateOn = _newEpl;
+ else
+ eplToOperateOn = $scope.eplList[$scope.selectedEplIdx];
+
+ switch(_eplActionState) {
+
+ case actionState.DEL:
+
+ dbg.p("about to delete " + eplToOperateOn.id,1);
+ eplServices.deleteEpl(eplToOperateOn)
+ .then(onEplDeleteResp, onRequestError);
+ break;
+
+ case actionState.ADD:
+
+ updateEplWithSelectedUnis(eplToOperateOn);
+
+ delete $scope.eplToEdit.uni1Selected;
+ delete $scope.eplToEdit.uni2Selected;
+
+ dbg.p("about to add " + eplToOperateOn.id, 1);
+ dbg.pj(eplToOperateOn);
+ eplServices.createEpl(eplToOperateOn)
+ .then(onEplCreateResp, onRequestError);
+ break;
+
+ case actionState.UPDATE:
+
+ updateEplWithSelectedUnis(eplToOperateOn);
+
+ delete $scope.eplToEdit.uni1Selected;
+ delete $scope.eplToEdit.uni2Selected;
+
+ dbg.p("about to update " + eplToOperateOn.id, 1);
+ eplServices.updateEpl(eplToOperateOn)
+ .then(onEplUpdateResp, onRequestError);
+ break;
+
+ default:
+ dbg.e("invalid action state");
+ break;
+ }
+
+ _eplActionState = actionState.IDLE;
+ }
+
+ //
+ // State query fxns
+ //
+
+ $scope.eplActionInProgress = function () {
+ return ( _eplActionState != actionState.IDLE )
+ }
+ $scope.eplEditInProgress = function () {
+ return ( _eplActionState === actionState.ADD ||
+ _eplActionState === actionState.UPDATE );
+ }
+ $scope.eplUpdateInProgress = function () {
+ return ( _eplActionState === actionState.UPDATE );
+ }
+ $scope.eplAddInProgress = function () {
+ return ( _eplActionState === actionState.ADD );
+ }
+ $scope.eplDelInProgress = function () {
+ return ( _eplActionState === actionState.DEL );
+ }
+ $scope.cosExists = function () {
+ return ( $scope.availableCosIds &&
+ $scope.availableCosIds.length > 0 );
+ }
+ $scope.cosConflict = function () {
+ return ( $scope.eplAddInProgress() &&
+ !$scope.cosExists() );
+ }
+ $scope.eplNameConflict = function () {
+ return ( $scope.eplAddInProgress() &&
+ $scope.eplExists($scope.eplToEdit.id));
+ }
+ $scope.uniConflict = function () {
+ return ( $scope.eplToEdit &&
+ $scope.eplToEdit.uni1Selected &&
+ $scope.eplToEdit.uni2Selected &&
+ $scope.eplEditInProgress() &&
+ ( $scope.eplToEdit.uni1Selected.ip ==
+ $scope.eplToEdit.uni2Selected.ip ));
+ }
+ $scope.showEplInputs = function () {
+ return $scope.eplEditInProgress();
+ }
+ $scope.showEplValues = function () {
+ return (!$scope.showEplInputs() &&
+ validEplIdx($scope.selectedEplIdx));
+ }
+ $scope.showEplNameInput = function () {
+ return $scope.eplAddInProgress();
+ }
+ $scope.showEplName = function () {
+ return (! $scope.showEplNameInput());
+ }
+ $scope.showEplActionButton = function () {
+ return ( $scope.eplAddInProgress() ||
+ ($scope.eplActionInProgress() &&
+ $scope.eplList.length > 0 ));
+ }
+ $scope.activateEplActionButton = function () {
+ return ( $scope.eplActionInProgress() &&
+ !$scope.eplNameConflict() &&
+ !$scope.uniConflict() &&
+ !$scope.cosConflict() )
+ }
+
+ //
+ // Initial Page Set Up
+ //
+
+ eplUrlWatcher = function () { return eplServices.getEplUrl(); }
+ onEplUrlChange = function (newEplUrl, oldEplUrl) {
+ if (newEplUrl !== oldEplUrl) {
+ dbg.p("detected epl url change, getting epl list");
+ eplServices.getEplList()
+ .then(onEplListResp, onRequestError);
+ }
+ }
+ $scope.$watch( eplUrlWatcher, onEplUrlChange );
+
+
+ uniListWatcher = function () { return model.getAvailableUnis(); }
+ onUniListChange = function (newUniList, oldUniList) {
+ if (newUniList !== oldUniList) {
+ dbg.p("detected uni list change")
+ dbg.p("new avilable uni list:")
+ $scope.availableUnis = newUniList;
+ dbg.pj($scope.availableUnis);
+ }
+ }
+ $scope.$watch( uniListWatcher, onUniListChange );
+};
+
+// register controller in the module
+app.controller("EplController", EplController);
+
+}()); \ No newline at end of file
diff --git a/demo-ui/app/controllers/MainController.js b/demo-ui/app/controllers/MainController.js
new file mode 100644
index 0000000..199048b
--- /dev/null
+++ b/demo-ui/app/controllers/MainController.js
@@ -0,0 +1,31 @@
+// Create our module
+var app = angular.module("vcpe", []);
+
+(function() {
+var MainController = function($http, $log, cosServices, eplServices, mefServices, model, dbg ) {
+
+ dbg.p("---> in MainController()");
+
+ // config our app
+ var cfgFile = "../config.json";
+ $http.get(cfgFile).
+ success(function(data) {
+ dbg.p("in cfgFile read callback");
+ dbg.p("incoming data");
+ dbg.pj(data);
+ cosServices.setCosUrl(data.cosMgr);
+ eplServices.setEplUrl(data.eplMgr);
+ mefServices.setEvcUrl(data.evcMgr);
+ mefServices.setUniUrl(data.uniMgr);
+ mefServices.setEvcPathUrl(data.evcPathMgr);
+ model.setAvailableUnis(data.uniList);
+ }).
+ error(function() {
+ dbg.e("Could not read " + cfgFile);
+ });
+};
+
+// register controller in the module
+app.controller("MainController", MainController);
+
+}()); \ No newline at end of file
diff --git a/demo-ui/app/controllers/MefController.js b/demo-ui/app/controllers/MefController.js
new file mode 100644
index 0000000..9531b49
--- /dev/null
+++ b/demo-ui/app/controllers/MefController.js
@@ -0,0 +1,117 @@
+(function() {
+
+var MefController = function($scope, $log, mefServices, model, dbg ) {
+
+ //
+ // Controller Set Up
+ //
+
+ dbg.p("---> in MefController()");
+ $scope.currentEpl = null;
+ $scope.currentEplEvc = null;
+ $scope.currentEvcUnis = [];
+
+ //
+ // Utils
+ //
+
+ $scope.curEplEvcToJson = function () {
+ return angular.toJson($scope.currentEplEvc, true);
+ };
+
+
+ $scope.uniToSpeedString = function(uni) {
+ if (uni) {
+ speed = uni.uni[0].speed;
+ return Object.getOwnPropertyNames(speed)[0];s
+ }
+ else
+ return "";
+ }
+ //
+ // HTTP Response Handlers
+ //
+
+ var onRequestError = function(reason){
+ dbg.e("HTTP Request Problem\n" + JSON.stringify(reason));
+ };
+
+
+ var onUniGetResp = function(data) {
+ dbg.p("in onUniGetResp()");
+ dbg.pj(data);
+
+ // dbg.p("current EVC UNI list before adding");
+ //oSpeedString($scop dbg.pj($scope.currentEvcUnis);
+
+ $scope.currentEvcUnis.push(data);
+
+ // dbg.p("current EVC UNI list after adding");
+ // dbg.pj($scope.currentEvcUnis);
+ }
+
+ var onEvcGetResp = function(data) {
+ dbg.p("in onEvcGetResp()");
+ dbg.pj(data);
+ $scope.currentEplEvc = data;
+
+ // out with the old unis
+ $scope.currentEvcUnis = [];
+
+ // in with the new unis
+ mefServices.getUni($scope.currentEplEvc.uniIdList[0])
+ .then(onUniGetResp, onRequestError);
+ mefServices.getUni($scope.currentEplEvc.uniIdList[1])
+ .then(onUniGetResp, onRequestError);
+ };
+
+ //
+ // State query fxns
+ //
+
+ $scope.showEvcValues = function () {
+ return ( $scope.currentEplEvc != null );
+ }
+
+ $scope.showUniValues = function () {
+ return ( $scope.currentEvcUnis != null );
+ }
+
+ //
+ // Watch for change in selected EPL so tht we can
+ // update the corresponding EVC info
+ //
+
+ eplWatcher = function () { return model.getCurrentEpl(); }
+ onEplChange = function (newEpl, oldEpl) {
+ dbg.p("in MefController:onEplChange");
+ // dbg.p("oldEpl:"); dbg.pj(oldEpl);
+ // dbg.p("newEpl:"); dbg.pj(newEpl);
+
+ // only make a chance if we have a new EPL object
+ if ( newEpl ) {
+ var newEplId = newEpl.id;
+ var oldEplId = "null";
+ if (oldEpl) oldId = oldEpl.id
+
+ dbg.p("detected selected EPL change from " +
+ oldEplId + " to " + newEplId, 1);
+ $scope.currentEpl = newEpl;
+
+ // get the EVC info
+ mefServices.getEvc($scope.currentEpl.evcId)
+ .then(onEvcGetResp, onRequestError);
+ }
+ else {
+ $scope.currentEplEvc = null;
+ $scope.currentEvcUnis = null;
+ }
+
+ }
+ $scope.$watch( eplWatcher, onEplChange );
+};
+
+// register controller in the module
+app.controller("MefController", MefController);
+
+}()); \ No newline at end of file
diff --git a/demo-ui/app/services/cosServices.js b/demo-ui/app/services/cosServices.js
new file mode 100644
index 0000000..c650b6d
--- /dev/null
+++ b/demo-ui/app/services/cosServices.js
@@ -0,0 +1,64 @@
+(function(){
+
+ var cosServices = function ($http, dbg) {
+
+ var _cosBasePath = "/cosmgr/webapi/cos"
+ var _cosUrl = "unset";
+
+ var setCosUrl = function (cosMgrCfg) {
+ dbg.p("in setCosUrl");
+ _cosUrl = "http://"+cosMgrCfg.ip+":"+cosMgrCfg.port+_cosBasePath;
+ dbg.p(_cosUrl);
+ }
+
+ var getCosUrl = function () {
+ dbg.p("in getCosUrl");
+ return _cosUrl;
+ }
+
+ var getCosList = function() {
+ dbg.p("in cosServices.getCosList()",2);
+ var url = _cosUrl+ "/list;"
+ dbg.p("GET: " + url , 2);
+ return $http.get(url)
+ .then(function(response){ return response.data; });
+ };
+ var createCos = function(cos) {
+ dbg.p("in cosServices.createCos()",2)
+ var url = _cosUrl;
+ dbg.p("POST: " + url, 2);
+ dbg.pj(cos);
+ return $http.post(url, cos)
+ .then(function(response){ return response.data; });
+ };
+ var updateCos = function(cos) {
+ dbg.p("in cosServices.updateCos()",2)
+ var url = _cosUrl + "/" + cos.id;
+ dbg.p("PUT: " + url, 2);
+ dbg.pj(cos);
+ return $http.put(url, cos)
+ .then(function(response){ return response.data; });
+ };
+ var deleteCos = function(cos) {
+ dbg.p("in cosServices.deleteCos()",2)
+ var url = _cosUrl + "/" + cos.id;
+ dbg.p("DELELE: " + url, 2);
+ dbg.pj(cos);
+ return $http.delete(url)
+ .then(function(response){ return response.data; });
+ };
+
+ return { // public API
+ setCosUrl : setCosUrl,
+ getCosUrl : getCosUrl,
+ createCos : createCos,
+ getCosList : getCosList,
+ deleteCos : deleteCos,
+ updateCos : updateCos
+ };
+ };
+
+ var module = angular.module("vcpe");
+ module.factory("cosServices", cosServices);
+
+}()); \ No newline at end of file
diff --git a/demo-ui/app/services/dbg.js b/demo-ui/app/services/dbg.js
new file mode 100644
index 0000000..1e12523
--- /dev/null
+++ b/demo-ui/app/services/dbg.js
@@ -0,0 +1,54 @@
+(function(){
+
+ var dbg = function ($log) {
+
+ var dbgFlag = true;
+
+ var on = function() { dbgFlag = true; };
+ var off = function() { dbgFlag = false; };
+
+ var p = function(str, tab, emphasize) {
+ if ( dbgFlag )
+ {
+ var tabStr = "";
+ if (tab) {
+ for (var i = 0; i < tab; i++ )
+ tabStr += " ";
+ }
+ var preStr = "... " + tabStr;
+ var postStr = "";
+ if ( emphasize ) {
+ preStr = tabStr + "####################### ";
+ postStr = " ####################### ";
+ }
+ $log.info(preStr + str + postStr);
+ }
+ };
+
+ var pj = function(obj) {
+ if ( dbgFlag )
+ $log.info(JSON.stringify(obj,null,3))
+ };
+
+ var e = function(eMsg) {
+ $log.error("!! ERROR: " + eMsg);
+ }
+
+ var w = function(eMsg) {
+ $log.warn("!! WARNIING: " + eMsg);
+ }
+
+ return { // Public API
+ on : on, // squelch DBG printing
+ off : off, // enable DBG printing
+ p : p, // print a debug string
+ pj : pj, // print JSON version of an object
+ w : w, // print an warning msg
+ e : e // print an error msg
+ };
+ };
+
+ var module = angular.module("vcpe");
+ module.factory("dbg", dbg);
+
+}()); \ No newline at end of file
diff --git a/demo-ui/app/services/eplServices.js b/demo-ui/app/services/eplServices.js
new file mode 100644
index 0000000..20fb7af
--- /dev/null
+++ b/demo-ui/app/services/eplServices.js
@@ -0,0 +1,64 @@
+(function(){
+
+ var eplServices = function ($http, dbg) {
+
+ var _eplBasePath = "/svcmgr/webapi/svc/epl"
+ var _eplUrl = "unset";
+
+ var setEplUrl = function (eplMgrCfg) {
+ dbg.p("in setEplUrl");
+ _eplUrl = "http://"+eplMgrCfg.ip+":"+eplMgrCfg.port+_eplBasePath;
+ dbg.p(_eplUrl);
+ }
+
+ var getEplUrl = function () {
+ dbg.p("in setEplUrl");
+ return _eplUrl;
+ }
+
+ var getEplList = function() {
+ dbg.p("in eplServices.getEplList()",2);
+ var url = _eplUrl+ "/list;"
+ dbg.p("GET: " + url , 2);
+ return $http.get(url)
+ .then(function(response){ return response.data; });
+ };
+ var createEpl = function(epl) {
+ dbg.p("in eplServices.createEpl()",2)
+ var url = _eplUrl;
+ dbg.p("POST: " + url, 2);
+ dbg.pj(epl);
+ return $http.post(url, epl)
+ .then(function(response){ return response.data; });
+ };
+ var updateEpl = function(epl) {
+ dbg.p("in eplServices.updateEpl()",2)
+ var url = _eplUrl + "/" + epl.id;
+ dbg.p("PUT: " + url, 2);
+ dbg.pj(epl);
+ return $http.put(url, epl)
+ .then(function(response){ return response.data; });
+ };
+ var deleteEpl = function(epl) {
+ dbg.p("in eplServices.deleteEpl()",2)
+ var url = _eplUrl + "/" + epl.id;
+ dbg.p("DELELE: " + url, 2);
+ dbg.pj(epl);
+ return $http.delete(url)
+ .then(function(response){ return response.data; });
+ };
+
+ return { // public API
+ setEplUrl : setEplUrl,
+ getEplUrl : getEplUrl,
+ createEpl : createEpl,
+ getEplList : getEplList,
+ deleteEpl : deleteEpl,
+ updateEpl : updateEpl
+ };
+ };
+
+ var module = angular.module("vcpe");
+ module.factory("eplServices", eplServices);
+
+}()); \ No newline at end of file
diff --git a/demo-ui/app/services/mefServices.js b/demo-ui/app/services/mefServices.js
new file mode 100644
index 0000000..798ceb3
--- /dev/null
+++ b/demo-ui/app/services/mefServices.js
@@ -0,0 +1,71 @@
+(function(){
+
+ var mefServices = function ($http, dbg) {
+
+ var _evcBasePath = "/evcmgr/webapi/evc"
+ var _evcUrl = "unset";
+
+ var _uniBasePath = "/restconf/operational/cl-vcpe-mef:unis/uni"
+ var _uniUrl = "unset";
+
+ var _evcPathBasePath = "/restconf/operational/cl-vcpe-mef:evcs"
+ var _evcPathUrl = "unset";
+
+ //
+ // Configuration services
+ //
+
+ var setEvcUrl = function (evcMgrCfg) {
+ dbg.p("in setEvcUrl");
+ _evcUrl = "http://"+evcMgrCfg.ip+":"+evcMgrCfg.port+_evcBasePath;
+ dbg.p(_evcUrl);
+ }
+
+ var setUniUrl = function (uniMgrCfg) {
+ dbg.p("in setUniUrl");
+ _uniUrl = "http://"+uniMgrCfg.ip+":"+uniMgrCfg.port+_uniBasePath;
+ dbg.p(_uniUrl);
+ }
+
+ var setEvcPathUrl = function (evcPathMgrCfg) {
+ dbg.p("in setEvcPathUrl");
+ _evcPathUrl = "http://"+evcPathMgrCfg.ip+":"+evcPathMgrCfg.port+_evcPathBasePath;
+ dbg.p(_evcPathUrl);
+ }
+
+ //
+ // REST services
+ //
+
+ var getEvc = function(evcId) {
+ dbg.p("in mefServices.getEvc()",2)
+ var url = _evcUrl + "/" + evcId;
+ dbg.p("GET: " + url, 2);
+ return $http.get(url)
+ .then(function(response){ return response.data; });
+ };
+
+ var getUni = function(uniId) {
+ dbg.p("in mefServices.getUni()",2)
+ var url = _uniUrl + "/" + uniId;
+ dbg.p("GET: " + url, 2);
+ // return $http.get(url)
+ // .then(function(response){ return response.data; });
+ return $http({method: 'GET', url: url,
+ headers: {'Authorization': 'Basic YWRtaW46YWRtaW4='}})
+ .then(function(response){ return response.data; });
+ };
+
+ return { // public API
+ setEvcUrl : setEvcUrl,
+ setUniUrl : setUniUrl,
+ setEvcPathUrl : setEvcPathUrl,
+ getEvc : getEvc,
+ getUni : getUni
+ };
+ };
+
+ var module = angular.module("vcpe");
+ module.factory("mefServices", mefServices);
+
+}()); \ No newline at end of file
diff --git a/demo-ui/app/services/model.js b/demo-ui/app/services/model.js
new file mode 100644
index 0000000..c41794b
--- /dev/null
+++ b/demo-ui/app/services/model.js
@@ -0,0 +1,61 @@
+//
+// For data that must be shared accross controllers
+//
+
+(function(){
+var model = function ($log, dbg) {
+
+ var _shared = {
+ availableUnis : [],
+ currentEpl : null
+ };
+
+ var getAvailableUnis = function () {
+ return _shared.availableUnis;
+ }
+
+ var setAvailableUnis = function (availableUnis) {
+ dbg.p("in model:setAvailableUnis");
+ _shared.availableUnis = availableUnis;
+ dbg.pj(_shared.availableUnis);
+ }
+
+ // var getCurrentEplId = function () {
+ // // dbg.p("in model:getCurrentEplId, returning: "+_shared.currentEplId);
+ // return _shared.currentEplId;
+ // }
+
+ // var setCurrentEplId = function (currentEplId) {
+ // // dbg.p("in model:setCurrentEplId, setting to: "+currentEplId);
+ // _shared.currentEplId = currentEplId;
+ // }
+
+ var getCurrentEpl = function () {
+ dbg.p("in model:getCurrentEpl:");
+ //dbg.pj(_shared.currentEpl);
+ return _shared.currentEpl;
+ }
+
+ var setCurrentEpl = function (currentEpl) {
+ // dbg.p("in model:setCurrentEplId, setting to: "+currentEplId);
+ dbg.p("in model:setCurrentEpl");
+ //dbg.pj(currentEpl);
+ _shared.currentEpl = currentEpl;
+ }
+
+
+ var dumpShareDdata = function() { dbg.pj(_shared); };
+
+ return { // Public API
+ getAvailableUnis : getAvailableUnis,
+ setAvailableUnis : setAvailableUnis,
+ getCurrentEpl : getCurrentEpl,
+ setCurrentEpl : setCurrentEpl,
+ dumpShareDdata : dumpShareDdata
+ };
+ };
+
+ var module = angular.module("vcpe");
+ module.factory("model", model);
+
+}()); \ No newline at end of file
diff --git a/demo-ui/app/views/cos-panel.html b/demo-ui/app/views/cos-panel.html
new file mode 100644
index 0000000..682f154
--- /dev/null
+++ b/demo-ui/app/views/cos-panel.html
@@ -0,0 +1,171 @@
+<!--
+
+To Do
+ Change Bandwidth to drop down, with selected levels (10M, 100M, 1G, etc)
+ -->
+
+<div class="primary-lable ">Service Levels</div>
+<div class="cos-list">
+ <div class="list-item choosable"
+ ng-repeat="cos in cosList track by $index"
+
+ ng-class="{'selected-item-idle' : $index === selectedCosIdx &&
+ !cosActionInProgress(),
+ 'selected-item-update' : $index === selectedCosIdx &&
+ cosUpdateInProgress(),
+ 'selected-item-delete' : $index === selectedCosIdx &&
+ cosDelInProgress() }"
+ ng-click="onCosClick($index)">
+ {{ cos.id }}
+ </div>
+</div>
+
+<div class = "action-icon-container">
+
+ <span class="action-icon glyphicon glyphicon-plus"
+ ng-click="onAddCos()"
+ ng-class="{'action' : cosAddInProgress()}">
+ </span>
+
+ <span class="action-icon glyphicon glyphicon-pencil"
+ ng-click="onUpdateCos()"
+ ng-class="{'action' : cosUpdateInProgress()}">
+ </span>
+
+ <span class="action-icon glyphicon glyphicon-minus"
+ ng-click="onDelCos()"
+ ng-class="{'action' : cosDelInProgress()}">
+ </span>
+</div>
+
+<div class="cos-info-contaier">
+ <form role="form" ng-submit="onCosInputSubmit()">
+
+ <!-- Name -->
+
+ <div class="data-row">
+ <div class="label-col">Name:</div>
+ <div class="data-col" ng-show="showCosName()">
+ <span class="data-item">
+ {{ cosList[selectedCosIdx].id }}
+ </span>
+ </div>
+ <div class="data-col" ng-show="showCosNameInput()">
+ <input class="data-input" name="id"
+ type="text" ng-required="showCosInputs()"
+ ng-model="cosToEdit.id">
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="label-col">Bandwidth:</div>
+ <div class="data-col" ng-show="showCosValues()">
+
+ <span class="data-item">
+ {{ bwText(cosList[selectedCosIdx]) }}
+ </span>
+ </div>
+ <div class="data-col" ng-show="showCosInputs()">
+ <select class="cos-dd-input"
+ name="bandwidth"
+ ng-required="showCosInputs()"
+ ng-model="cosToEdit.bwSelected"
+ ng-options="bw.name for bw in availableBWs"
+ >
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Availability -->
+
+ <div class="data-row">
+ <div class="label-col">Availability:</div>
+ <div class="data-col" ng-show="showCosValues()">
+ <span class="data-item">
+ {{ cosList[selectedCosIdx].availbility }}
+ </span>
+ <span class="data-unit">%</span>
+ </div>
+ <div class="data-col" ng-show="showCosInputs()">
+ <input class="data-input" name="availbility"
+ type="number" step="any" ng-required="showCosInputs()"
+ min="1" max="100"
+ ng-model="cosToEdit.availbility">
+ <span class="data-unit">%</span>
+ </div>
+ </div>
+
+ <!-- Frame Delay -->
+
+ <div class="data-row">
+ <div class="label-col">Frame Delay:</div>
+ <div class="data-col" ng-show="showCosValues()">
+ <span class="data-item">
+ {{ cosList[selectedCosIdx].frameDelay }}
+ </span>
+ <span class="data-unit">ms</span>
+ </div>
+ <div class="data-col" ng-show="showCosInputs()">
+ <input class="data-input" name="frameDelay"
+ type="number" step="any" ng-required="showCosInputs()"
+ min="0"
+ ng-model="cosToEdit.frameDelay">
+ <span class="data-unit">ms</span>
+ </div>
+ </div>
+
+ <!-- Jitter -->
+
+ <div class="data-row">
+ <div class="label-col">Jitter:</div>
+ <div class="data-col" ng-show="showCosValues()">
+ <span class="data-item">
+ {{ cosList[selectedCosIdx].jitter }}
+ </span>
+ <span class="data-unit">ms</span>
+ </div>
+ <div class="data-col" ng-show="showCosInputs()">
+ <input class="data-input" name="jitter"
+ type="number" step="any" ng-required="showCosInputs()"
+ min="0"
+ ng-model="cosToEdit.jitter">
+ <span class="data-unit">ms</span>
+ </div>
+ </div>
+
+ <!-- Frame Loss -->
+
+ <div class="data-row">
+ <div class="label-col">Frame Loss:</div>
+ <div class="data-col" ng-show="showCosValues()">
+ <span class="data-item">
+ {{ cosList[selectedCosIdx].frameLoss }}
+ </span>
+ <span class="data-unit">%</span>
+ </div>
+ <div class="data-col" name="frameLoss"ng-show="showCosInputs()">
+ <input class="data-input" name="frameLoss"
+ type="number" step="any" ng-required="showCosInputs()"
+ min="0" max="100"
+ ng-model="cosToEdit.frameLoss">
+ <span class="data-unit">%</span>
+ </div>
+ </div>
+
+
+ <div class="warning-container">
+ <div class="warning" ng-show="cosNameConflict()">Please Enter Unique Name</div>
+ </div>
+ <div class="button-container">
+ <button type="submit"
+ class="btn btn-sm my-btn-addon"
+ <div ng-class="{'btn-success' : cosAddInProgress(),
+ 'btn-warning' : cosUpdateInProgress(),
+ 'btn-danger' : cosDelInProgress()}"
+ ng-disabled="!activateCosActionButton()"
+ ng-show="showCosActionButton()")>
+ {{ cosActionButtonText }}</button>
+ </div>
+ </form>
+</div>
diff --git a/demo-ui/app/views/css/vcpe.css b/demo-ui/app/views/css/vcpe.css
new file mode 100644
index 0000000..0d08734
--- /dev/null
+++ b/demo-ui/app/views/css/vcpe.css
@@ -0,0 +1,340 @@
+input[type='number'] {
+ font-size: 12px;
+}
+.choosable {
+ cursor: pointer;
+}
+.gradient {
+ background: -webkit-linear-gradient(rgba(255, 255, 255, 0) 0%, #ffffff 100%);
+ background: linear-gradient(to bottom, #ffffff 0%, rgba(255, 255, 255, 0) 100%);
+}
+.my-btn-addon {
+ height: 30px;
+ width: 100px;
+}
+.primary-lable {
+ color: darkred;
+ font-size: large;
+ font-weight: bold;
+ text-align: center;
+}
+.list-item {
+ color: black;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ background-color: none;
+}
+.selected-item-idle {
+ background-color: #e3e3e3;
+}
+.selected-item-delete {
+ background-color: #FFE4E1;
+}
+.selected-item-update {
+ background-color: #FFEFD5;
+}
+.primary-container {
+ border: 3px solid black;
+ background-color: #e3e3e3;
+ border-radius: 15px;
+ padding: 10px;
+ height: 400px;
+}
+.secondary-container {
+ border: 1px solid darkgray;
+ background-color: white;
+ padding: 5px 10px;
+ margin: 10px 10px -1px 10px;
+ height: 125px;
+ overflow-y: scroll;
+}
+.action-icon-container {
+ border: 1px solid darkgray;
+ background: -webkit-linear-gradient(rgba(255, 255, 255, 0) 0%, #ffffff 100%);
+ background: linear-gradient(to bottom, #ffffff 0%, rgba(255, 255, 255, 0) 100%);
+ margin: 0px 10px;
+ padding: 2px 10px;
+}
+.action-icon {
+ cursor: pointer;
+ padding: 0px 1px;
+ font-size: small;
+ color: black;
+ padding: 2px;
+}
+.action {
+ color: blue !important;
+}
+.warning {
+ color: red !important;
+ font-size: small !important;
+}
+.data-row {
+ display: flex;
+ flex-flow: row nowrap;
+ height: 20px;
+ width: 300;
+}
+.label-col {
+ color: purple;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ flex: 0 0 auto;
+ order: 1;
+ width: 85px;
+}
+.data-col {
+ flex: 0 0 auto;
+ order: 2;
+ color: black;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+}
+.data-label {
+ color: purple;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ vertical-align: middle;
+}
+.data-item {
+ color: black;
+ font-size: small;
+ font-weight: bold;
+ text-align: left;
+}
+.data-input {
+ height: 20px;
+ width: 70px;
+ border-radius: 5px;
+}
+.cos-dd-input {
+ height: 20px;
+ width: 70px;
+}
+.epl-dd-input {
+ height: 20px;
+ width: 150px;
+}
+.data-unit {
+ color: black;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ margin-left: 5px;
+ vertical-align: middle;
+}
+.header-container {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+}
+.header-container .hdr {
+ flex: 1 1 auto;
+ order: 1;
+}
+.header-container .img {
+ flex: 1 1 auto;
+ order: 2;
+ align-self: flex-end;
+ margin-bottom: 12px;
+}
+.frame {
+ display: flex;
+ flex-flow: row nowrap;
+ border: 1px solid lightgray;
+ margin: 20px;
+ padding: 20px 0px;
+}
+.frame .left {
+ flex: 1 6 5%;
+ order: 1;
+}
+.frame .right {
+ flex: 1 6 5%;
+ order: 3;
+}
+.frame .content {
+ border: none;
+ flex: 1 6 90%;
+ order: 2;
+}
+.frame .content .action-container {
+ border: none;
+ display: flex;
+ flex-flow: row nowrap;
+}
+.frame .content .action-container .cos-container {
+ border: 3px solid black;
+ background-color: #e3e3e3;
+ border-radius: 15px;
+ padding: 10px;
+ height: 400px;
+ flex: 1 1 50%;
+ order: 1;
+ margin-right: 20px;
+}
+.frame .content .action-container .cos-container .cos-list {
+ border: 1px solid darkgray;
+ background-color: white;
+ padding: 5px 10px;
+ margin: 10px 10px -1px 10px;
+ height: 125px;
+ overflow-y: scroll;
+}
+.frame .content .action-container .cos-container .cos-info-contaier {
+ display: flex;
+ flex-flow: column nowrap;
+ margin: 10px 20px;
+}
+.frame .content .action-container .cos-container .cos-info-contaier .labelContainer {
+ flex: 1 1 33%;
+ order: 1;
+ min-width: 80px;
+ color: purple;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ vertical-align: middle;
+}
+.frame .content .action-container .cos-container .warning-container {
+ height: 15px;
+ margin-top: 10px;
+}
+.frame .content .action-container .epl-container {
+ border: 3px solid black;
+ background-color: #e3e3e3;
+ border-radius: 15px;
+ padding: 10px;
+ height: 400px;
+ flex: 1 1 50%;
+ order: 2;
+ margin-right: 20px;
+}
+.frame .content .action-container .epl-container .epl-list {
+ border: 1px solid darkgray;
+ background-color: white;
+ padding: 5px 10px;
+ margin: 10px 10px -1px 10px;
+ height: 125px;
+ overflow-y: scroll;
+}
+.frame .content .action-container .epl-container .epl-info-contaier {
+ display: flex;
+ flex-flow: column nowrap;
+ margin: 10px 20px;
+}
+.frame .content .action-container .epl-container .epl-info-contaier .labelContainer {
+ flex: 1 1 33%;
+ order: 1;
+ min-width: 80px;
+ color: purple;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ vertical-align: middle;
+}
+.frame .content .action-container .epl-container .warning-container {
+ height: 15px;
+ margin-top: 10px;
+}
+.frame .content .monitor-container .mef-container {
+ border: 3px solid black;
+ background-color: #e3e3e3;
+ border-radius: 15px;
+ padding: 10px;
+ height: 400px;
+ height: 450px;
+ background-color: #B0C4DE;
+ margin: 20px 20px 20px 0px;
+}
+.frame .content .monitor-container .mef-container .primary-mef-lable {
+ color: SteelBlue;
+ font-size: large;
+ font-weight: bold;
+ text-align: center;
+ margin-bottom: 10px;
+}
+.frame .content .monitor-container .mef-container .mef-panel-hdr {
+ border-bottom: 1px solid SlateGray;
+}
+.frame .content .monitor-container .mef-container .secondary-mef-label {
+ color: SlateGray;
+ font-size: small;
+ font-weight: bold;
+ text-align: center;
+ background-color: #e3e3e3;
+}
+.frame .content .monitor-container .mef-container .mef-data-label {
+ color: purple;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ vertical-align: middle;
+}
+.frame .content .monitor-container .mef-container .mef-label-col {
+ color: purple;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ flex: 0 0 auto;
+ order: 1;
+ padding-left: 10px;
+}
+.frame .content .monitor-container .mef-container .evc-label-col {
+ color: purple;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ flex: 0 0 auto;
+ order: 1;
+ padding-left: 10px;
+ width: 200px;
+}
+.frame .content .monitor-container .mef-container .uni-label-col {
+ color: purple;
+ font-size: small;
+ font-weight: normal;
+ text-align: left;
+ flex: 0 0 auto;
+ order: 1;
+ padding-left: 10px;
+ width: 100px;
+}
+.frame .content .monitor-container .mef-container .mef-info-container {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+}
+.frame .content .monitor-container .mef-container .mef-info-container .mef-secondary-container {
+ border: 1px solid darkgray;
+ background-color: WhiteSmoke;
+ margin: 5px;
+}
+.frame .content .monitor-container .mef-container .mef-info-container .uni1-container {
+ border: 1px solid darkgray;
+ background-color: WhiteSmoke;
+ margin: 5px;
+ border: 2px solid SlateGray;
+ flex: 1 1 24%;
+ order: 1;
+}
+.frame .content .monitor-container .mef-container .mef-info-container .evc-container {
+ border: 1px solid darkgray;
+ background-color: WhiteSmoke;
+ margin: 5px;
+ border: 2px solid SlateGray;
+ flex: 1 1 38%;
+ order: 2;
+}
+.frame .content .monitor-container .mef-container .mef-info-container .uni2-container {
+ border: 1px solid darkgray;
+ background-color: WhiteSmoke;
+ margin: 5px;
+ border: 2px solid SlateGray;
+ flex: 1 1 24%;
+ order: 3;
+}
diff --git a/demo-ui/app/views/epl-panel.html b/demo-ui/app/views/epl-panel.html
new file mode 100644
index 0000000..3de2523
--- /dev/null
+++ b/demo-ui/app/views/epl-panel.html
@@ -0,0 +1,187 @@
+<!--
+
+To Do
+ construct default name based on UNIs and COS
+ display EVC params when clicked
+ changed currently hardcoded widths (150) of input fields/dd's to be set by a class
+ update config file based on watch (already have hit issue), rather than wait
+ update available cos list when new cos added (low priority)
+
+ -->
+
+<!-- List of existing EPLs -->
+
+<div class="primary-lable ">Ethernet Private Line Services</div>
+<div class="epl-list">
+ <div class="list-item choosable"
+ ng-repeat="epl in eplList track by $index"
+
+ ng-class="{'selected-item-idle' : $index === selectedEplIdx &&
+ !eplActionInProgress(),
+ 'selected-item-update' : $index === selectedEplIdx &&
+ eplUpdateInProgress(),
+ 'selected-item-delete' : $index === selectedEplIdx &&
+ eplDelInProgress() }"
+ ng-click="onEplClick($index)">
+ {{ epl.id }}
+ </div>
+</div>
+
+
+<!-- Action Icons (add/mod/del) -->
+
+<div class = "action-icon-container">
+
+ <span class="action-icon glyphicon glyphicon-plus"
+ ng-click="onAddEpl()"
+ ng-class="{'action' : eplAddInProgress()}">
+ </span>
+
+ <span class="action-icon glyphicon glyphicon-pencil"
+ ng-click="onUpdateEpl()"
+ ng-class="{'action' : eplUpdateInProgress()}">
+ </span>
+
+ <span class="action-icon glyphicon glyphicon-minus"
+ ng-click="onDelEpl()"
+ ng-class="{'action' : eplDelInProgress()}">
+ </span>
+</div>
+
+<!-- Data for EPL selected / being acted on -->
+
+<div class="epl-info-contaier">
+ <form role="form" ng-submit="onEplInputSubmit()">
+
+ <!-- Name -->
+
+ <div class="data-row">
+ <div class="label-col">Name:</div>
+ <div class="data-col" ng-show="showEplName()">
+ <span class="data-item">
+ {{ eplList[selectedEplIdx].id }}
+ </span>
+ </div>
+
+ <div class="data-col" ng-show="showEplNameInput()">
+ <input class="data-input" name="id"
+ style="width:150"
+ type="text" ng-required="showEplInputs()"
+ ng-model="eplToEdit.id">
+ </div>
+ </div>
+
+ <!-- Class of servce -->
+
+ <div class="data-row">
+ <div class="label-col">Service level</div>
+ <div class="data-col" ng-show="showEplValues()">
+ <span class="data-item">
+ {{ eplList[selectedEplIdx].cos }}</span>
+ </div>
+ <div class="data-col" ng-show="showEplInputs()">
+ <select class="dd-input"
+ style="width:150"
+ name="cos Id"
+ ng-required="showEplInputs()"
+ ng-model="eplToEdit.cos"
+ >
+ <option ng-repeat="cos in availableCosIds"
+ value="{{ cos }}"
+ ng-selected="{{cos == eplToEdit.cos}}"
+ >
+ {{ cos }}
+ </option>
+ </select>
+ </div>
+ </div>
+
+
+ <!-- UNI 1 -->
+
+ <div class="data-row">
+ <div class="label-col">UNI 1</div>
+ <div class="data-col" ng-show="showEplValues()">
+ <span class="data-item">
+ {{ eplList[selectedEplIdx].uniHostIpList[0] }}&nbsp
+ ( {{ eplList[selectedEplIdx].custAddressList[0] }} )
+ </span>
+ </div>
+
+ <div class="data-col" ng-show="showEplInputs()">
+ <select class="dd-input"
+ style="width:150"
+ name="uni 1"
+ ng-required="showEplInputs()"
+ ng-model="eplToEdit.uni1Selected"
+ ng-options="uni.ip + ' ( ' + uni.address + ' )' for uni in availableUnis"
+ >
+ </option>
+ </select>
+ </div>
+ </div>
+
+
+ <!-- UNI 2 -->
+
+ <div class="data-row">
+ <div class="label-col">UNI 2</div>
+ <div class="data-col" ng-show="showEplValues()">
+ <span class="data-item">
+ {{ eplList[selectedEplIdx].uniHostIpList[1] }}&nbsp
+ ( {{ eplList[selectedEplIdx].custAddressList[1] }} )
+ </span>
+ </div>
+
+ <div class="data-col" ng-show="showEplInputs()">
+ <select class="dd-input"
+ style="width:150"
+ name="uni 1"
+ ng-required="showEplInputs()"
+ ng-model="eplToEdit.uni2Selected"
+ ng-options="uni.ip + ' ( ' + uni.address + ' )' for uni in availableUnis"
+ >
+ </option>
+ </select>
+ </div>
+ </div>
+
+
+ <!-- Evc -->
+
+ <div class="data-row" >
+ <div class="label-col" ng-hide="eplAddInProgress()">Assoc Evc:</div>
+ <div class="data-col">
+ <span class="data-item" ng-hide="eplAddInProgress()">
+ {{ eplList[selectedEplIdx].evcId }}
+ </span>
+ </div>
+ </div>
+
+ <!-- Action Buttons -->
+
+ <div class="warning-container">
+ &nbsp<span class="warning" ng-show="eplNameConflict()">[Please Enter Unique Name]&nbsp&nbsp&nbsp</span>
+ <span class="warning" ng-show="uniConflict()">[UNI-1 and UNI-2 must be different]&nbsp&nbsp&nbsp</span>
+ <span class="warning" ng-show="cosConflict()">[Must create SL before creating EPL]&nbsp&nbsp&nbsp</span>
+ </div>
+ <div class="button-container">
+ <button type="submit"
+ class="btn btn-sm my-btn-addon"
+ <div ng-class="{'btn-success' : eplAddInProgress(),
+ 'btn-warning' : eplUpdateInProgress(),
+ 'btn-danger' : eplDelInProgress()}"
+ ng-disabled="!activateEplActionButton()"
+ ng-show="showEplActionButton()")>
+ {{ eplActionButtonText }}</button>
+ </div>
+
+ <!-- Debug -->
+
+<!--
+ <div>
+ </div>
+ -->
+
+ </form>
+</div>
diff --git a/demo-ui/app/views/less/vcpe.less b/demo-ui/app/views/less/vcpe.less
new file mode 100644
index 0000000..5598160
--- /dev/null
+++ b/demo-ui/app/views/less/vcpe.less
@@ -0,0 +1,365 @@
+@dbg-flg: false;
+
+@side-panel-width: 5%;
+@main-panel-width: 100%-(2*@side-panel-width);
+
+@backgroundGray: #E3E3E3;
+
+@cos-basis: 50%;
+@epl-basis: 100%-(@cos-basis);
+
+
+//
+// Utilities
+//
+
+.dbg-border( // show border if @dbg-flg is true
+ @dbg-bordersFlg,
+ @dbg-borderClr,
+ @dbg-borderStyle)
+ when (@dbg-bordersFlg=true) {
+ border: 3px @dbg-borderStyle @dbg-borderClr;
+ margin: 1px; padding: 1px;
+}
+
+.set-text(@color,
+ @font-size,
+ @font-weight,
+ @text-align) {
+ color:@color; font-size:@font-size;
+ font-weight:@font-weight; text-align:@text-align;
+}
+
+//
+// Common Styles
+//
+
+input[type='number'] { font-size: 12px; }
+
+.choosable {
+ cursor: pointer;
+}
+
+.gradient {
+ background: -webkit-linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
+}
+
+.my-btn-addon {
+ height: 30px;
+ width: 100px;
+}
+
+.primary-lable {
+ .set-text(darkred, large, bold, center); }
+
+.list-item {
+ .set-text(black, small, normal, left);
+ background-color:none; }
+
+.selected-item-idle {
+ background-color: @backgroundGray; }
+
+.selected-item-delete {
+ background-color: #FFE4E1; }
+
+.selected-item-update {
+ background-color: #FFEFD5; }
+
+.primary-container {
+ border: 3px solid black;
+ .dbg-border(@dbg-flg, blue, solid);
+ background-color: @backgroundGray;
+ border-radius: 15px;
+ padding: 10px;
+ height: 400px;
+}
+
+.secondary-container {
+ border: 1px solid darkgray;
+ .dbg-border(@dbg-flg, green, solid);
+ background-color: white;
+ padding: 5px 10px; // TB RL
+ margin: 10px 10px -1px 10px; // T R B L
+ height: 125px;
+ overflow-y: scroll;
+}
+
+.action-icon-container {
+ border: 1px solid darkgray;
+ .dbg-border(@dbg-flg, magenta, solid);
+ .gradient;
+ margin: 0px 10px; // TB RL
+ padding: 2px 10px; // TB RL
+}
+
+.action-icon {
+ .choosable;
+ padding: 0px 1px; // TB RL
+ font-size: small;
+ color: black;
+ padding: 2px;
+}
+
+.action {
+ color: blue !important;
+}
+
+.warning {
+ color: red !important;
+ font-size: small !important;
+}
+
+.data-row {
+ .dbg-border(@dbg-flg, red, solid);
+ display: flex;
+ flex-flow:row nowrap;
+ height: 20px;
+ width: 300;
+ }
+.label-col {
+ .dbg-border(@dbg-flg, gray, solid);
+ .set-text(purple, small, normal, left);
+ flex: 0 0 auto; order: 1;
+ width: 85px;
+ }
+
+
+.data-col {
+ .dbg-border(@dbg-flg, gray, solid);
+ flex: 0 0 auto; order: 2;
+ .set-text(black, small, normal, left);
+
+ }
+
+.data-label {
+ .set-text(purple, small, normal, left);
+ vertical-align: middle;
+}
+.data-item {
+ .set-text(black, small, bold, left);
+}
+.data-input {
+ height: 20px;
+ width: 70px;
+ border-radius: 5px;
+}
+.cos-dd-input {
+ height: 20px;
+ width: 70px;
+}
+
+.epl-dd-input {
+ height: 20px;
+ width: 150px;
+}
+
+
+.data-unit {
+ .set-text(black, small, normal, left);
+ margin-left: 5px;
+ vertical-align: middle;
+}
+
+//
+// Page starts here
+//
+
+.header-container {
+ display: flex;
+ flex-flow:row nowrap;
+ justify-content: space-between;
+ .hdr {
+ flex: 1 1 auto; order: 1;
+ }
+ .img {
+ flex: 1 1 auto; order: 2;
+ align-self: flex-end;
+ margin-bottom: 12px;
+ }
+}
+
+
+.frame{
+ .dbg-border(@dbg-flg, lightgray, solid);
+ display: flex;
+ flex-flow:row nowrap;
+ border: 1px solid lightgray;
+
+ margin: 20px; // TBRL
+ padding: 20px 0px; // TB RL
+
+ .left { .dbg-border(@dbg-flg, lightgray, solid);
+ flex: 1 6 @side-panel-width; order: 1; }
+
+ .right{ .dbg-border(@dbg-flg, lightgray, solid);
+ flex: 1 6 @side-panel-width; order: 3; }
+
+ .content {
+ border: none;
+ .dbg-border(@dbg-flg, lightgray, solid);
+ flex: 1 6 @main-panel-width; order: 2;
+
+ .action-container {
+ border: none;
+ .dbg-border(@dbg-flg, yellow, solid);
+
+ display: flex;
+ flex-flow:row nowrap;
+
+ .cos-container {
+ .primary-container;
+ flex: 1 1 @cos-basis; order: 1;
+ margin-right: 20px;
+
+ .cos-list {
+ .secondary-container;
+ }
+ .cos-info-contaier {
+ .dbg-border(@dbg-flg, green, solid);
+ display: flex;
+ flex-flow:column nowrap;
+ margin: 10px 20px; // TB RL
+ .labelContainer {
+ .dbg-border(@dbg-flg, yellow, solid);
+ flex: 1 1 33%; order: 1;
+ min-width: 80px;
+ .data-label;
+ }
+ }
+ .warning-container {
+ .dbg-border(@dbg-flg, magenta, solid);
+ height: 15px;
+ margin-top: 10px;
+ }
+ .button-container {
+ .dbg-border(@dbg-flg, magenta, solid);
+ }
+ }
+ .epl-container {
+ .primary-container;
+ flex: 1 1 @epl-basis; order: 2;
+ margin-right: 20px;
+
+ .epl-list {
+ .secondary-container;
+ }
+ .epl-info-contaier {
+ .dbg-border(@dbg-flg, green, solid);
+ display: flex;
+ flex-flow:column nowrap;
+ margin: 10px 20px; // TB RL
+ .labelContainer {
+ .dbg-border(@dbg-flg, yellow, solid);
+ flex: 1 1 33%; order: 1;
+ min-width: 80px;
+ .data-label;
+ }
+ }
+ .warning-container {
+ .dbg-border(@dbg-flg, magenta, solid);
+ height: 15px;
+ margin-top: 10px;
+ }
+ .button-container {
+ .dbg-border(@dbg-flg, magenta, solid);
+ }
+ }
+ }
+
+ .monitor-container {
+ .dbg-border(@dbg-flg, yellow, solid);
+
+
+ .mef-container {
+ .primary-container;
+ height: 450px;
+
+ .dbg-border(@dbg-flg, blue, solid);
+ background-color: #B0C4DE;
+ margin: 20px 20px 20px 0px; // T R B L
+
+ .primary-mef-lable {
+ .set-text(SteelBlue, large, bold, center);
+ margin-bottom: 10px;
+ }
+
+ @mef-container-border-color: SlateGray;
+
+ .mef-panel-hdr {
+ border-bottom: 1px solid @mef-container-border-color;
+ .dbg-border(@dbg-flg, blue, solid);
+ }
+
+ .secondary-mef-label {
+ .set-text(SlateGray, small, bold, center);
+ background-color: @backgroundGray;
+ }
+
+ .mef-data-label {
+ .set-text(purple, small, normal, left);
+ vertical-align: middle;
+
+ }
+ .mef-label-col {
+ .dbg-border(@dbg-flg, gray, solid);
+ .set-text(purple, small, normal, left);
+ flex: 0 0 auto; order: 1;
+ padding-left: 10px;
+ }
+
+ .evc-label-col {
+ .mef-label-col;
+ width: 200px;
+
+ }
+ .uni-label-col {
+ .mef-label-col;
+ width: 100px;
+ }
+
+ .mef-info-container {
+ display: flex;
+ flex-flow:row nowrap;
+ justify-content: center;
+
+ .mef-secondary-container {
+ border: 1px solid darkgray;
+ .dbg-border(@dbg-flg, green, solid);
+ background-color: WhiteSmoke;
+ //padding: 5px 30px; // TB RL
+ margin: 5px;
+ }
+
+ @evc-basis: 38%;
+ @uni-basis: 100%-(2*@evc-basis);
+
+ .uni1-container {
+ .mef-secondary-container;
+ border: 2px solid @mef-container-border-color;
+ .dbg-border(@dbg-flg, darkred, solid);
+ flex: 1 1 @uni-basis; order: 1;
+
+ }
+ .evc-container {
+ .mef-secondary-container;
+ border: 2px solid @mef-container-border-color;
+ .dbg-border(@dbg-flg, darkred, solid);
+ flex: 1 1 @evc-basis; order: 2;
+
+ }
+ .uni2-container {
+ .mef-secondary-container;
+ border: 2px solid @mef-container-border-color;
+ .dbg-border(@dbg-flg, darkred, solid);
+ flex: 1 1 @uni-basis; order: 3;
+
+ }
+ }
+ }
+ }
+ }
+}
+
+
+
diff --git a/demo-ui/app/views/mef-panel.html b/demo-ui/app/views/mef-panel.html
new file mode 100644
index 0000000..df9f60b
--- /dev/null
+++ b/demo-ui/app/views/mef-panel.html
@@ -0,0 +1,370 @@
+<!--
+
+To Do
+ Use cases to handle properly RE EVC/UNI displaying info
+ - EPL exist at start up
+ - no EPL at start up
+ - delete an EPL
+ - delete last EPL
+ -->
+
+<div class="primary-mef-lable">MEF EPL Data</div>
+<div class="mef-info-container">
+
+
+ <!-- UNI 1 -->
+
+ <div class="uni1-container">
+ <div class="mef-panel-hdr">
+ <div class="secondary-mef-label">UNI-1 MEF Attributes</div>
+ <div class="secondary-mef-label"
+ style="font-size: xx-small; font-weight: normal; color: #b3b3b3;">
+ (Source = ODL UNI Mgr DB)
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Uni ID:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[0].uni[0]["id"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">IP Address:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[0].uni[0]["ip-address"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">MAC Address:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[0].uni[0]["mac-address"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Speed:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ uniToSpeedString(currentEvcUnis[0]) }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Mac Layer:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[0].uni[0]["mac-layer"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Phys Medium:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[0].uni[0]["physical-medium"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">MTU Size:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[0].uni[0]["mtu-size"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Mode:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[0].uni[0]["mode"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Type:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[0].uni[0]["type"] }}
+ </span>
+ </div>
+ </div>
+
+ </div>
+
+ <!-- EVC -->
+
+ <div class="evc-container">
+
+ <div class="mef-panel-hdr">
+ <div class="secondary-mef-label">EVC MEF Attributes</div>
+ <div class="secondary-mef-label"
+ style="font-size: xx-small; font-weight: normal; color: #b3b3b3;">
+ (Source = EVC Mgr DB)</div>
+ </div>
+ <div class="data-row">
+ <div class="evc-label-col">Evc ID:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.id }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">CoS ID:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.cosId }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">Uni-1/2 IDs:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+
+ <span class="data-item">
+ {{ "[" + currentEplEvc.uniIdList[0] + "] | [" + currentEplEvc.uniIdList[1] + "]"}}
+ </span>
+
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">Uni-1/2 IPs:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.uniIpList[0] }}
+ </span>
+ </div>
+ </div>
+ <div class="data-row">
+ <div class="evc-label-col"></div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.uniIpList[1] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">Uni-1/2 MACs:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.uniMacList[0] }}
+ </span>
+ </div>
+ </div>
+ <div class="data-row">
+ <div class="evc-label-col"></div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.uniMacList[1] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">1-Way Availability:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.oneWayAvailability }}
+ </span>
+ <span class="data-unit">%</span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">1-Way Frame Delay:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.oneWayFrameDelay }}
+ </span>
+ <span class="data-unit">ms</span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">1-Way Frame Loss Ratio:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.oneWayFrameLossRatio }}
+ </span>
+ <span class="data-unit">%</span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">EVC Type:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.evcType }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">Unicast Frame Delivery:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.unicastFrameDelivery }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">Broadcast Frame Delivery:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.broadcastFrameDelivery }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">Multicast Frame Delivery:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.multicastFrameDelivery }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">CE VLAN ID Preservation:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.ceVLanIdPreservation }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">CE VLAN CoS Preservation:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.ceVlanCosPreservation }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="evc-label-col">Max Service Frame Size:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEplEvc.evcMaxSvcFrameSize }}
+ </span>
+ <span class="data-unit">bytes</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- UNI 2 -->
+
+ <div class="uni2-container">
+ <div class="mef-panel-hdr">
+ <div class="secondary-mef-label">UNI-2 MEF Attributes</div>
+ <div class="secondary-mef-label"
+ style="font-size: xx-small; font-weight: normal; color: #b3b3b3;">
+ (Source = ODL UNI Mgr DB)
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Uni ID:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[1].uni[0]["id"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">IP Address:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[1].uni[0]["ip-address"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">MAC Address:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[1].uni[0]["mac-address"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Speed:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ uniToSpeedString(currentEvcUnis[1]) }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Mac Layer:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[1].uni[0]["mac-layer"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Phys Medium:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[1].uni[0]["physical-medium"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">MTU Size:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[0].uni[0]["mtu-size"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Mode:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[1].uni[0]["mode"] }}
+ </span>
+ </div>
+ </div>
+
+ <div class="data-row">
+ <div class="uni-label-col">Type:</div>
+ <div class="data-col" ng-show="showEvcValues()">
+ <span class="data-item">
+ {{ currentEvcUnis[1].uni[0]["type"] }}
+ </span>
+ </div>
+ </div>
+
+ </div>
+</div> \ No newline at end of file
diff --git a/demo-ui/app/views/vcpe-portal.html b/demo-ui/app/views/vcpe-portal.html
new file mode 100644
index 0000000..437485f
--- /dev/null
+++ b/demo-ui/app/views/vcpe-portal.html
@@ -0,0 +1,71 @@
+<!--
+
+TODO
+ - Use watchers to trigger changes aftger config file reads as opposed
+ to the current "sleep for a bit" approach
+
+ -->
+
+<html ng-app="vcpe">
+<head ng-controller="MainController">
+
+ <script src="../../bower_components/angular/angular.min.js"></script>
+
+ <link rel="stylesheet" href="../../bower_components/bootstrap/dist/css/bootstrap.min.css">
+ <link rel="stylesheet" href="../../bower_components/bootstrap/dist/css/bootstrap-theme.min.css">
+
+ <link rel="stylesheet" href="css/vcpe.css" />
+ <script src="../controllers/MainController.js"></script>
+ <script src="../controllers/CosController.js"></script>
+ <script src="../controllers/EplController.js"></script>
+ <script src="../controllers/MefController.js"></script>
+ <script src="../services/cosServices.js"></script>
+ <script src="../services/eplServices.js"></script>
+ <script src="../services/mefServices.js"></script>
+ <script src="../services/model.js"></script>
+ <script src="../services/dbg.js"></script>
+
+</head>
+
+<body>
+<div class="frame">
+<div class="left"></div>
+
+<div class="content">
+
+ <div class="header-container">
+ <div class="hdr">
+ <h2>Virtual Business CPE Demo</h2>
+ </div>
+ </div>
+
+ <div class="action-container">
+
+ <div class="cos-container"
+ ng-controller="CosController"
+ ng-include="'cos-panel.html'">
+ </div>
+
+ <div class="epl-container"
+ ng-controller="EplController"
+ ng-include="'epl-panel.html'">
+ </div>
+
+ </div>
+
+ <div class="monitor-container">
+
+ <div class="mef-container"
+ ng-controller="MefController"
+ ng-include="'mef-panel.html'">
+ </div>
+
+
+ </div>
+
+</div>
+
+<div class="right"></div>
+</div>
+</body>
+</html>
diff --git a/demo-ui/bower.json b/demo-ui/bower.json
new file mode 100644
index 0000000..aaac185
--- /dev/null
+++ b/demo-ui/bower.json
@@ -0,0 +1,11 @@
+{
+ "name": "dhd",
+ "version": "0.0.1",
+ "dependencies": {
+ "angular": "1.4.1",
+ "bootstrap": "3.3.5"
+ },
+ "resolutions": {
+ "angular": "1.4.1"
+ }
+}
diff --git a/demo-ui/gruntfile.js b/demo-ui/gruntfile.js
new file mode 100644
index 0000000..7e8bc68
--- /dev/null
+++ b/demo-ui/gruntfile.js
@@ -0,0 +1,25 @@
+module.exports = function (grunt) {
+
+ grunt.initConfig({
+ less: {
+ app: {
+ files: {
+ 'app/views/css/vcpe.css': 'app/views/less/vcpe.less'
+ }
+ }
+ },
+
+ watch: {
+ less: {
+ files: 'app/views/less/*.less',
+ tasks: ['css']
+ }
+ }
+ });
+
+ grunt.loadNpmTasks('grunt-contrib-less');
+ grunt.loadNpmTasks('grunt-contrib-watch');
+ grunt.registerTask('css', ['less']);
+ grunt.registerTask('default', ['css']);
+
+};
diff --git a/demo-ui/package.json b/demo-ui/package.json
new file mode 100644
index 0000000..086cadb
--- /dev/null
+++ b/demo-ui/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "vcpe-demo-portal",
+ "version": "0.0.1",
+ "description": "Portal that drives VCPE MEF demo",
+ "main": "src/portal.js",
+ "author": "steve@sentosatech.com",
+ "dependencies": {
+ "less": "latest",
+ "express": "latest",
+ "body-parser": "*"
+ },
+ "devDependencies": {
+ "grunt": "^0.4.5",
+ "grunt-contrib-copy": "^0.8.0",
+ "grunt-contrib-less": "^1.0.1",
+ "grunt-contrib-watch": "^0.6.1"
+ }
+}
diff --git a/demo-ui/vcpeUiServer.js b/demo-ui/vcpeUiServer.js
new file mode 100644
index 0000000..94c5b9c
--- /dev/null
+++ b/demo-ui/vcpeUiServer.js
@@ -0,0 +1,19 @@
+// Create express HTTP server app
+var express = require('express');
+var app = express();
+
+var bodyParser = require('body-parser');
+app.use(bodyParser.json());
+app.use(express.static("."));
+
+// defaults
+var PORT = 4000;
+
+// grap the command line arguments
+var args = process.argv.slice(2);
+if ( args[0] ) {
+ PORT = args[0];
+}
+
+app.listen(PORT);
+console.log('Running on http://localhost:' + PORT); \ No newline at end of file