Added slices and instances to tree layout
diff --git a/gui/ngXosViews/serviceTopology/env/default.js b/gui/ngXosViews/serviceTopology/env/default.js
index 6f37b43..a4c949a 100644
--- a/gui/ngXosViews/serviceTopology/env/default.js
+++ b/gui/ngXosViews/serviceTopology/env/default.js
@@ -7,7 +7,7 @@
// (works only for local environment as both application are served on the same domain)
module.exports = {
- host: 'http://xos:9999/',
- xoscsrftoken: 'ECWvwOnY0QfcDAlot6KZsInrPvNoz5F8',
- xossessionid: 'nxc5wr1l6qhqwzl802on8f9xfk9uypv1'
+ host: 'http://clnode078.clemson.cloudlab.us:9999/',
+ xoscsrftoken: 'jhkNhrGWS7FyvzPAWEsJtVkgwYSODDhm',
+ xossessionid: 'dr26sgw3bixae7pqyy7soydf864a554u'
};
diff --git a/gui/ngXosViews/serviceTopology/src/css/serviceTopology.css b/gui/ngXosViews/serviceTopology/src/css/serviceTopology.css
index 2afcf08..b241361 100644
--- a/gui/ngXosViews/serviceTopology/src/css/serviceTopology.css
+++ b/gui/ngXosViews/serviceTopology/src/css/serviceTopology.css
@@ -4,6 +4,11 @@
display: block;
}
+service-canvas svg {
+ position: absolute;
+ top: 0;
+}
+
.node {
cursor: pointer;
}
@@ -19,6 +24,14 @@
stroke: #ffdb07;
}
+.node.slice circle {
+ stroke: #b01dff;
+}
+
+.node.instance circle {
+ stroke: #ea25ff;
+}
+
.node rect.slice-detail{
fill: #fff;
stroke: steelblue;
diff --git a/gui/ngXosViews/serviceTopology/src/js/config.js b/gui/ngXosViews/serviceTopology/src/js/config.js
index b5c0db0..3ef8748 100644
--- a/gui/ngXosViews/serviceTopology/src/js/config.js
+++ b/gui/ngXosViews/serviceTopology/src/js/config.js
@@ -4,7 +4,8 @@
angular.module('xos.serviceTopology')
.constant('serviceTopologyConfig', {
widthMargin: 100,
- heightMargin: 30
+ heightMargin: 30,
+ duration: 750
})
}());
\ No newline at end of file
diff --git a/gui/ngXosViews/serviceTopology/src/js/d3.js b/gui/ngXosViews/serviceTopology/src/js/d3.js
index 52127a4..9452e5f 100644
--- a/gui/ngXosViews/serviceTopology/src/js/d3.js
+++ b/gui/ngXosViews/serviceTopology/src/js/d3.js
@@ -5,10 +5,214 @@
.factory('d3', function($window){
return $window.d3;
})
- .service('TreeLayout', function(){
- this.updateTree = (source) => {
+ .service('TreeLayout', function($window, lodash, ServiceRelation, serviceTopologyConfig, Slice, Instances){
- }
+ var _svg, _layout, _source;
+
+ var i = 0;
+
+ // given a canvas, a layout and a data source, draw a tree layout
+ const updateTree = (svg, layout, source) => {
+
+ //cache data
+ _svg = svg;
+ _layout = layout;
+ _source = source;
+
+ const maxDepth = ServiceRelation.depthOf(source);
+
+ const diagonal = d3.svg.diagonal()
+ .projection(d => [d.y, d.x]);
+
+ // Compute the new tree layout.
+ var nodes = layout.nodes(source).reverse(),
+ links = layout.links(nodes);
+
+ // Normalize for fixed-depth.
+ nodes.forEach(function(d) {
+ // position the child node horizontally
+ d.y = d.depth * (($window.innerWidth - (serviceTopologyConfig.widthMargin * 2)) / maxDepth);
+ console.log(d.id);
+ if(d.type == 'slice'){
+ console.info('slice found!', d);
+ }
+ });
+
+ // Update the nodes…
+ var node = svg.selectAll('g.node')
+ .data(nodes, function(d) { return d.id || (d.id = ++i); });
+
+ // Enter any new nodes at the parent's previous position.
+ var nodeEnter = node.enter().append('g')
+ .attr({
+ class: d => `node ${d.type}`,
+ transform: d => `translate(${source.y0},${source.x0})` // this is the starting position
+ });
+
+ nodeEnter.append('circle')
+ .attr('r', 1e-6)
+ .style('fill', d => d._children ? 'lightsteelblue' : '#fff')
+ .on('click', serviceClick);
+
+ nodeEnter.append('text')
+ .attr('x', function(d) { return d.children || d._children ? -13 : 13; })
+ .attr('transform', function(d) {
+ if((d.children || d._children) && d.parent || d._parent){
+ return 'rotate(30)';
+ }
+ })
+ .attr('dy', '.35em')
+ .attr('text-anchor', function(d) { return d.children || d._children ? 'end' : 'start'; })
+ .text(function(d) { return d.name; })
+ .style('fill-opacity', 1e-6);
+
+ // Transition nodes to their new position.
+ var nodeUpdate = node.transition()
+ .duration(serviceTopologyConfig.duration)
+ .attr('transform', function(d) {
+ return 'translate(' + d.y + ',' + d.x + ')';
+ });
+
+ nodeUpdate.select('circle')
+ .attr('r', d => d.selected ? 15 : 10)
+ .style('fill', function(d) { return d._children ? 'lightsteelblue' : '#fff'; });
+
+ nodeUpdate.select('text')
+ .style('fill-opacity', 1);
+
+ // Transition exiting nodes to the parent's new position.
+ var nodeExit = node.exit().transition()
+ .duration(serviceTopologyConfig.duration)
+ .attr('transform', function(d) { return 'translate(' + source.y + ',' + source.x + ')'; })
+ .remove();
+
+ nodeExit.select('circle')
+ .attr('r', 1e-6);
+
+ nodeExit.select('text')
+ .style('fill-opacity', 1e-6);
+
+ // Update the links…
+ var link = svg.selectAll('path.link')
+ .data(links, function(d) { return d.target.id; });
+
+ // Enter any new links at the parent's previous position.
+ link.enter().insert('path', 'g')
+ .attr('class', 'link')
+ .attr('d', function(d) {
+ var o = {x: source.x0, y: source.y0};
+ return diagonal({source: o, target: o});
+ });
+
+ // Transition links to their new position.
+ link.transition()
+ .duration(serviceTopologyConfig.duration)
+ .attr('d', diagonal);
+
+ // Transition exiting nodes to the parent's new position.
+ link.exit().transition()
+ .duration(serviceTopologyConfig.duration)
+ .attr('d', function(d) {
+ var o = {x: source.x, y: source.y};
+ return diagonal({source: o, target: o});
+ })
+ .remove();
+
+ // Stash the old positions for transition.
+ nodes.forEach(function(d) {
+ d.x0 = d.x;
+ d.y0 = d.y;
+ });
+ };
+
+ const serviceClick = function(d) {
+
+ // empty panel
+ //_this.slices = [];
+ //_this.instances = [];
+
+ // reset all the nodes to default radius
+ var nodes = d3.selectAll('circle')
+ .transition()
+ .duration(serviceTopologyConfig.duration)
+ .attr('r', 10);
+
+ // remove slices details
+ d3.selectAll('rect.slice-detail')
+ .remove();
+ d3.selectAll('text.slice-name')
+ .remove();
+
+ var selectedNode = d3.select(this);
+
+ selectedNode
+ .transition()
+ .duration(serviceTopologyConfig.duration)
+ .attr('r', 15);
+
+ if(!d.service){
+ return;
+ }
+
+ //_this.selectedService = {
+ // id: d.id,
+ // name: d.name
+ //};
+
+ ServiceRelation.getServiceInterfaces(d.service.id)
+ .then(interfaceTree => {
+
+ const isDetailed = lodash.find(d.children, {type: 'slice'});
+ if(isDetailed){
+ d.selected = false;
+ lodash.remove(d.children, {type: 'slice'});
+ }
+ else {
+ d.selected = true;
+
+ d.children = d.children.concat(interfaceTree);
+ }
+
+ updateTree(_svg, _layout, _source);
+ // draw a rect with slice names
+ //const parentNode = d3.select(this.parentNode);
+ //parentNode
+ // .append('rect')
+ // .style('opacity', 0)
+ // .attr({
+ // width: 150,
+ // height: 50,
+ // y: 35,
+ // x: -75,
+ // class: 'slice-detail'
+ // })
+ // .transition()
+ // .duration(serviceTopologyConfig.duration)
+ // .style('opacity', 1);
+ // TODO attach a click listener to draw instances and networks
+
+ //parentNode
+ // .append('text')
+ // .style('opacity', 0)
+ // .attr({
+ // y: 65,
+ // x: -60,
+ // class: 'slice-name'
+ // })
+ // .text(() => {
+ // if(slices[0]){
+ // return slices[0].humanReadableName;
+ // }
+ //
+ // return '';
+ // })
+ // .transition()
+ // .duration(serviceTopologyConfig.duration)
+ // .style('opacity', 1);
+ });
+ };
+
+ this.updateTree = updateTree;
});
}());
\ No newline at end of file
diff --git a/gui/ngXosViews/serviceTopology/src/js/main.js b/gui/ngXosViews/serviceTopology/src/js/main.js
index 5fe1896..4e6239a 100644
--- a/gui/ngXosViews/serviceTopology/src/js/main.js
+++ b/gui/ngXosViews/serviceTopology/src/js/main.js
@@ -17,7 +17,4 @@
})
.config(function($httpProvider){
$httpProvider.interceptors.push('NoHyperlinks');
-})
-.factory('_', function($window){
- return window._;
});
diff --git a/gui/ngXosViews/serviceTopology/src/js/services.js b/gui/ngXosViews/serviceTopology/src/js/services.js
index e7c6e3f..55eac9c 100644
--- a/gui/ngXosViews/serviceTopology/src/js/services.js
+++ b/gui/ngXosViews/serviceTopology/src/js/services.js
@@ -17,7 +17,7 @@
.service('Subscribers', function($resource){
return $resource('/xos/subscribers', {id: '@id'});
})
- .service('ServiceRelation', function($q, _, lodash, Services, Tenant){
+ .service('ServiceRelation', function($q, lodash, Services, Tenant, Slice, Instances){
// count the mas depth of an object
const depthOf = (obj) => {
@@ -128,6 +128,49 @@
return deferred.promise;
};
+ const getServiceInterfaces = (serviceId) => {
+ var deferred = $q.defer();
+
+ var _slices;
+
+ Slice.query({service: serviceId}).$promise
+ .then((slices) => {
+ _slices = slices;
+ const promisesArr = slices.reduce((promises, slice) => {
+ promises.push(Instances.query({slice: slice.id}).$promise);
+ return promises;
+ }, []);
+
+ return $q.all(promisesArr);
+ })
+ .then((instances) => {
+ console.log(instances);
+ // parse data to build a tree (2 level only)
+ var interfaceTree = [];
+ lodash.forEach(_slices, (slice, i) => {
+ let current = {
+ name: slice.name,
+ slice: slice,
+ type: 'slice',
+ children: instances[i].map((instance) => {
+ return {
+ name: instance.humanReadableName,
+ children: [],
+ type: 'instance',
+ instance: instance
+ };
+
+ })
+ };
+ interfaceTree.push(current);
+ });
+ console.log(interfaceTree)
+ deferred.resolve(interfaceTree);
+ });
+
+ return deferred.promise;
+ };
+
// export APIs
this.get = get;
this.buildLevel = buildLevel;
@@ -135,6 +178,7 @@
this.findLevelRelation = findLevelRelation;
this.findLevelServices = findLevelServices;
this.depthOf = depthOf;
+ this.getServiceInterfaces = getServiceInterfaces;
});
}());
\ No newline at end of file
diff --git a/gui/ngXosViews/serviceTopology/src/js/topologyCanvas.js b/gui/ngXosViews/serviceTopology/src/js/topologyCanvas.js
index 008eaa3..b53d29f 100644
--- a/gui/ngXosViews/serviceTopology/src/js/topologyCanvas.js
+++ b/gui/ngXosViews/serviceTopology/src/js/topologyCanvas.js
@@ -9,7 +9,7 @@
bindToController: true,
controllerAs: 'vm',
templateUrl: 'templates/topology_canvas.tpl.html',
- controller: function($element, $window, d3, serviceTopologyConfig, ServiceRelation, Slice, Instances, Subscribers){
+ controller: function($element, $window, d3, serviceTopologyConfig, ServiceRelation, Slice, Instances, Subscribers, TreeLayout){
this.instances = [];
this.slices = [];
@@ -17,18 +17,15 @@
const width = $window.innerWidth - serviceTopologyConfig.widthMargin;
const height = $window.innerHeight - serviceTopologyConfig.heightMargin;
- const tree = d3.layout.tree()
+ const treeLayout = d3.layout.tree()
.size([height, width]);
- const diagonal = d3.svg.diagonal()
- .projection(d => [d.y, d.x]);
-
const svg = d3.select($element[0])
.append('svg')
.style('width', `${$window.innerWidth}px`)
.style('height', `${$window.innerHeight}px`)
.append('g')
- .attr('transform', 'translate(' + serviceTopologyConfig.widthMargin+ '','' + serviceTopologyConfig.heightMargin + '')'');;
+ .attr('transform', 'translate(' + serviceTopologyConfig.widthMargin+ ',' + serviceTopologyConfig.heightMargin + ')');
//const resizeCanvas = () => {
// var targetSize = svg.node().getBoundingClientRect();
@@ -47,199 +44,16 @@
// update(root);
// });
var root;
- var i = 0;
- var duration = 750;
const draw = (tree) => {
root = tree;
root.x0 = $window.innerHeight / 2;
root.y0 = 0;
- update(root);
+ TreeLayout.updateTree(svg, treeLayout, root);
};
- function update(source) {
-
- const maxDepth = depthOf(source);
-
- // Compute the new tree layout.
- var nodes = tree.nodes(root).reverse(),
- links = tree.links(nodes);
-
- // Normalize for fixed-depth.
- nodes.forEach(function(d) {
- // position the child node horizontally
- d.y = d.depth * (($window.innerWidth - (serviceTopologyConfig.widthMargin * 2)) / maxDepth);
- });
-
- // Update the nodes…
- var node = svg.selectAll('g.node')
- .data(nodes, function(d) { return d.id || (d.id = ++i); });
-
- // Enter any new nodes at the parent's previous position.
- var nodeEnter = node.enter().append('g')
- .attr({
- class: d => `node ${d.type}`
- })
- .attr('transform', function(d) {
- // this is the starting position
- return 'translate(' + source.y0 + ',' + source.x0 + ')';
- });
-
- nodeEnter.append('circle')
- .attr('r', 1e-6)
- .style('fill', function(d) { return d._children ? 'lightsteelblue' : '#fff'; })
- .on('click', click);
-
- nodeEnter.append('text')
- .attr('x', function(d) { return d.children || d._children ? -13 : 13; })
- .attr('transform', function(d) {
- if((d.children || d._children) && d.parent || d._parent){
- return 'rotate(30)';
- }
- })
- .attr('dy', '.35em')
- .attr('text-anchor', function(d) { return d.children || d._children ? 'end' : 'start'; })
- .text(function(d) { return d.name; })
- .style('fill-opacity', 1e-6);
-
- // Transition nodes to their new position.
- var nodeUpdate = node.transition()
- .duration(duration)
- .attr('transform', function(d) {
- return 'translate(' + d.y + ',' + d.x + ')';
- });
-
- nodeUpdate.select('circle')
- .attr('r', 10)
- .style('fill', function(d) { return d._children ? 'lightsteelblue' : '#fff'; });
-
- nodeUpdate.select('text')
- .style('fill-opacity', 1);
-
- // Transition exiting nodes to the parent's new position.
- var nodeExit = node.exit().transition()
- .duration(duration)
- .attr('transform', function(d) { return 'translate(' + source.y + ',' + source.x + ')'; })
- .remove();
-
- nodeExit.select('circle')
- .attr('r', 1e-6);
-
- nodeExit.select('text')
- .style('fill-opacity', 1e-6);
-
- // Update the links…
- var link = svg.selectAll('path.link')
- .data(links, function(d) { return d.target.id; });
-
- // Enter any new links at the parent's previous position.
- link.enter().insert('path', 'g')
- .attr('class', 'link')
- .attr('d', function(d) {
- var o = {x: source.x0, y: source.y0};
- return diagonal({source: o, target: o});
- });
-
- // Transition links to their new position.
- link.transition()
- .duration(duration)
- .attr('d', diagonal);
-
- // Transition exiting nodes to the parent's new position.
- link.exit().transition()
- .duration(duration)
- .attr('d', function(d) {
- var o = {x: source.x, y: source.y};
- return diagonal({source: o, target: o});
- })
- .remove();
-
- // Stash the old positions for transition.
- nodes.forEach(function(d) {
- d.x0 = d.x;
- d.y0 = d.y;
- });
- }
-
var _this = this;
- const click = function(d) {
-
- // empty panel
- _this.slices = [];
- _this.instances = [];
-
- var nodes = d3.selectAll('circle')
- .transition()
- .duration(duration)
- .attr('r', 10);
-
- d3.selectAll('rect.slice-detail')
- .remove();
-
- d3.selectAll('text.slice-name')
- .remove();
-
- var selectedNode = d3.select(this);
-
- selectedNode
- .transition()
- .duration(duration)
- .attr('r', 15);
-
- if(!d.service){
- return;
- }
-
- _this.selectedService = {
- id: d.id,
- name: d.name
- };
-
- Slice.query({service: d.service.id}).$promise
- .then(slices => {
- _this.slices = slices;
-
- if(slices.length > 0){
- // TODO slice can be more than 1 create a for loop
- const parentNode = d3.select(this.parentNode);
-
- parentNode
- .append('rect')
- .style('opacity', 0)
- .attr({
- width: 150,
- height: 50,
- y: 35,
- x: -75,
- class: 'slice-detail'
- })
- .transition()
- .duration(duration)
- .style('opacity', 1);
- // TODO attach a click listener to draw instances and networks
-
- parentNode
- .append('text')
- .style('opacity', 0)
- .attr({
- y: 65,
- x: -60,
- class: 'slice-name'
- })
- .text(() => {
- if(slices[0]){
- return slices[0].humanReadableName;
- }
-
- return '';
- })
- .transition()
- .duration(duration)
- .style('opacity', 1);
- }
- })
- };
Subscribers.query().$promise
.then((subscribers) => {
@@ -258,6 +72,7 @@
})
};
+ // redraw when subrsbiber change
this.getServiceChain = (subscriber) => {
ServiceRelation.get(subscriber)
.then((tree) => {
diff --git a/gui/ngXosViews/serviceTopology/src/templates/topology_canvas.tpl.html b/gui/ngXosViews/serviceTopology/src/templates/topology_canvas.tpl.html
index 5336ddc..35857a3 100644
--- a/gui/ngXosViews/serviceTopology/src/templates/topology_canvas.tpl.html
+++ b/gui/ngXosViews/serviceTopology/src/templates/topology_canvas.tpl.html
@@ -1,30 +1,30 @@
<select class="form-control" ng-options="s as s.name for s in vm.subscribers" ng-model="vm.selectedSubscriber" ng-change="vm.getServiceChain(vm.selectedSubscriber)">
<option value="">Select a subscriber...</option>
</select>
-<div class="service-details">
- <div class="service-slices animate" ng-hide="vm.slices.length == 0">
- <div class="panel panel-info">
- <div class="panel-heading">
- Slices for service:
- <h3 class="panel-title">{{vm.selectedService.name}}</h3>
- </div>
- <ul class="list-group">
- <li class="list-group-item" ng-repeat="slice in vm.slices" ng-click="vm.getInstances(slice)" ng-class="{active: slice.id === vm.selectedSlice.id}">
- {{slice.humanReadableName}}
- </li>
- </ul>
- </div>
- </div>
- <div class="service-instances animate" ng-hide="vm.instances.length == 0">
- <div class="panel panel-warning">
- <div class="panel-heading">
- Instances:
- </div>
- <ul class="list-group">
- <li class="list-group-item" ng-repeat="instance in vm.instances">
- {{instance.humanReadableName}}
- </li>
- </ul>
- </div>
- </div>
-</div>
\ No newline at end of file
+<!--<div class="service-details">-->
+ <!--<div class="service-slices animate" ng-hide="vm.slices.length == 0">-->
+ <!--<div class="panel panel-info">-->
+ <!--<div class="panel-heading">-->
+ <!--Slices for service:-->
+ <!--<h3 class="panel-title">{{vm.selectedService.name}}</h3>-->
+ <!--</div>-->
+ <!--<ul class="list-group">-->
+ <!--<li class="list-group-item" ng-repeat="slice in vm.slices" ng-click="vm.getInstances(slice)" ng-class="{active: slice.id === vm.selectedSlice.id}">-->
+ <!--{{slice.humanReadableName}}-->
+ <!--</li>-->
+ <!--</ul>-->
+ <!--</div>-->
+ <!--</div>-->
+ <!--<div class="service-instances animate" ng-hide="vm.instances.length == 0">-->
+ <!--<div class="panel panel-warning">-->
+ <!--<div class="panel-heading">-->
+ <!--Instances:-->
+ <!--</div>-->
+ <!--<ul class="list-group">-->
+ <!--<li class="list-group-item" ng-repeat="instance in vm.instances">-->
+ <!--{{instance.humanReadableName}}-->
+ <!--</li>-->
+ <!--</ul>-->
+ <!--</div>-->
+ <!--</div>-->
+<!--</div>-->
\ No newline at end of file