From 5133bbb5a98f5c6072d80d33bb1d6efa870364c9 Mon Sep 17 00:00:00 2001 From: Dana Gutride Date: Fri, 27 Jan 2017 16:59:48 -0500 Subject: [PATCH 1/5] Add topology view component, example and css --- src/charts/charts.less | 168 +++++ src/charts/topology/examples/topology-view.js | 254 +++++++ src/charts/topology/topology.component.js | 630 ++++++++++++++++++ 3 files changed, 1052 insertions(+) create mode 100644 src/charts/topology/examples/topology-view.js create mode 100644 src/charts/topology/topology.component.js diff --git a/src/charts/charts.less b/src/charts/charts.less index 62ba8f891..fccb2b9fc 100644 --- a/src/charts/charts.less +++ b/src/charts/charts.less @@ -186,3 +186,171 @@ 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.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 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 line.ReplicationControllerPod { + stroke-linecap: round; + stroke-dasharray: 5, 2; +} + +.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 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 text.glyph { + font-size: 20px; + fill: #1186C1; +} + +.pf-topology-svg g.Container text.glyph { + font-size: 18px; +} + +.pf-topology-svg g.ContainerGroup text.glyph { + font-size: 18px; +} + +.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/examples/topology-view.js b/src/charts/topology/examples/topology-view.js new file mode 100644 index 000000000..bcefacb21 --- /dev/null +++ b/src/charts/topology/examples/topology-view.js @@ -0,0 +1,254 @@ +/** + * @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. + * + * 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": "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" + }, { + "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.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; + } + }); + +
+ */ diff --git a/src/charts/topology/topology.component.js b/src/charts/topology/topology.component.js new file mode 100644 index 000000000..61631eb4f --- /dev/null +++ b/src/charts/topology/topology.component.js @@ -0,0 +1,630 @@ +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) { + 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"; + } + } + } +}); From deb7437720bd2b97e4d97d427cc0d248020e3ec8 Mon Sep 17 00:00:00 2001 From: Dana Gutride Date: Fri, 27 Jan 2017 17:00:00 -0500 Subject: [PATCH 2/5] Add topology unit tests for new chart --- test/charts/topology/topology.spec.js | 255 ++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 test/charts/topology/topology.spec.js 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); + }); +}); From 768afa8f7898d60ed8535107a2a882a1ca24f71f Mon Sep 17 00:00:00 2001 From: Dana Gutride Date: Mon, 30 Jan 2017 08:34:33 -0500 Subject: [PATCH 3/5] Move sample specific css out of angular-patternfly less (so it won't ship with it). --- src/charts/charts.less | 54 +----------------- src/charts/topology/examples/topology-view.js | 57 ++++++++++++++++++- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/src/charts/charts.less b/src/charts/charts.less index fccb2b9fc..876fcaf25 100644 --- a/src/charts/charts.less +++ b/src/charts/charts.less @@ -192,7 +192,6 @@ pf-topology { user-select: none; } - .container-topology pf-topology { height: 500px; position: relative; @@ -227,8 +226,9 @@ pf-topology { cursor: pointer; } + .container-topology label.checkbox-inline { - font-size:14px; + font-size: 14px; } .pf-topology-svg g { @@ -247,25 +247,6 @@ pf-topology { opacity: .6; } -.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 g circle { stroke: #aaa; fill: #fff; @@ -285,11 +266,6 @@ pf-topology { stroke-width: 1; } -.pf-topology-svg line.ReplicationControllerPod { - stroke-linecap: round; - stroke-dasharray: 5, 2; -} - .pf-topology-svg g text.attached-label { display: none; } @@ -324,33 +300,7 @@ pf-topology { stroke: #bbb; } -.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 text.glyph { font-size: 20px; fill: #1186C1; } - -.pf-topology-svg g.Container text.glyph { - font-size: 18px; -} - -.pf-topology-svg g.ContainerGroup text.glyph { - font-size: 18px; -} - -.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/examples/topology-view.js b/src/charts/topology/examples/topology-view.js index bcefacb21..abacfec40 100644 --- a/src/charts/topology/examples/topology-view.js +++ b/src/charts/topology/examples/topology-view.js @@ -4,7 +4,7 @@ * @restrict E * * @description - * Component for rendering a topology chart. Individual nodes and relationships can be represented with this view. + * Component for rendering a topology chart. Individual nodes and relationships can be represented with this view. CSS is especially important for rendering the noes 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.
* @@ -250,5 +250,60 @@ } }); + + + .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: 18px; + } + + .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; + } + + */ From f8b2bea7af0bd23c814b3a566809661b42df2392 Mon Sep 17 00:00:00 2001 From: Dana Gutride Date: Mon, 30 Jan 2017 10:55:17 -0500 Subject: [PATCH 4/5] Update to one-way bindings and fix example to provide better samples and descriptions --- src/charts/topology/examples/topology-view.js | 11 +++++++--- src/charts/topology/topology.component.js | 20 ++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/charts/topology/examples/topology-view.js b/src/charts/topology/examples/topology-view.js index abacfec40..5a7beb955 100644 --- a/src/charts/topology/examples/topology-view.js +++ b/src/charts/topology/examples/topology-view.js @@ -12,7 +12,7 @@ *
    *
  • .name - name of the item the node represents *
  • .status - optional status of the node (can be used to differentiate the circle color) - *
  • .kind - the kind of node + *
  • .kind - the kind of node - this is a general key that needs to be unique for grouping the nodes Filtering and styles use this value as well to correctly select the nodes. *
