mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
webui: topology graph component
https://fedorahosted.org/freeipa/ticket/4286 Reviewed-By: Martin Babinsky <mbabinsk@redhat.com>
This commit is contained in:
parent
ce1645ceec
commit
24fead79cb
@ -193,7 +193,8 @@
|
||||
"widget.alert_helper",
|
||||
"IPA.option_widget_base",
|
||||
"IPA.column",
|
||||
"IPA.html_util"
|
||||
"IPA.html_util",
|
||||
"topology_graph.TopoGraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -257,6 +258,7 @@
|
||||
"radiusproxy",
|
||||
"stageuser",
|
||||
"topology",
|
||||
"topology_graph",
|
||||
"user",
|
||||
"plugins.api_browser",
|
||||
"plugins.caacl",
|
||||
|
@ -33,7 +33,7 @@
|
||||
+octal_number # leading zeros make an octal number
|
||||
+nested_comment # nested comment
|
||||
+misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma
|
||||
+ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement
|
||||
-ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement
|
||||
+empty_statement # empty statement or extra semicolon
|
||||
-missing_option_explicit # the "option explicit" control comment is missing
|
||||
+partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag
|
||||
|
@ -144,4 +144,47 @@
|
||||
}
|
||||
|
||||
// workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=409254
|
||||
tbody:empty { display: none; }
|
||||
tbody:empty { display: none; }
|
||||
|
||||
// Topology Graph
|
||||
|
||||
.topology-view {
|
||||
svg {
|
||||
background-color: #FFF;
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
path.link {
|
||||
fill: none;
|
||||
stroke-width: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.marker {
|
||||
stroke: rgba(0, 0, 0);
|
||||
}
|
||||
|
||||
path.link.selected {
|
||||
stroke-dasharray: 10,2;
|
||||
}
|
||||
|
||||
circle.node {
|
||||
stroke-width: 1.5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
text {
|
||||
font: 16px sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
text.id {
|
||||
text-anchor: middle;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
380
install/ui/src/freeipa/topology_graph.js
Normal file
380
install/ui/src/freeipa/topology_graph.js
Normal file
@ -0,0 +1,380 @@
|
||||
//
|
||||
// Copyright (C) 2015 FreeIPA Contributors see COPYING for license
|
||||
//
|
||||
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'dojo/_base/lang',
|
||||
'dojo/_base/declare',
|
||||
'dojo/on',
|
||||
'dojo/Evented',
|
||||
'./jquery',
|
||||
'libs/d3'
|
||||
],
|
||||
function(lang, declare, on, Evented, $, d3) {
|
||||
/**
|
||||
* Topology Graph module
|
||||
* @class
|
||||
* @singleton
|
||||
*/
|
||||
var topology_graph = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Topology graph visualization
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
topology_graph.TopoGraph = declare([Evented], {
|
||||
width: 960,
|
||||
height: 500,
|
||||
_colors: d3.scale.category10(),
|
||||
_svg : null,
|
||||
_path: null,
|
||||
_circle: null,
|
||||
|
||||
_selected_link: null,
|
||||
_mousedown_link: null,
|
||||
|
||||
/**
|
||||
* Nodes - IPA servers
|
||||
* id - int
|
||||
*
|
||||
* @property {Array}
|
||||
*/
|
||||
nodes: [],
|
||||
|
||||
/**
|
||||
* Links between nodes
|
||||
* @property {Array}
|
||||
*/
|
||||
links: [],
|
||||
|
||||
/**
|
||||
* List of suffices
|
||||
* @property {Array}
|
||||
*/
|
||||
suffices: [],
|
||||
|
||||
/**
|
||||
* Initializes the graph
|
||||
* @param {HTMLElement} container container where to put the graph svg element
|
||||
*/
|
||||
initialize: function(container) {
|
||||
this._create_svg(container);
|
||||
this.update(this.nodes, this.links, this.suffices);
|
||||
return;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the graph
|
||||
* @param {Array} nodes array of node objects
|
||||
* @param {Array} links array of link objects
|
||||
* @param {Array} suffices array of suffices
|
||||
*/
|
||||
update: function(nodes, links, suffices) {
|
||||
// delete all from svg
|
||||
this._svg.selectAll("*").remove();
|
||||
this._svg.attr('width', this.width)
|
||||
.attr('height', this.height);
|
||||
|
||||
this.links = links;
|
||||
this.nodes = nodes;
|
||||
this.suffices = suffices;
|
||||
|
||||
// load saved coordinates
|
||||
for (var i=0,l=nodes.length; i<l; i++) {
|
||||
var node = nodes[i];
|
||||
if (this._get_local_storage_attr(node.id, 'fixed') === 'true') {
|
||||
node.fixed = true;
|
||||
node.x = Number(this._get_local_storage_attr(node.id, 'x'));
|
||||
node.y = Number(this._get_local_storage_attr(node.id, 'y'));
|
||||
}
|
||||
}
|
||||
|
||||
this._init_layout();
|
||||
this._define_shapes();
|
||||
|
||||
// handles to link and node element groups
|
||||
this._path = this._svg.append('svg:g').selectAll('path');
|
||||
this._circle = this._svg.append('svg:g').selectAll('g');
|
||||
|
||||
this._selected_link = null;
|
||||
this._mouseup_node = null;
|
||||
this._mousedown_link = null;
|
||||
this.restart();
|
||||
},
|
||||
|
||||
_create_svg: function(container) {
|
||||
this._svg = d3.select(container[0]).
|
||||
append('svg').
|
||||
attr('width', this.width).
|
||||
attr('height', this.height);
|
||||
},
|
||||
|
||||
_init_layout: function() {
|
||||
var l = this._layout = d3.layout.force();
|
||||
l.links(this.links);
|
||||
l.nodes(this.nodes);
|
||||
l.size([this.width, this.height]);
|
||||
l.linkDistance(150);
|
||||
l.charge(-1000);
|
||||
l.on('tick', lang.hitch(this, this._tick));
|
||||
},
|
||||
|
||||
_get_local_storage_attr: function(id, attr) {
|
||||
return window.localStorage.getItem('topo_' + id + attr);
|
||||
},
|
||||
|
||||
_set_local_storage_attr: function(id, attr, value) {
|
||||
window.localStorage.setItem('topo_' + id + attr, value);
|
||||
},
|
||||
|
||||
_remove_local_storage_attr: function(id, attr) {
|
||||
window.localStorage.removeItem('topo_' + id + attr);
|
||||
},
|
||||
|
||||
_save_node_info: function(d) {
|
||||
if (d.fixed) {
|
||||
this._set_local_storage_attr(d.id, 'fixed', 'true');
|
||||
this._set_local_storage_attr(d.id, 'x', d.x);
|
||||
this._set_local_storage_attr(d.id, 'y', d.y);
|
||||
} else {
|
||||
this._remove_local_storage_attr(d.id, 'fixed');
|
||||
this._remove_local_storage_attr(d.id, 'x');
|
||||
this._remove_local_storage_attr(d.id, 'y');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulation tick which
|
||||
*
|
||||
* - adjusts link path and position
|
||||
* - node position
|
||||
* - saves node position
|
||||
*/
|
||||
_tick: function() {
|
||||
var self = this;
|
||||
// draw directed edges with proper padding from node centers
|
||||
this._path.attr('d', function(d) {
|
||||
var node_targets = d.source.targets[d.target.id];
|
||||
var target_count = node_targets.length;
|
||||
target_count = target_count ? target_count : 0;
|
||||
|
||||
// ensure right direction of curve
|
||||
var link_i = node_targets.indexOf(d);
|
||||
link_i = link_i === -1 ? 0 : link_i;
|
||||
var dir = link_i % 2;
|
||||
if (d.source.id < d.target.id) {
|
||||
dir = dir ? 0 : 1;
|
||||
}
|
||||
|
||||
var dx = d.target.x - d.source.x,
|
||||
dy = d.target.y - d.source.y;
|
||||
if (dx === 0) dx = 1;
|
||||
if (dy === 0) dy = 1;
|
||||
var dist = Math.sqrt(dx * dx + dy * dy),
|
||||
ux = dx / dist, // directional vector
|
||||
uy = dy / dist,
|
||||
nx = -uy, // normal vector
|
||||
ny = ux, // normal vector
|
||||
off = dir ? -1 : 1, // determines shift direction of curve
|
||||
ns = 5, // shift on normal vector
|
||||
s = target_count > 1 ? 1 : 0, // shift from center?
|
||||
spad = d.left ? 18 : 18, // source padding
|
||||
tpad = d.right ? 18 : 18, // target padding
|
||||
sourceX = d.source.x + (spad * ux) + off * nx * ns * s,
|
||||
sourceY = d.source.y + (spad * uy) + off * ny * ns * s,
|
||||
targetX = d.target.x - (tpad * ux) + off * nx * ns * s,
|
||||
targetY = d.target.y - (tpad * uy) + off * ny * ns * s,
|
||||
dr = s ? dist * Math.log10(dist) : 0;
|
||||
|
||||
return 'M' + sourceX + ',' + sourceY +
|
||||
'A' + dr + " " + dr + " 0 0 " + dir +" " +
|
||||
targetX + " " + targetY;
|
||||
});
|
||||
|
||||
this._circle.attr('transform', function(d) {
|
||||
self._save_node_info(d);
|
||||
return 'translate(' + d.x + ',' + d.y + ')';
|
||||
});
|
||||
},
|
||||
|
||||
_get_marker_name: function(suffix, start) {
|
||||
|
||||
var name = suffix ? suffix.cn[0] : 'drag';
|
||||
var arrow = start ? 'start-arrow' : 'end-arrow';
|
||||
return name + '-' + arrow;
|
||||
},
|
||||
|
||||
/**
|
||||
* Markers on the end of links
|
||||
*/
|
||||
_add_marker: function(name, color, refX) {
|
||||
this._svg.append('svg:defs')
|
||||
.append('svg:marker')
|
||||
.attr('id', name)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 6)
|
||||
.attr('markerWidth', 3)
|
||||
.attr('markerHeight', 3)
|
||||
.attr('orient', 'auto')
|
||||
.append('svg:path')
|
||||
.attr('d', refX)
|
||||
.attr('fill', color);
|
||||
},
|
||||
|
||||
/**
|
||||
* Suffix hint so user will know which links belong to which suffix
|
||||
*/
|
||||
_append_suffix_hint: function(suffix, x, y) {
|
||||
var color = d3.rgb(this._colors(suffix.cn[0]));
|
||||
this._svg.append('svg:text')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('class', 'suffix')
|
||||
.attr('fill', color)
|
||||
.text(suffix.cn[0]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Defines link arrows and colors of suffices(links) and nodes
|
||||
*/
|
||||
_define_shapes: function() {
|
||||
|
||||
var name, color;
|
||||
|
||||
var defs = this._svg.selectAll('defs');
|
||||
defs.remove();
|
||||
|
||||
var x = 10;
|
||||
var y = 20;
|
||||
|
||||
for (var i=0,l=this.suffices.length; i<l; i++) {
|
||||
|
||||
var suffix = this.suffices[i];
|
||||
color = d3.rgb(this._colors(suffix.cn[0]));
|
||||
|
||||
name = this._get_marker_name(suffix, false);
|
||||
this._add_marker(name, color, 'M0,-5L10,0L0,5');
|
||||
|
||||
name = this._get_marker_name(suffix, true);
|
||||
this._add_marker(name, color, 'M10,-5L0,0L10,5');
|
||||
|
||||
this._append_suffix_hint(suffix, x, y);
|
||||
y += 30;
|
||||
}
|
||||
|
||||
this._circle_color = this._colors(1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Restart the simulation to reflect changes in data/state
|
||||
*/
|
||||
restart: function() {
|
||||
var self = this;
|
||||
|
||||
// set the graph in motion
|
||||
self._layout.start();
|
||||
|
||||
// path (link) group
|
||||
this._path = this._path.data(self._layout.links());
|
||||
|
||||
// update existing links
|
||||
this._path
|
||||
.classed('selected', function(d) {
|
||||
return d === self._selected_link;
|
||||
})
|
||||
.style('marker-start', function(d) {
|
||||
var name = self._get_marker_name(d.suffix, true);
|
||||
return d.left ? 'url(#'+name+')' : '';
|
||||
})
|
||||
.style('marker-end', function(d) {
|
||||
var name = self._get_marker_name(d.suffix, false);
|
||||
return d.right ? 'url(#'+name+')' : '';
|
||||
});
|
||||
|
||||
|
||||
// add new links
|
||||
this._path.enter().append('svg:path')
|
||||
.attr('class', 'link')
|
||||
.style('stroke', function(d) {
|
||||
var suffix = d.suffix ? d.suffix.cn[0] : '';
|
||||
return d3.rgb(self._colors(suffix)).toString();
|
||||
})
|
||||
.classed('selected', function(d) {
|
||||
return d === self._selected_link;
|
||||
})
|
||||
.style('marker-start', function(d) {
|
||||
var name = self._get_marker_name(d.suffix, true);
|
||||
return d.left ? 'url(#'+name+')' : '';
|
||||
})
|
||||
.style('marker-end', function(d) {
|
||||
var name = self._get_marker_name(d.suffix, false);
|
||||
return d.right ? 'url(#'+name+')' : '';
|
||||
})
|
||||
.on('mousedown', function(d) {
|
||||
if (d3.event.ctrlKey) return;
|
||||
|
||||
// select link
|
||||
self._mousedown_link = d;
|
||||
if (self._mousedown_link === self._selected_link) {
|
||||
self._selected_link = null;
|
||||
} else {
|
||||
self._selected_link = self._mousedown_link;
|
||||
}
|
||||
self.emit('link-selected', { link: self._selected_link });
|
||||
self.restart();
|
||||
});
|
||||
|
||||
// remove old links
|
||||
this._path.exit().remove();
|
||||
|
||||
// circle (node) group
|
||||
this._circle = this._circle.data(
|
||||
self._layout.nodes(),
|
||||
function(d) {
|
||||
return d.id;
|
||||
}
|
||||
);
|
||||
|
||||
// add new nodes
|
||||
var g = this._circle.enter()
|
||||
.append('svg:g')
|
||||
.on("dblclick", function(d) {
|
||||
d.fixed = !d.fixed;
|
||||
})
|
||||
.call(self._layout.drag);
|
||||
|
||||
g.append('svg:circle')
|
||||
.attr('class', 'node')
|
||||
.attr('r', 12)
|
||||
.style('fill', function(d) {
|
||||
return self._colors(1);
|
||||
})
|
||||
.style('stroke', function(d) {
|
||||
return d3.rgb(self._colors(1)).darker().toString();
|
||||
});
|
||||
|
||||
// show node IDs
|
||||
g.append('svg:text')
|
||||
.attr('dx', 0)
|
||||
.attr('dy', 30)
|
||||
.attr('class', 'id')
|
||||
.attr('fill', '#002235')
|
||||
.text(function(d) {
|
||||
return d.id.split('.')[0];
|
||||
});
|
||||
|
||||
// remove old nodes
|
||||
self._circle.exit().remove();
|
||||
},
|
||||
|
||||
constructor: function(spec) {
|
||||
lang.mixin(this, spec);
|
||||
}
|
||||
});
|
||||
|
||||
return topology_graph;
|
||||
});
|
Loading…
Reference in New Issue
Block a user