diff --git a/src/charts/charts.less b/src/charts/charts.less index 62ba8f891..876fcaf25 100644 --- a/src/charts/charts.less +++ b/src/charts/charts.less @@ -186,3 +186,121 @@ pf-c3-chart { .camelcase { text-transform: capitalize; } + +pf-topology { + display: block; + user-select: none; +} + +.container-topology pf-topology { + height: 500px; + position: relative; +} +.container-topology .canvas { + position: absolute; +} + +.container-topology .popup { + position: absolute; + left: 0; + top: 0; + background-color: #fff; + width: 180px; + border: 1px #ccc solid; + border-radius: 6px; + box-shadow: #333 2px 2px 4px; + padding: 6px; + font-size: 14px; +} + +.container-topology .popup h5 { + font-weight: bold; +} + +.container-topology .popup p { + margin: 0 0 4px; +} + +.container-topology .popup p:hover { + color : #0099cc; + cursor: pointer; +} + + +.container-topology label.checkbox-inline { + font-size: 14px; +} + +.pf-topology-svg g { + font-family: PatternFlyIcons-webfont; + font-size: 18px; + text-anchor: middle; + cursor: pointer; +} + +.pf-topology-svg g text { + stroke: none; + stroke-width: 0px; +} + +.pf-topology-svg g.weak use { + opacity: .6; +} + +.pf-topology-svg g circle { + stroke: #aaa; + fill: #fff; +} + +.pf-topology-svg g.fixed use { + stroke-width: 2px; +} + +.pf-topology-svg g.selected use, +.pf-topology-svg g.selected circle { + stroke-width: 4px; +} + +.pf-topology-svg line { + stroke: #aaa; + stroke-width: 1; +} + +.pf-topology-svg g text.attached-label { + display: none; +} + +.pf-topology-svg g text.attached-label.visible { + font-size: 12px; + fill: black; + display: block; +} + +.pf-topology-svg g.selected { + stroke-width: 4px; +} + +.pf-topology-svg g circle { + stroke-width: 2px; +} + +.pf-topology-svg g circle.success { + stroke: #3F9C35; +} + +.pf-topology-svg g circle.error { + stroke: #CC0000; +} + +.pf-topology-svg g circle.warning { + stroke: #EC7A08; +} + +.pf-topology-svg g circle.unknown { + stroke: #bbb; +} + +.pf-topology-svg g text.glyph { + font-size: 20px; + fill: #1186C1; +} diff --git a/src/charts/topology/examples/topology-view.js b/src/charts/topology/examples/topology-view.js new file mode 100644 index 000000000..084e90a95 --- /dev/null +++ b/src/charts/topology/examples/topology-view.js @@ -0,0 +1,314 @@ +/** + * @ngdoc directive + * @name patternfly.charts.component:pfTopology + * @restrict E + * + * @description + * Component for rendering a topology chart. Individual nodes and relationships can be represented with this view. CSS is especially important for rendering the nodes and lines. The example inline contains specific examples that can be used to change the icon size and the line type of the relationships. + * + * In addition; searching, filtering and label visibility is also supported.
+ * + * @param {object} items items to display in the topology chart, each is represented as an individual node. The keys of this object are used in the relations attribute. The items should have a item.kind attribute, as well as the usual item.metadata and so on.:
+ * + * + * @param {object} relations the object containing all of the node relationships:
+ * + * + * @param {object} icons The different icons to be used in the node representations + * @param {object} selection The item to be selected + * @param {object} force Optional. A D3 force layout to use instead of creating one by default. The force layout size will be updated, and layout will be started as appropriate. + * @param {object} nodes The node configuration for the various types of nodes
+ * @param {string} searchText Search text which is watched for changes and highlights the nodes matching the search text + * @param {object} kinds The different kinds of nodes represented in the topology chart + * @param {function (vertices, added) } chartRendered The argument will be D3 selection of elements that correspond to items. Each item has its data set to one of the items. The default implementation of this event sets the title from Kubernetes metadata and tweaks the look of for certain statuses. Use event.preventDefault() to prevent this default behavior. + * @param {boolean} itemSelected A function that is dispatched when an item is selected (along with the node data associated with the function + * @param {boolean} showLabels A watched boolean that determines whether or not lables should be displayed beneath the nodes + * @param {function (node) } tooltipFunction A passed in tooltip function which can be used to overwrite the default tooltip behavior + * + * @example + + +
+ + + +
+ + + + + + +
+
+
+ + + angular.module( 'patternfly.charts' ).controller( 'TopologyCtrl', function( $scope, $rootScope ) { + var index = 0; + var datasets = []; + + function sink(dataset) { + datasets.push(dataset); + } + + sink({ + "items": { + "ContainerManager10r20": { + "name": "ocp-master.example.com", + "kind": "ContainerManager", + "miq_id": 10000000000020, + "status": "Valid", + "display_kind": "OpenshiftEnterprise" + }, + "ContainerNode10r14": { + "name": "ocp-master.example.com", + "kind": "ContainerNode", + "miq_id": 10000000000014, + "status": "Ready", + "display_kind": "Node" + }, + "ContainerGroup10r240": { + "name": "docker-registry-2-vrguw", + "kind": "ContainerGroup", + "miq_id": 10000000000240, + "status": "Running", + "display_kind": "Pod" + }, + "Container10r235": { + "name": "registry", + "kind": "Container", + "miq_id": 10000000000235, + "status": "Error", + "display_kind": "Container" + }, + "ContainerReplicator10r56": { + "name": "docker-registry-2", + "kind": "ContainerReplicator", + "miq_id": 10000000000056, + "status": "OK", + "display_kind": "Replicator" + }, + "ContainerService10r61": { + "name": "docker-registry", + "kind": "ContainerService", + "miq_id": 10000000000061, + "status": "Unknown", + "display_kind": "Service" + }, + }, + "relations": [ + { + "source": "ContainerManager10r20", + "target": "ContainerNode10r14" + }, { + "source": "ContainerNode10r14", + "target": "ContainerGroup10r240" + }, { + "source": "ContainerGroup10r240", + "target": "Container10r235" + }, { + "source": "ContainerGroup10r240", + "target": "ContainerReplicator10r56" + }, { + "source": "ContainerGroup10r240", + "target": "ContainerService10r61" + }, { + "source": "ContainerNode10r14", + "target": "ContainerGroup10r241" + }, { + "source": "ContainerGroup10r241", + "target": "Container10r236" + }, { + "source": "ContainerGroup10r241", + "target": "ContainerReplicator10r57" + } + ], + "icons": { + "AvailabilityZone": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerReplicator": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerGroup": { + "type": "glyph", + "icon": "", + "fontfamily": "FontAwesome" + }, + "ContainerNode": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerService": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerRoute": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "Container": { + "type": "glyph", + "icon": "", + "fontfamily": "FontAwesome" + }, + "Host": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "Vm": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerManager": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + } + }, + }); + + $rootScope.data = datasets[index]; + + var nodeKinds = { + "ContainerReplicator": true, + "ContainerGroup": true, + "Container": true, + "ContainerNode": true, + "ContainerService": true, + "Host": true, + "Vm": true, + "ContainerRoute": true, + "ContainerManager": true + }; + + $rootScope.kinds = nodeKinds; + + var icons = $rootScope.data.icons; + $scope.nodes = {}; + for(var kind in nodeKinds) { + var icon = icons[kind]; + $scope.nodes[kind] = { + "name": kind, + "enabled": nodeKinds[kind], + "radius": 16, + "textX": 0, + "textY": 5, + "height": 18, + "width": 18, + "icon": icon.icon, + "fontFamily": icon.fontfamily + }; + } + + // Individual values can also be set for specific icons + $scope.nodes.ContainerService.textY = 9; + $scope.nodes.ContainerService.textX = -1; + + $scope.nodes.ContainerGroup.height = 30; + $scope.nodes.ContainerGroup.width = 30; + $scope.nodes.ContainerGroup.radius = 28; + $scope.nodes.ContainerGroup.textY = 8; + + $scope.itemSelected = function (item) { + var text = ""; + if (item) { + text = "Selected: " + item.name; + } + angular.element(document.getElementById("selected")).text(text); + }; + + $scope.removeKind = function () { + if($rootScope.kinds.ContainerNode) { + delete $rootScope.kinds.ContainerNode; + } + }; + + $scope.tooltip = function (node) { + var status = [ + 'Name: ' + node.item.name, + 'Type: ' + node.item.kind, + 'Status: ' + node.item.status + ]; + return status; + } + }); + + + + .pf-topology-svg g.Pod text { + font-family: FontAwesome; + font-size: 16px; + fill: #1186C1; + } + + .pf-topology-svg g.Node text { + fill: #636363; + } + + .pf-topology-svg g.Service text { + fill: #ff7f0e; + } + + .pf-topology-svg g.ReplicationController text { + fill: #9467bd; + font-size: 20px; + } + + .pf-topology-svg line.ReplicationControllerPod { + stroke-linecap: round; + stroke-dasharray: 5, 2; + } + + + .pf-topology-svg line.ContainerServiceContainerGroup, .pf-topology-svg line.ContainerReplicatorContainerGroup, .pf-topology-svg line.ContainerServiceContainerRoute, + .pf-topology-svg line.ContainerGroupContainerService, .pf-topology-svg line.ContainerGroupContainerReplicator { + stroke-linecap: round; + stroke-dasharray: 5.5; + } + + + .pf-topology-svg g.Container text.glyph { + font-size: 18px; + } + + .pf-topology-svg g.ContainerGroup text.glyph { + font-size: 28px; + } + + .pf-topology-svg g.Vm text.glyph, .pf-topology-svg g.Host text.glyph { + fill: #636363; + } + + .pf-topology-svg g.ContainerNode text.glyph { + font-size: 18px; + } + + .pf-topology-svg g.ContainerManager text.glyph { + font-size: 18px; + } + + +
+ */ diff --git a/src/charts/topology/topology.component.js b/src/charts/topology/topology.component.js new file mode 100644 index 000000000..f9fb38e51 --- /dev/null +++ b/src/charts/topology/topology.component.js @@ -0,0 +1,625 @@ +angular.module('patternfly.charts').component('pfTopology', { + bindings: { + items: '<', + relations: '<', + kinds: '<', + icons: '<', + selection: '<', + force: '<', + radius: '<', + nodes: '<', + searchText: ' 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) { + if (item !== undefined) { + selection = item; + } + svg.selectAll("g") + .classed("selected", function (d) { + return d.item === selection; + }); + } + + 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 () { + var added; + + 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(); + + 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; + var item, id, kind, node; + var i, len, relation, s, t; + /* The actual data for the graph */ + nodes = []; + links = []; + lookup = {}; + + for (id in items) { + if (id) { + 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); + } + } + 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(); + } + return d3.select(); + } + + function resized () { + window.clearTimeout(timeout); + timeout = window.setTimeout(adjust, 1); + } + + window.addEventListener('resize', resized); + + adjust(); + resized(); + + return { + select: select, + kinds: function (value) { + var added; + kinds = value; + added = digest(); + return [vertices, added]; + }, + data: function (newItems, newRelations) { + var added; + items = newItems || {}; + relations = newRelations || []; + added = digest(); + return [vertices, added]; + }, + close: function () { + var id, node; + 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. + */ + cache = {}; + for (id in lookup) { + if (id) { + node = nodes[lookup[id]]; + delete node.item; + cache[id] = node; + } + } + + nodes = []; + lookup = {}; + } + }; + } + + function search (query) { + var svg = getSVG(); + var nodes = svg.selectAll("g"); + var selected, links; + if (query !== "") { + selected = nodes.filter(function (d) { + return d.item.name.indexOf(query) === -1; + }); + selected.style("opacity", "0.2"); + links = svg.selectAll("line"); + links.style("opacity", "0.2"); + } + } + + function resetSearch (d3) { + // Display all topology nodes and links + d3.selectAll("g, line").transition() + .duration(2000) + .style("opacity", 1); + } + + function toggleLabelVisibility () { + if (ctrl.showLabels) { + vs.selectAll("text.attached-label") + .classed("visible", true); + } else { + vs.selectAll("text.attached-label") + .classed("visible", false); + } + } + + function getSVG () { + var graph = d3.select("pf-topology"); + var svg = graph.select('svg'); + return svg; + } + + function notify (item) { + ctrl.itemSelected({item: item}); + if ($attrs.selection === undefined) { + graph.select(item); + } + } + + function icon (d) { + return '#' + d.item.kind; + } + + function title (d) { + return d.item.name; + } + + function render (args) { + var vertices = args[0]; + var added = args[1]; + var event; + + // allow custom rendering of chart + if (angular.isFunction(ctrl.chartRendered)) { + event = ctrl.chartRendered({vertices: vertices, added: added}); + } + + if (!event || !event.defaultPrevented) { + added.attr("class", function (d) { + return d.item.kind; + }); + + added.append("circle") + .attr("r", function (d) { + return getDimensions(d).r; + }) + .attr('class', function (d) { + return getItemStatusClass(d); + }) + .on("contextmenu", function (d) { + contextMenu(ctrl, d); + }); + + added.append("title"); + + added.on("dblclick", function (d) { + return dblclick(d); + }); + + added.append("image") + .attr("xlink:href", function (d) { + // overwrite this . . . + var iconInfo = ctrl.icons[d.item.kind]; + switch (iconInfo.type) { + case 'image': + return iconInfo.icon; + case "glyph": + return null; + } + }) + .attr("height", function (d) { + var iconInfo = ctrl.icons[d.item.kind]; + if (iconInfo.type !== 'image') { + return 0; + } + return 40; + }) + .attr("width", function (d) { + var iconInfo = ctrl.icons[d.item.kind]; + if (iconInfo.type !== 'image') { + return 0; + } + return 40; + }) + .attr("y", function (d) { + return getDimensions(d).y; + }) + .attr("x", function (d) { + return getDimensions(d).x; + }) + .on("contextmenu", function (d) { + contextMenu(ctrl, d); + }); + + added.append("text") + .each(function (d) { + var iconInfo = ctrl.icons[d.item.kind]; + if (iconInfo.type !== 'glyph') { + return; + } + d3.select(this).text(iconInfo.icon) + .attr("class", "glyph") + .attr('font-family', iconInfo.fontfamily); + }) + + .attr("y", function (d) { + return getDimensions(d).y; + }) + .attr("x", function (d) { + return getDimensions(d).x; + }) + .on("contextmenu", function (d) { + contextMenu(this, d); + }); + + + added.append("text") + .attr("x", 26) + .attr("y", 24) + .text(function (d) { + return d.item.name; + }) + .attr('class', function () { + var className = "attached-label"; + if (ctrl.showLabels) { + return className + ' visible'; + } + return className; + }); + + added.selectAll("title").text(function (d) { + return tooltip(d).join("\n"); + }); + + vs = vertices; + } + graph.select(); + } + + function tooltip (d) { + if (ctrl.tooltipFunction) { + return ctrl.tooltipFunction({node: d}); + } + return 'Name: ' + d.item.name; + } + + function removeContextMenu () { + d3.event.preventDefault(); + d3.select('.popup').remove(); + contextMenuShowing = false; + } + + function contextMenu (that, data) { + var canvasSize, popupSize, canvas, mousePosition, popup; + + if (contextMenuShowing) { + removeContextMenu(); + } else { + d3.event.preventDefault(); + + canvas = d3.select('pf-topology'); + mousePosition = d3.mouse(canvas.node()); + + popup = canvas.append('div') + .attr('class', 'popup') + .style('left', mousePosition[0] + 'px') + .style('top', mousePosition[1] + 'px'); + popup.append('h5').text('Actions on ' + data.item.kind); + + buildContextMenuOptions(popup, data); + + canvasSize = [ + canvas.node().offsetWidth, + canvas.node().offsetHeight + ]; + + popupSize = [ + popup.node().offsetWidth, + popup.node().offsetHeight + ]; + + if (popupSize[0] + mousePosition[0] > canvasSize[0]) { + popup.style('left', 'auto'); + popup.style('right', 0); + } + + if (popupSize[1] + mousePosition[1] > canvasSize[1]) { + popup.style('top', 'auto'); + popup.style('bottom', 0); + } + contextMenuShowing = !contextMenuShowing; + } + } + + function buildContextMenuOptions (popup, data) { + if (data.item.kind === 'Tag') { + return false; + } + addContextMenuOption(popup, 'Go to summary page', data, dblclick); + } + + function dblclick (d) { + window.location.assign(d.url); + } + + function addContextMenuOption (popup, text, data, callback) { + popup.append('p').text(text).on('click', function () { + callback(data); + }); + } + + function getDimensions (d) { + var nodeEntry = ctrl.nodes[d.item.kind]; + var defaultDimensions = defaultElementDimensions(); + if (nodeEntry) { + if (nodeEntry.textX) { + defaultDimensions.x = nodeEntry.textX; + } + if (nodeEntry.textY) { + defaultDimensions.y = nodeEntry.textY; + } + + if (nodeEntry.radius) { + defaultDimensions.r = nodeEntry.radius; + } + } + return defaultDimensions; + } + + function defaultElementDimensions () { + return { x: 0, y: 9, r: 17 }; + } + + function getItemStatusClass (d) { + switch (d.item.status.toLowerCase()) { + case "ok": + case "active": + case "available": + case "on": + case "ready": + case "running": + case "succeeded": + case "valid": + return "success"; + case "notready": + case "failed": + case "error": + case "unreachable": + return "error"; + case 'warning': + case 'waiting': + case 'pending': + return "warning"; + case 'unknown': + case 'terminated': + return "unknown"; + } + } + } +}); diff --git a/test/charts/topology/topology.spec.js b/test/charts/topology/topology.spec.js new file mode 100644 index 000000000..6b1855376 --- /dev/null +++ b/test/charts/topology/topology.spec.js @@ -0,0 +1,255 @@ + +describe('Component: pfTopology', function() { + var $scope, $compile, $timeout; + + beforeEach(module( + 'patternfly.charts' + )); + + beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_) { + $compile = _$compile_; + $scope = _$rootScope_; + $timeout = _$timeout_; + })); + + var compileTopology = function (markup, scope) { + var el = $compile(markup)(scope); + scope.$digest(); + return angular.element(el); + }; + + beforeEach(function() { + var index = 0; + var datasets = []; + + function sink(dataset) { + datasets.push(dataset); + } + + sink({ + "items": { + "ContainerManager10r20": { + "name": "ocp-master.example.com", + "kind": "ContainerManager", + "miq_id": 10000000000020, + "status": "Valid", + "display_kind": "OpenshiftEnterprise" + }, + "ContainerNode10r14": { + "name": "ocp-master.example.com", + "kind": "ContainerNode", + "miq_id": 10000000000014, + "status": "Ready", + "display_kind": "Node" + }, + "ContainerGroup10r240": { + "name": "docker-registry-2-vrguw", + "kind": "ContainerGroup", + "miq_id": 10000000000240, + "status": "Running", + "display_kind": "Pod" + }, + "Container10r235": { + "name": "registry", + "kind": "Container", + "miq_id": 10000000000235, + "status": "Running", + "display_kind": "Container" + }, + "ContainerReplicator10r56": { + "name": "docker-registry-2", + "kind": "ContainerReplicator", + "miq_id": 10000000000056, + "status": "OK", + "display_kind": "Replicator" + }, + "ContainerService10r61": { + "name": "docker-registry", + "kind": "ContainerService", + "miq_id": 10000000000061, + "status": "Unknown", + "display_kind": "Service" + }, + }, + "relations": [ + { + "source": "ContainerManager10r20", + "target": "ContainerNode10r14" + }, { + "source": "ContainerNode10r14", + "target": "ContainerGroup10r240" + }, { + "source": "ContainerGroup10r240", + "target": "Container10r235" + }, { + "source": "ContainerGroup10r240", + "target": "ContainerReplicator10r56" + }, { + "source": "ContainerGroup10r240", + "target": "ContainerService10r61" + }, { + "source": "ContainerNode10r14", + "target": "ContainerGroup10r241" + } + ], + "icons": { + "AvailabilityZone": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerReplicator": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerGroup": { + "type": "glyph", + "icon": "", + "fontfamily": "FontAwesome" + }, + "ContainerNode": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerService": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerRoute": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "Container": { + "type": "glyph", + "icon": "", + "fontfamily": "FontAwesome" + }, + "Host": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "Vm": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "ContainerManager": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + } + } + }); + + $scope.data = datasets[index]; + + var nodeKinds = { + "ContainerReplicator": true, + "ContainerGroup": true, + "Container": true, + "ContainerNode": true, + "ContainerService": true, + "Host": true, + "Vm": true, + "ContainerRoute": true, + "ContainerManager": true + }; + + $scope.kinds = nodeKinds; + + var icons = $scope.data.icons; + $scope.nodes = {}; + for(var kind in nodeKinds) { + var icon = icons[kind]; + $scope.nodes[kind] = { + "name": kind, + "enabled": nodeKinds[kind], + "radius": 16, + "textX": 0, + "textY": 5, + "height": 18, + "width": 18, + "icon": icon.icon, + "fontFamily": icon.fontfamily + }; + } + + // Individual values can also be set for specific icons + $scope.nodes.ContainerService.textY = 9; + $scope.nodes.ContainerService.textX = -1; + + $scope.itemSelected = function (item) { + var text = ""; + if (item) { + text = "Selected: " + item.name; + } + angular.element(document.getElementById("selected")).text(text); + }; + + $scope.tooltip = function (node) { + var status = [ + 'Name: ' + node.item.name, + 'Type: ' + node.item.kind, + 'Status: ' + node.item.status + ]; + return status; + }; + }); + + afterEach(function() { + d3.selectAll('pf-topology').remove(); + }); + + it("should create an svg internally", function() { + var element = compileTopology('
', $scope); + expect(element.find('svg')).not.toBe(undefined); + }); + + it("should generate 6 nodes", function () { + var elementDiv = ''; + var body = angular.element(document.body); + body.append(elementDiv); + compileTopology(body, $scope); + var topologyElement = body.find('pf-topology svg g'); + expect(topologyElement.length).toBe(6); + }); + + it("should generate 5 lines", function () { + var elementDiv = ''; + var body = angular.element(document.body); + body.append(elementDiv); + compileTopology(body, $scope); + var topologyElement = body.find('pf-topology svg line'); + expect(topologyElement.length).toBe(5); + }); + + it("should hide/show the text labels", function () { + var elementDiv = ''; + var body = angular.element(document.body); + body.append(elementDiv); + compileTopology(body, $scope); + var topologyElement = body.find('pf-topology svg'); + expect(topologyElement.find('text.visible').length).toBe(0); + $scope.showLabels = true; + $scope.$digest(); + expect(topologyElement.find('text.visible').length).toBe(6); + }); + + it("should update view on search", function () { + var elementDiv = ''; + var body = angular.element(document.body); + body.append(elementDiv); + compileTopology(body, $scope); + var disabledNodes = body.find('g[style="opacity: 0.2;"]') + expect(disabledNodes.length).toBe(0); + $scope.searchText = 'vrguw'; + $scope.$digest(); + disabledNodes = body.find('g[style="opacity: 0.2;"]') + expect(disabledNodes.length).toBe(5); + }); +});