* * @param {object} relations the object containing all of the node relationships:
@@ -90,7 +90,7 @@ "name": "registry", "kind": "Container", "miq_id": 10000000000235, - "status": "Running", + "status": "Error", "display_kind": "Container" }, "ContainerReplicator10r56": { @@ -226,6 +226,11 @@ $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) { @@ -289,7 +294,7 @@ } .pf-topology-svg g.ContainerGroup text.glyph { - font-size: 18px; + font-size: 28px; } .pf-topology-svg g.Vm text.glyph, .pf-topology-svg g.Host text.glyph { diff --git a/src/charts/topology/topology.component.js b/src/charts/topology/topology.component.js index 61631eb4f..c7ffa76ae 100644 --- a/src/charts/topology/topology.component.js +++ b/src/charts/topology/topology.component.js @@ -1,12 +1,12 @@ angular.module('patternfly.charts').component('pfTopology', { bindings: { - items: '=', - relations: '=', - kinds: '=', - icons: '=', - selection: '=', - force: '=', - radius: '=', + items: '<', + relations: '<', + kinds: '<', + icons: '<', + selection: '<', + force: '<', + radius: '<', nodes: '<', searchText: ' Date: Mon, 30 Jan 2017 13:49:15 -0500 Subject: [PATCH 5/5] Changes from code review. . . --- src/charts/topology/examples/topology-view.js | 66 +++++++++---------- src/charts/topology/topology.component.js | 37 +++++------ 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/src/charts/topology/examples/topology-view.js b/src/charts/topology/examples/topology-view.js index 5a7beb955..084e90a95 100644 --- a/src/charts/topology/examples/topology-view.js +++ b/src/charts/topology/examples/topology-view.js @@ -4,7 +4,7 @@ * @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 noes and lines. The example inline contains specific examples that can be used to change the icon size and the line type of the relationships. + * 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.
* @@ -172,11 +172,11 @@ "fontfamily": "FontAwesome" }, "Host": { - "type": "glyph", - "icon": "", - "fontfamily": "PatternFlyIcons-webfont" - }, - "Vm": { + "type": "glyph", + "icon": "", + "fontfamily": "PatternFlyIcons-webfont" + }, + "Vm": { "type": "glyph", "icon": "", "fontfamily": "PatternFlyIcons-webfont" @@ -186,41 +186,41 @@ "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 - }; + "ContainerReplicator": true, + "ContainerGroup": true, + "Container": true, + "ContainerNode": true, + "ContainerService": true, + "Host": true, + "Vm": true, + "ContainerRoute": true, + "ContainerManager": true + }; - $rootScope.kinds = nodeKinds; + $rootScope.kinds = nodeKinds; - var icons = $rootScope.data.icons; + 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 - }; - } + 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; diff --git a/src/charts/topology/topology.component.js b/src/charts/topology/topology.component.js index c7ffa76ae..f9fb38e51 100644 --- a/src/charts/topology/topology.component.js +++ b/src/charts/topology/topology.component.js @@ -28,7 +28,6 @@ angular.module('patternfly.charts').component('pfTopology', { ctrl.$onInit = function () { $element.css("display", "block"); options = {"force": ctrl.force, "radius": ctrl.radius}; - //graph = topologyGraph($element[0], notify, options); ctrl.showLabels = false; $element.on("$destroy", function () { @@ -598,27 +597,27 @@ angular.module('patternfly.charts').component('pfTopology', { } function getItemStatusClass (d) { - switch (d.item.status) { - case "OK": - case "Active": - case "Available": - case "On": - case "Ready": - case "Running": - case "Succeeded": - case "Valid": + 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": + case "notready": + case "failed": + case "error": + case "unreachable": return "error"; - case 'Warning': - case 'Waiting': - case 'Pending': + case 'warning': + case 'waiting': + case 'pending': return "warning"; - case 'Unknown': - case 'Terminated': + case 'unknown': + case 'terminated': return "unknown"; } }