Displaying service coarse graph
Change-Id: I88e6c83421d0f9fee1f34595fb640e4987e93514
diff --git a/views/ngXosViews/mcordTopology/env/default.js b/views/ngXosViews/mcordTopology/env/default.js
deleted file mode 100644
index acc8b5a..0000000
--- a/views/ngXosViews/mcordTopology/env/default.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// This is a default configuration for your development environment.
-// You can duplicate this configuration for any of your Backend Environments.
-// Different configurations are loaded setting a NODE_ENV variable that contain the config file name.
-// `NODE_ENV=local npm start`
-//
-// If xoscsrftoken or xossessionid are not specified the browser value are used
-// (works only for local environment as both application are served on the same domain)
-
-module.exports = {
- host: 'http://xos.dev:9999/',
- xoscsrftoken: 's1hmvk8d66UwxnRlDze64FrswXBeHjRD',
- xossessionid: '4qgqwp46w5ln4q24vitc8f5gx6i66wyk'
-};
diff --git a/views/ngXosViews/mcordTopology/env/mock.js b/views/ngXosViews/mcordTopology/env/mock.js
deleted file mode 100644
index 610ad78..0000000
--- a/views/ngXosViews/mcordTopology/env/mock.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- host: 'http://localhost:4000',
- xoscsrftoken: 'Pkq9PqoAsaMvrEiFAgxfw47IxTOtd0Y5',
- xossessionid: 'qa1t49qeecdehofjkndqvxik71iwzfvf'
-};
\ No newline at end of file
diff --git a/views/ngXosViews/serviceGrid/bower.json b/views/ngXosViews/serviceGrid/bower.json
index 18bd401..941c8db 100644
--- a/views/ngXosViews/serviceGrid/bower.json
+++ b/views/ngXosViews/serviceGrid/bower.json
@@ -17,7 +17,8 @@
"dependencies": {
"js-yaml": "~3.6.1",
"jszip": "Stuk/jszip#~3.0.0",
- "file-saver": "eligrey/FileSaver.js#~1.3.2"
+ "file-saver": "eligrey/FileSaver.js#~1.3.2",
+ "graphlib": "~2.1.0"
},
"devDependencies": {
"jquery": "2.1.4",
@@ -32,5 +33,8 @@
"angular-chart.js": "~0.10.2",
"d3": "~3.5.17",
"angular-recursion": "~1.0.5"
+ },
+ "resolutions": {
+ "lodash": "~4.11.1"
}
}
diff --git a/views/ngXosViews/serviceGrid/src/css/main.css b/views/ngXosViews/serviceGrid/src/css/main.css
index ae13614..c36e56d 100644
--- a/views/ngXosViews/serviceGrid/src/css/main.css
+++ b/views/ngXosViews/serviceGrid/src/css/main.css
@@ -1,3 +1,37 @@
+xos-side-panel .xos-side-panel-content {
+ position: fixed;
+ width: 30%;
+ height: 100%;
+ padding: 25px;
+ background: #fff;
+ overflow: scroll; }
+ xos-side-panel .xos-side-panel-content.right {
+ border-left: 1px solid #555555;
+ top: 0;
+ right: 0;
+ visibility: hidden;
+ box-shadow: -10px 0px 20px -8px rgba(0, 0, 0, 0.75); }
+ xos-side-panel .xos-side-panel-content.right.out {
+ animation: 0.5s slideOutRight ease-in-out;
+ visibility: visible; }
+ xos-side-panel .xos-side-panel-content.right.in {
+ animation: 0.5s slideInRight ease-in-out;
+ visibility: visible; }
+
+@keyframes slideInRight {
+ from {
+ transform: translate3d(100%, 0, 0);
+ visibility: visible; }
+ to {
+ transform: translate3d(0, 0, 0); } }
+
+@keyframes slideOutRight {
+ from {
+ transform: translate3d(0, 0, 0); }
+ to {
+ visibility: hidden;
+ transform: translate3d(100%, 0, 0); } }
+
#xosServiceGrid service-graph {
display: block;
width: 100%;
diff --git a/views/ngXosViews/serviceGrid/src/css/side-panel.css b/views/ngXosViews/serviceGrid/src/css/side-panel.css
new file mode 100644
index 0000000..3cde922
--- /dev/null
+++ b/views/ngXosViews/serviceGrid/src/css/side-panel.css
@@ -0,0 +1,33 @@
+xos-side-panel .xos-side-panel-content {
+ position: fixed;
+ width: 30%;
+ height: 100%;
+ padding: 25px;
+ background: #fff;
+ overflow: scroll; }
+ xos-side-panel .xos-side-panel-content.right {
+ border-left: 1px solid #555555;
+ top: 0;
+ right: 0;
+ visibility: hidden;
+ box-shadow: -10px 0px 20px -8px rgba(0, 0, 0, 0.75); }
+ xos-side-panel .xos-side-panel-content.right.out {
+ animation: 0.5s slideOutRight ease-in-out;
+ visibility: visible; }
+ xos-side-panel .xos-side-panel-content.right.in {
+ animation: 0.5s slideInRight ease-in-out;
+ visibility: visible; }
+
+@keyframes slideInRight {
+ from {
+ transform: translate3d(100%, 0, 0);
+ visibility: visible; }
+ to {
+ transform: translate3d(0, 0, 0); } }
+
+@keyframes slideOutRight {
+ from {
+ transform: translate3d(0, 0, 0); }
+ to {
+ visibility: hidden;
+ transform: translate3d(100%, 0, 0); } }
diff --git a/views/ngXosViews/serviceGrid/src/index.html b/views/ngXosViews/serviceGrid/src/index.html
index 8f95e59..0e3fa40 100644
--- a/views/ngXosViews/serviceGrid/src/index.html
+++ b/views/ngXosViews/serviceGrid/src/index.html
@@ -6,6 +6,7 @@
<!-- endcss -->
<!-- inject:css -->
<link rel="stylesheet" href="/css/main.css">
+<link rel="stylesheet" href="/css/side-panel.css">
<link rel="stylesheet" href="/../../../xos/core/static/xosNgLib.css">
<!-- endinject -->
@@ -17,6 +18,8 @@
<script src="vendor/js-yaml/dist/js-yaml.js"></script>
<script src="vendor/jszip/dist/jszip.js"></script>
<script src="vendor/file-saver/FileSaver.js"></script>
+<script src="vendor/lodash/lodash.js"></script>
+<script src="vendor/graphlib/dist/graphlib.core.js"></script>
<script src="vendor/jquery/dist/jquery.js"></script>
<script src="vendor/angular/angular.js"></script>
<script src="vendor/angular-mocks/angular-mocks.js"></script>
@@ -24,7 +27,6 @@
<script src="vendor/angular-cookies/angular-cookies.js"></script>
<script src="vendor/angular-animate/angular-animate.js"></script>
<script src="vendor/angular-resource/angular-resource.js"></script>
-<script src="vendor/lodash/lodash.js"></script>
<script src="vendor/bootstrap-css/js/bootstrap.min.js"></script>
<script src="vendor/Chart.js/Chart.js"></script>
<script src="vendor/angular-chart.js/dist/angular-chart.js"></script>
@@ -38,6 +40,7 @@
<script src="/.tmp/tosca_encoder.service.js"></script>
<script src="/.tmp/slices_encorder.service.js"></script>
<script src="/.tmp/site_encode.service.js"></script>
+<script src="/.tmp/sidePanel.component.js"></script>
<script src="/.tmp/service_encorder.service.js"></script>
<script src="/.tmp/service-graph.js"></script>
<script src="/.tmp/networks_encoder.service.js"></script>
diff --git a/views/ngXosViews/serviceGrid/src/js/service-graph.js b/views/ngXosViews/serviceGrid/src/js/service-graph.js
index 51a711a..904466c 100644
--- a/views/ngXosViews/serviceGrid/src/js/service-graph.js
+++ b/views/ngXosViews/serviceGrid/src/js/service-graph.js
@@ -2,6 +2,64 @@
'use strict';
angular.module('xos.serviceGrid')
+ .service('Graph', function($q, Tenants, Services, Subscribers){
+
+ let tenancyGraph = new graphlib.Graph();
+ let cached = false;
+
+ const buildGraph = () => {
+
+ let deferred = $q.defer();
+
+ $q.all([
+ Tenants.query().$promise,
+ Services.query().$promise,
+ Subscribers.query().$promise
+ ])
+ .then((res) => {
+ let [tenants, services, subscribers] = res;
+ // adding service nodes
+ services.forEach(s => tenancyGraph.setNode(s.id, angular.extend(s, {type: 'service'})));
+
+
+ // coarse tenant
+ tenants.filter(t => t.subscriber_service && t.provider_service)
+ .forEach(t => tenancyGraph.setEdge(t.subscriber_service, t.provider_service, t, t.name));
+
+ // fine grain tenant
+ // adding subscribers as nodes (to build fine grain graph)
+ // subscribers.forEach(s => tenancyGraph.setNode(`sub-${s.id}`, angular.extend(s, {type: 'subscriber'})));
+ // TODO
+ // - Find tenant that start from a subscriber
+ // - Follow the chain: from the first tenant follow where subscriber_tenant = tenant_id untill we cannot find any more tenant
+ // tenants.filter(t => t.subscriber_root && t.provider_service)
+ // .forEach(t => tenancyGraph.setEdge(`sub-${t.subscriber_root}`, t.provider_service, t, t.name));
+
+ deferred.resolve(tenancyGraph);
+ });
+
+ return deferred.promise;
+ };
+
+ this.getGraph = () => {
+ let deferred = $q.defer();
+
+ if(cached){
+ deferred.resolve(tenancyGraph);
+ }
+ else {
+ buildGraph()
+ .then((res) => {
+ cached = true;
+ deferred.resolve(res);
+ })
+ .catch(console.log);
+ }
+
+ return {$promise: deferred.promise};
+ };
+
+ })
.directive('serviceGraph', function(){
return {
restrict: 'E',
@@ -9,54 +67,61 @@
bindToController: true,
controllerAs: 'vm',
templateUrl: 'templates/service-graph.tpl.html',
- controller: function($element, GraphService){
+ controller: function($scope, $element, GraphService, Graph, ToscaEncoder){
let svg;
let el = $element[0];
let node;
let link;
+ const _this = this;
+ this.panelConfig = {
+ position: 'right'
+ };
+
+ // animate node and links in the correct place
const tick = (e) => {
- // Push different nodes in different directions for clustering.
-
node
- // .attr('cx', d => d.x)
- // .attr('cy', d => d.y)
- .attr({
- transform: d => `translate(${d.x}, ${d.y})`
- });
+ .attr('cx', d => d.x)
+ .attr('cy', d => d.y)
+ .attr({
+ transform: d => `translate(${d.x}, ${d.y})`
+ });
- link.attr('x1', d => d.source.x)
- .attr('y1', d => d.source.y)
- .attr('x2', d => d.target.x)
- .attr('y2', d => d.target.y);
- }
+ link
+ .attr('x1', d => d.source.x)
+ .attr('y1', d => d.source.y)
+ .attr('x2', d => getTargetNodeCircumferencePoint(d)[0])
+ .attr('y2', d => getTargetNodeCircumferencePoint(d)[1]);
+ };
- GraphService.loadCoarseData()
- .then((res) => {
+ Graph.getGraph().$promise
+ .then((graph) => {
// build links
- res.tenants = res.tenants.map(t => {
+ let links = graph.edges().map(e => {
return {
- source: t.provider_service,
- target: t.subscriber_service
+ source: graph.node(e.v),
+ target: graph.node(e.w)
}
});
+ let nodes = graph.nodes().map(n => graph.node(n));
- // add xos as a node
- res.services.push({
- name: 'XOS',
- class: 'xos',
- x: el.clientWidth / 2,
- y: el.clientHeight / 2,
- fixed: true
- })
+ //add xos as a node
+ nodes.push({
+ name: 'XOS',
+ type: 'xos',
+ x: el.clientWidth / 2,
+ y: el.clientHeight / 2,
+ fixed: true
+ });
handleSvg(el);
+ defineArrows();
var force = d3.layout.force()
- .nodes(res.services)
- .links(res.tenants)
+ .nodes(nodes)
+ .links(links)
.charge(-1060)
.gravity(0.1)
.linkDistance(200)
@@ -65,38 +130,66 @@
.start();
link = svg.selectAll('.link')
- .data(res.tenants).enter().insert('line')
- .attr('class', 'link');
+ .data(links).enter().insert('line')
+ .attr('class', 'link')
+ .attr('marker-end', 'url(#arrow)');
node = svg.selectAll('.node')
- .data(res.services)
+ .data(nodes)
.enter().append('g')
.call(force.drag)
- .on("mousedown", function() { d3.event.stopPropagation(); });
+ .on('mousedown', function(d) {
+ $scope.$apply(() => {
+ if(d.name === 'XOS'){
+ return;
+ }
+ _this.panelShow = true;
+ let status = parseInt(d.backend_status.match(/^[0-9]/)[0]);
+ console.log(status);
+ switch(status){
+ case 0:
+ d.icon = 'time';
+ break;
+ case 1:
+ d.icon = 'ok';
+ break;
+ case 2:
+ d.icon = 'remove';
+ break;
+ }
+ _this.selectedNode = d;
+ });
+ d3.event.stopPropagation();
+ });
node.append('circle')
.attr({
- class: d => `node ${d.class || ''}`,
+ class: d => `node ${d.type || ''}`,
r: 10
});
node.append('text')
.attr({
- 'text-anchor': 'middle'
+ 'text-anchor': 'middle',
+ 'alignment-baseline': 'middle'
})
- .text(d => d.name)
+ .text(d => d.humanReadableName || d.name);
+ // scale the node to fit the contained text
node.select('circle')
.attr({
r: function(d){
- let parent = d3.select(this).node().parentNode;
- let sib = d3.select(parent).select('text').node().getBBox()
- return (sib.width / 2) + 10
-
+ const parent = d3.select(this).node().parentNode;
+ const sib = d3.select(parent).select('text').node().getBBox();
+ const radius = (sib.width / 2) + 10;
+
+ // add radius as node attribute
+ d.nodeWidth = radius * 2;
+ return radius;
}
})
- })
+ });
const handleSvg = (el) => {
d3.select(el).select('svg').remove();
@@ -105,76 +198,39 @@
.append('svg')
.style('width', `${el.clientWidth}px`)
.style('height', `${el.clientHeight}px`);
+ };
+
+ const defineArrows = () => {
+ svg.append('svg:defs').selectAll('marker')
+ .data(['arrow']) // Different link/path types can be defined here
+ .enter().append('svg:marker') // This section adds in the arrows
+ .attr('id', String)
+ .attr('viewBox', '0 -5 10 10')
+ .attr('refX', 10)
+ .attr('refY', 0)
+ .attr('markerWidth', 6)
+ .attr('markerHeight', 6)
+ .attr('orient', 'auto')
+ .append('svg:path')
+ .attr('d', 'M0,-5L10,0L0,5');
+ };
+
+ const getTargetNodeCircumferencePoint = d => {
+ const radius = d.target.nodeWidth / 2; // nodeWidth is just a custom attribute I calculate during the creation of the nodes depending on the node width
+ const dx = d.target.x - d.source.x;
+ const dy = d.target.y - d.source.y;
+ const gamma = Math.atan2(dy, dx); // Math.atan2 returns the angle in the correct quadrant as opposed to Math.atan
+
+ const tx = d.target.x - (Math.cos(gamma) * radius);
+ const ty = d.target.y - (Math.sin(gamma) * radius);
+
+ return [tx, ty];
+ };
+
+ this.exportToTosca = (service) => {
+ ToscaEncoder.serviceToTosca(service);
}
}
};
})
-})();
-
-// Draw services around xos and calculate coarse tenant as links
-
-// var width = 960, height = 500;
-
-// var fill = d3.scale.category10();
-
-// var nodes = [
-// {id: 1},
-// {id: 2},
-// {id: 3},
-// {id: 4},
-// {id: 5}
-// ];
-
-// var links = [
-// {source: 1, target: 2},
-// {source: 2, target: 3}
-// ];
-
-// var svg = d3.select("body").append("svg")
-// .attr("width", width)
-// .attr("height", height);
-
-// var force = d3.layout.force()
-// .nodes(nodes)
-// .links(links)
-// .charge(-8*12)
-// .gravity(0.1)
-// .size([width, height])
-// .on("tick", tick)
-// .start();
-
-// svg.append('circle')
-// .attr({
-// "class": "xos",
-// r: 20,
-// cx: () => width / 2,
-// cy: () => height / 2,
-// })
-
-// var node = svg.selectAll(".node")
-// .data(nodes)
-// .enter().append("circle")
-// .attr("class", "node")
-// .attr("cx", ({ x }) => x)
-// .attr("cy", ({ y }) => y)
-// .attr("r", 8)
-// .style("fill", ({}, index) => fill(index & 3))
-// .style("stroke", ({}, index) => d3.rgb(fill(index & 3)).darker(2))
-// .call(force.drag)
-// .on("mousedown", ({}) => d3.event.stopPropagation());
-
-// var link = svg.selectAll(".link")
-// .data(links).enter().insert("line")
-// .attr("class", "link");
-
-// function tick(e) {
-// // Push different nodes in different directions for clustering.
-
-// node.attr("cx", function(d) { return d.x; })
-// .attr("cy", function(d) { return d.y; });
-
-// link.attr("x1", function(d) { return d.source.x; })
-// .attr("y1", function(d) { return d.source.y; })
-// .attr("x2", function(d) { return d.target.x; })
-// .attr("y2", function(d) { return d.target.y; });
-// }
+})();
\ No newline at end of file
diff --git a/views/ngXosViews/serviceGrid/src/js/sidePanel.component.js b/views/ngXosViews/serviceGrid/src/js/sidePanel.component.js
new file mode 100644
index 0000000..4421ee7
--- /dev/null
+++ b/views/ngXosViews/serviceGrid/src/js/sidePanel.component.js
@@ -0,0 +1,74 @@
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 7/18/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+ .directive('xosSidePanel', function(){
+ return {
+ restrict: 'E',
+ scope: {
+ config: '=',
+ show: '='
+ },
+ template: `
+ <div class="xos-side-panel-content {{vm.classes.join(' ')}}">
+ <div class="row">
+ <div class="col-xs-12">
+ <button type="button" class="close" ng-click="vm.dismiss()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-xs-12" ng-transclude></div>
+ </div>
+ </div>
+ `,
+ transclude: true,
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: function($scope, $timeout, _){
+ console.log(this.show);
+
+ this.classes = [];
+
+ this.classes.push(this.config.position);
+
+ this.dismiss = () => {
+ this.show = false;
+ this.classes = this.toggleClass(this.classes);
+ $timeout(() => {
+ return _.remove(this.classes, c => c === 'out');
+ }, 500);
+ };
+
+ this.toggleClass = (classes) => {
+ if(classes.indexOf('in') > -1){
+ _.remove(this.classes, c => c === 'in');
+ this.classes.push('out');
+ return classes;
+ }
+ _.remove(this.classes, c => c === 'out');
+ this.classes.push('in');
+ return classes;
+ };
+
+ $scope.$watch(() => this.show, val => {
+ if (angular.isDefined(val)){
+ if (val && val === true){
+ this.classes = this.toggleClass(this.classes);
+ }
+ }
+ })
+ }
+ }
+ });
+})();
+
diff --git a/views/ngXosViews/serviceGrid/src/sass/main.scss b/views/ngXosViews/serviceGrid/src/sass/main.scss
index bc8d14a..e892746 100644
--- a/views/ngXosViews/serviceGrid/src/sass/main.scss
+++ b/views/ngXosViews/serviceGrid/src/sass/main.scss
@@ -1,4 +1,5 @@
@import '../../../../style/sass/lib/_variables.scss';
+@import './side-panel.scss';
#xosServiceGrid {
service-graph {
diff --git a/views/ngXosViews/serviceGrid/src/sass/side-panel.scss b/views/ngXosViews/serviceGrid/src/sass/side-panel.scss
new file mode 100644
index 0000000..0d5d508
--- /dev/null
+++ b/views/ngXosViews/serviceGrid/src/sass/side-panel.scss
@@ -0,0 +1,54 @@
+@import '../../../../style/sass/lib/_variables.scss';
+
+xos-side-panel{
+
+ .xos-side-panel-content{
+ position: fixed;
+ width: 30%;
+ height: 100%;
+ padding: 25px;
+ background: #fff;
+ overflow: scroll;
+
+ &.right {
+ border-left: 1px solid $gray;
+ top: 0;
+ //right: -35%;
+ right: 0;
+ visibility: hidden;
+ box-shadow: -10px 0px 20px -8px rgba(0,0,0,0.75);
+ }
+
+ &.right.out {
+ animation: 0.5s slideOutRight ease-in-out;
+ visibility: visible;
+ }
+
+ &.right.in {
+ animation: 0.5s slideInRight ease-in-out;
+ visibility: visible;
+ }
+ }
+}
+
+@keyframes slideInRight {
+ from {
+ transform: translate3d(100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes slideOutRight {
+ from {
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ transform: translate3d(100%, 0, 0);
+ }
+}
\ No newline at end of file
diff --git a/views/ngXosViews/serviceGrid/src/templates/service-graph.tpl.html b/views/ngXosViews/serviceGrid/src/templates/service-graph.tpl.html
index c61da91..328ea7d 100644
--- a/views/ngXosViews/serviceGrid/src/templates/service-graph.tpl.html
+++ b/views/ngXosViews/serviceGrid/src/templates/service-graph.tpl.html
@@ -1,9 +1,6 @@
<div class="row">
<div class="col-sm-10">
<h1>Graph</h1>
- <ul>
- <li>Use D3 to create a service chart based on coarse services?</li>
- </ul>
</div>
<div class="col-sm-2">
<a href="/admin/core/service/add" class="btn btn-success btn-block">
@@ -14,4 +11,30 @@
Service List
</a>
</div>
-</div>
\ No newline at end of file
+</div>
+<xos-side-panel config="vm.panelConfig" show="vm.panelShow">
+ <h1>
+ {{vm.selectedNode.name}}
+ <small>
+ <i class="glyphicon glyphicon-{{vm.selectedNode.icon}}"></i>
+ </small>
+ </h1>
+ <table class="table">
+ <tr>
+ <td>Kind:</td>
+ <td>{{vm.selectedNode.kind}}</td>
+ </tr>
+ <tr>
+ <td>Enabled:</td>
+ <td>{{vm.selectedNode.enabled}}</td>
+ </tr>
+ <tr>
+ <td>Status:</td>
+ <td>{{vm.selectedNode.backend_status}}</td>
+ </tr>
+ </table>
+ <a ng-click="vm.exportToTosca(vm.selectedNode)" class="btn btn-primary">
+ Export to TOSCA
+ <i class="glyphicon glyphicon-export"></i>
+ </a>
+</xos-side-panel>
\ No newline at end of file
diff --git a/views/ngXosViews/serviceGrid/src/templates/service-grid.tpl.html b/views/ngXosViews/serviceGrid/src/templates/service-grid.tpl.html
index 8e88e38..37402d6 100644
--- a/views/ngXosViews/serviceGrid/src/templates/service-grid.tpl.html
+++ b/views/ngXosViews/serviceGrid/src/templates/service-grid.tpl.html
@@ -7,9 +7,9 @@
<i class="glyphicon glyphicon-plus"></i>
Add Service
</a>
- <!-- <a href="#/graph" class="btn btn-default btn-block">
+ <a href="#/graph" class="btn btn-default btn-block">
Tenancy Graph
- </a> -->
+ </a>
</div>
</div>