diff options
author | Petr Vobornik <pvoborni@redhat.com> | 2015-11-12 18:17:50 +0100 |
---|---|---|
committer | Petr Vobornik <pvoborni@redhat.com> | 2015-11-27 15:50:56 +0100 |
commit | 24fead79cbe5ef168a42f08da6fd99b4ec2b1bab (patch) | |
tree | e9cf509f4d2a01a1a7d05b6af3cbf340318321ed /install/ui | |
parent | ce1645ceeca577d1c73ba77f3eb8cb13c2bef2a1 (diff) | |
download | freeipa-24fead79cbe5ef168a42f08da6fd99b4ec2b1bab.tar.gz freeipa-24fead79cbe5ef168a42f08da6fd99b4ec2b1bab.tar.xz freeipa-24fead79cbe5ef168a42f08da6fd99b4ec2b1bab.zip |
webui: topology graph component
https://fedorahosted.org/freeipa/ticket/4286
Reviewed-By: Martin Babinsky <mbabinsk@redhat.com>
Diffstat (limited to 'install/ui')
-rw-r--r-- | install/ui/doc/categories.json | 4 | ||||
-rw-r--r-- | install/ui/jsl.conf | 2 | ||||
-rw-r--r-- | install/ui/less/widgets.less | 45 | ||||
-rw-r--r-- | install/ui/src/freeipa/topology_graph.js | 380 |
4 files changed, 428 insertions, 3 deletions
diff --git a/install/ui/doc/categories.json b/install/ui/doc/categories.json index 93cadfe5d..7e9e19e9a 100644 --- a/install/ui/doc/categories.json +++ b/install/ui/doc/categories.json @@ -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", diff --git a/install/ui/jsl.conf b/install/ui/jsl.conf index 3e3bec05a..14d56810d 100644 --- a/install/ui/jsl.conf +++ b/install/ui/jsl.conf @@ -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 diff --git a/install/ui/less/widgets.less b/install/ui/less/widgets.less index 99b22068d..0f9bc8c17 100644 --- a/install/ui/less/widgets.less +++ b/install/ui/less/widgets.less @@ -144,4 +144,47 @@ } // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=409254 -tbody:empty { display: none; }
\ No newline at end of file +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; + } +} diff --git a/install/ui/src/freeipa/topology_graph.js b/install/ui/src/freeipa/topology_graph.js new file mode 100644 index 000000000..94d0aa6bf --- /dev/null +++ b/install/ui/src/freeipa/topology_graph.js @@ -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; +}); |