378 lines
12 KiB
JavaScript
378 lines
12 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2015 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
(function(root, factory) {
|
|
if (typeof(define) === 'function' && define.amd)
|
|
define(["angular", "d3" ], factory);
|
|
else
|
|
factory(root.angular, root.d3);
|
|
}(this, function(angular, d3) {
|
|
"use strict";
|
|
|
|
/* A cache to prevent jumping when rapidly toggling views */
|
|
var cache = { };
|
|
|
|
function topology_graph(selector, notify, options) {
|
|
var outer = d3.select(selector);
|
|
|
|
/* Kinds of objects to show */
|
|
var kinds = null;
|
|
|
|
/* Data we've been fed */
|
|
var items = { };
|
|
var relations = [ ];
|
|
|
|
/* Graph information */
|
|
var width;
|
|
var height;
|
|
var radius = 20;
|
|
if (options["radius"]) {
|
|
radius = options["radius"];
|
|
}
|
|
var timeout;
|
|
var nodes = [];
|
|
var links = [];
|
|
var lookup = { };
|
|
var selection = null;
|
|
var force = options["force"];
|
|
|
|
/* Allow the force to be passed in, default if not */
|
|
if (!force) {
|
|
force = d3.layout.force()
|
|
.charge(-800)
|
|
.gravity(0.2)
|
|
.linkDistance(80);
|
|
}
|
|
|
|
var drag = force.drag();
|
|
|
|
var svg = outer.append("svg")
|
|
.attr("viewBox", "0 0 1600 1200")
|
|
.attr("preserveAspectRatio", "xMidYMid meet")
|
|
.attr("class", "kube-topology");
|
|
|
|
var vertices = d3.select();
|
|
var edges = d3.select();
|
|
|
|
force.on("tick", function() {
|
|
edges.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; });
|
|
|
|
vertices
|
|
.attr("cx", function(d) {
|
|
d.x = d.fixed ? d.x : Math.max(radius, Math.min(width - radius, d.x));
|
|
return d.x;
|
|
})
|
|
.attr("cy", function(d) {
|
|
d.y = d.fixed ? d.y : Math.max(radius, Math.min(height - radius, d.y));
|
|
return d.y;
|
|
})
|
|
.attr("transform", function(d) {
|
|
return "translate(" + d.x + "," + d.y + ")";
|
|
});
|
|
});
|
|
|
|
drag
|
|
.on("dragstart", function(d) {
|
|
notify(d.item);
|
|
|
|
if (d.fixed !== true)
|
|
d.floatpoint = [ d.x, d.y ];
|
|
d.fixed = true;
|
|
d3.select(this).classed("fixed", true);
|
|
})
|
|
.on("dragend", function(d) {
|
|
var moved = true;
|
|
if (d.floatpoint) {
|
|
moved = (d.x < d.floatpoint[0] - 5 || d.x > d.floatpoint[0] + 5) ||
|
|
(d.y < d.floatpoint[1] - 5 || d.y > d.floatpoint[1] + 5);
|
|
delete d.floatpoint;
|
|
}
|
|
d.fixed = moved && d.x > 3 && d.x < (width - 3) && d.y >= 3 && d.y < (height - 3);
|
|
d3.select(this).classed("fixed", d.fixed);
|
|
});
|
|
|
|
svg
|
|
.on("dblclick", function() {
|
|
svg.selectAll("g")
|
|
.classed("fixed", false)
|
|
.each(function(d) { d.fixed = false; });
|
|
force.start();
|
|
})
|
|
.on("click", function(ev) {
|
|
if (!d3.select(d3.event.target).datum()) {
|
|
notify(null);
|
|
}
|
|
});
|
|
|
|
function select(item) {
|
|
selection = item;
|
|
svg.selectAll("g")
|
|
.classed("selected", function(d) { return d.item === item; });
|
|
}
|
|
|
|
function adjust() {
|
|
timeout = null;
|
|
width = outer.node().clientWidth;
|
|
height = outer.node().clientHeight;
|
|
|
|
force.size([width, height]);
|
|
svg.attr("viewBox", "0 0 " + width + " " + height);
|
|
update();
|
|
}
|
|
|
|
function update() {
|
|
edges = svg.selectAll("line")
|
|
.data(links);
|
|
|
|
edges.exit().remove();
|
|
edges.enter().insert("line", ":first-child");
|
|
|
|
edges.attr("class", function(d) { return d.kinds; });
|
|
|
|
vertices = svg.selectAll("g")
|
|
.data(nodes, function(d) { return d.id; });
|
|
|
|
vertices.exit().remove();
|
|
|
|
var added = vertices.enter().append("g")
|
|
.call(drag);
|
|
|
|
select(selection);
|
|
|
|
force
|
|
.nodes(nodes)
|
|
.links(links)
|
|
.start();
|
|
|
|
return added;
|
|
}
|
|
|
|
function digest() {
|
|
var pnodes = nodes;
|
|
var plookup = lookup;
|
|
|
|
/* The actual data for the graph */
|
|
nodes = [];
|
|
links = [];
|
|
lookup = { };
|
|
|
|
var item, id, kind, node;
|
|
for (id in items) {
|
|
item = items[id];
|
|
kind = item.kind;
|
|
|
|
if (kinds && !kinds[kind])
|
|
continue;
|
|
|
|
/* Prevents flicker */
|
|
node = pnodes[plookup[id]];
|
|
if (!node) {
|
|
node = cache[id];
|
|
delete cache[id];
|
|
if (!node)
|
|
node = { };
|
|
}
|
|
|
|
node.id = id;
|
|
node.item = item;
|
|
|
|
lookup[id] = nodes.length;
|
|
nodes.push(node);
|
|
}
|
|
|
|
var i, len, relation, s, t;
|
|
for (i = 0, len = relations.length; i < len; i++) {
|
|
relation = relations[i];
|
|
|
|
s = lookup[relation.source];
|
|
t = lookup[relation.target];
|
|
if (s === undefined || t === undefined)
|
|
continue;
|
|
|
|
links.push({ source: s, target: t, kinds: nodes[s].item.kind + nodes[t].item.kind });
|
|
}
|
|
|
|
if (width && height)
|
|
return update();
|
|
else
|
|
return d3.select();
|
|
}
|
|
|
|
function resized() {
|
|
window.clearTimeout(timeout);
|
|
timeout = window.setTimeout(adjust, 150);
|
|
}
|
|
|
|
window.addEventListener('resize', resized);
|
|
|
|
adjust();
|
|
resized();
|
|
|
|
return {
|
|
select: select,
|
|
kinds: function(value) {
|
|
kinds = value;
|
|
var added = digest();
|
|
return [vertices, added];
|
|
},
|
|
data: function(new_items, new_relations) {
|
|
items = new_items || { };
|
|
relations = new_relations || [];
|
|
var added = digest();
|
|
return [vertices, added];
|
|
},
|
|
close: function() {
|
|
window.removeEventListener('resize', resized);
|
|
window.clearTimeout(timeout);
|
|
|
|
/*
|
|
* Keep the positions of these items cached,
|
|
* in case we are asked to make the same graph again.
|
|
*/
|
|
var id, node;
|
|
cache = { };
|
|
for (id in lookup) {
|
|
node = nodes[lookup[id]];
|
|
delete node.item;
|
|
cache[id] = node;
|
|
}
|
|
|
|
nodes = [ ];
|
|
lookup = { };
|
|
}
|
|
};
|
|
}
|
|
|
|
/* The kubernetesUI component is quite loosely bound, define if it doesn't exist */
|
|
try { angular.module("kubernetesUI"); } catch(e) { angular.module("kubernetesUI", []); }
|
|
|
|
return angular.module('kubernetesUI')
|
|
.directive('kubernetesTopologyGraph', [
|
|
function() {
|
|
return {
|
|
restrict: 'E',
|
|
scope: {
|
|
items: '=',
|
|
relations: '=',
|
|
kinds: '=',
|
|
selection: '=',
|
|
force: '=',
|
|
radius: '='
|
|
},
|
|
link: function($scope, element, attributes) {
|
|
element.css("display", "block");
|
|
|
|
function notify(item) {
|
|
var event = $scope.$emit("select", item);
|
|
if (attributes["selection"] === undefined && !event.defaultPrevented)
|
|
graph.select(item);
|
|
}
|
|
|
|
function icon(d) {
|
|
var text;
|
|
var kinds = $scope.kinds;
|
|
if (kinds)
|
|
text = kinds[d.item.kind];
|
|
return text || "";
|
|
}
|
|
|
|
function weak(d) {
|
|
var status = d.item.status;
|
|
if (status && status.phase && status.phase !== "Running")
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
function title(d) {
|
|
return d.item.metadata.name;
|
|
}
|
|
|
|
function render(args) {
|
|
var vertices = args[0];
|
|
var added = args[1];
|
|
var event = $scope.$emit("render", vertices, added);
|
|
if (!event.defaultPrevented) {
|
|
added.attr("class", function(d) { return d.item.kind; });
|
|
added.append("use").attr("xlink:href", icon);
|
|
added.append("title");
|
|
vertices.selectAll("title")
|
|
.text(function(d) { return d.item.metadata.name; });
|
|
vertices.classed("weak", weak);
|
|
}
|
|
}
|
|
|
|
var options = {"force" : $scope.force, "radius" : $scope.radius};
|
|
var graph = topology_graph(element[0], notify, options);
|
|
|
|
/* If there's a kinds in the current scope, watch it for changes */
|
|
$scope.$watchCollection("kinds", function(value) {
|
|
render(graph.kinds(value));
|
|
});
|
|
|
|
$scope.$watchCollection('[items, relations]', function(values) {
|
|
render(graph.data(values[0], values[1]));
|
|
});
|
|
|
|
/* Watch the selection for changes */
|
|
$scope.$watch("selection", function(item) {
|
|
graph.select(item);
|
|
});
|
|
|
|
element.on("$destroy", function() {
|
|
graph.close();
|
|
});
|
|
}
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive('kubernetesTopologyIcon',
|
|
function() {
|
|
return {
|
|
restrict: 'E',
|
|
transclude: true,
|
|
template: "<ng-transclude></ng-transclude>",
|
|
link: function($scope, element, attrs) {
|
|
var kind = attrs.kind;
|
|
var value = $scope.kinds[kind];
|
|
|
|
$scope.$watchCollection("kinds", function() {
|
|
element.toggleClass("active", kind in $scope.kinds);
|
|
});
|
|
|
|
element.on("click", function() {
|
|
if (kind in $scope.kinds) {
|
|
value = $scope.kinds[kind];
|
|
delete $scope.kinds[kind];
|
|
} else {
|
|
$scope.kinds[kind] = value;
|
|
}
|
|
if ($scope.$parent)
|
|
$scope.$parent.$digest();
|
|
$scope.$digest();
|
|
});
|
|
}
|
|
};
|
|
}
|
|
);
|
|
}));
|