Make the topo viz denser when there are >50 nodes

By hiding node details and making nodes interactive instead, we can pack
more allocations on a screen.
This commit is contained in:
Michael Lange 2020-09-29 16:46:56 -07:00
parent 6e55d8a6eb
commit 066502d408
8 changed files with 154 additions and 10 deletions

View File

@ -13,6 +13,7 @@ export default class TopoViz extends Component {
@tracked element = null;
@tracked topology = { datacenters: [] };
@tracked activeNode = null;
@tracked activeAllocation = null;
@tracked activeEdges = [];
@ -36,6 +37,12 @@ export default class TopoViz extends Component {
return !this.isSingleColumn || (this.isSingleColumn && this.args.nodes.length <= 20);
}
// Once a cluster is large enough, the exact details of a node are
// typically irrelevant and a waste of space.
get isDense() {
return this.args.nodes.length > 50;
}
dataForNode(node) {
return {
node,
@ -135,6 +142,21 @@ export default class TopoViz extends Component {
this.element = element;
}
@action
showNodeDetails(node) {
if (this.activeNode) {
set(this.activeNode, 'isSelected', false);
}
this.activeNode = this.activeNode === node ? null : node;
if (this.activeNode) {
set(this.activeNode, 'isSelected', true);
}
if (this.args.onNodeSelect) this.args.onNodeSelect(this.activeNode);
}
@action
associateAllocations(allocation) {
if (this.activeAllocation === allocation) {
@ -151,6 +173,10 @@ export default class TopoViz extends Component {
set(this.topology, 'selectedKey', null);
}
} else {
if (this.activeNode) {
set(this.activeNode, 'isSelected', false);
}
this.activeNode = null;
this.activeAllocation = allocation;
const selectedAllocations = this.topology.allocationIndex[this.topology.selectedKey];
if (selectedAllocations) {
@ -171,6 +197,7 @@ export default class TopoViz extends Component {
}
if (this.args.onAllocationSelect)
this.args.onAllocationSelect(this.activeAllocation && this.activeAllocation.allocation);
if (this.args.onNodeSelect) this.args.onNodeSelect(this.activeNode);
}
@action

View File

@ -98,6 +98,13 @@ export default class TopoVizNode extends Component {
this.activeAllocation = null;
}
@action
selectNode() {
if (this.args.isDense && this.args.onNodeSelect) {
this.args.onNodeSelect(this.args.node.isSelected ? null : this.args.node);
}
}
@action
selectAllocation(allocation) {
if (this.args.onAllocationSelect) this.args.onAllocationSelect(allocation);

View File

@ -68,6 +68,31 @@ export default class TopologyControllers extends Controller {
});
}
@computed('activeNode')
get nodeUtilization() {
const node = this.activeNode;
const [formattedMemory, memoryUnits] = reduceToLargestUnit(node.memory * 1024 * 1024);
const totalReservedMemory = node.allocations
.mapBy('memory')
.reduce((sum, memory) => sum + (memory || 0), 0);
const totalReservedCPU = node.allocations
.mapBy('cpu')
.reduce((sum, cpu) => sum + (cpu || 0), 0);
return {
totalMemoryFormatted: formattedMemory.toFixed(2),
totalMemoryUnits: memoryUnits,
totalMemory: node.memory * 1024 * 1024,
totalReservedMemory: totalReservedMemory * 1024 * 1024,
reservedMemoryPercent: totalReservedMemory / node.memory,
totalCPU: node.cpu,
totalReservedCPU,
reservedCPUPercent: totalReservedCPU / node.cpu,
};
}
@computed('siblingAllocations.@each.node')
get uniqueActiveAllocationNodes() {
return this.siblingAllocations.mapBy('node').uniq();
@ -80,4 +105,9 @@ export default class TopologyControllers extends Controller {
allocation.reload();
}
}
@action
setNode(node) {
this.set('activeNode', node);
}
}

View File

@ -11,6 +11,17 @@
fill: $white-ter;
stroke-width: 1;
stroke: $grey-lighter;
&.is-interactive:hover {
fill: $white;
stroke: $grey-light;
}
&.is-selected,
&.is-selected:hover {
fill: $white;
stroke: $grey;
}
}
.dimension-background {
@ -42,6 +53,7 @@
alignment-baseline: central;
font-weight: $weight-normal;
fill: $grey;
pointer-events: none;
}
}

View File

@ -10,8 +10,10 @@
<TopoViz::Datacenter
@datacenter={{dc}}
@isSingleColumn={{this.datacenterIsSingleColumn}}
@isDense={{this.isDense}}
@heightScale={{this.topology.heightScale}}
@onAllocationSelect={{this.associateAllocations}}
@onNodeSelect={{this.showNodeDetails}}
@onLoad={{action reflow}}/>
</FlexMasonry>

View File

@ -10,8 +10,10 @@
<FlexMasonry @columns={{if @isSingleColumn 1 2}} @items={{@datacenter.nodes}} as |node|>
<TopoViz::Node
@node={{node}}
@isDense={{@isDense}}
@heightScale={{@heightScale}}
@onAllocationSelect={{@onAllocationSelect}} />
@onAllocationSelect={{@onAllocationSelect}}
@onNodeSelect={{@onNodeSelect}}/>
</FlexMasonry>
</div>
</div>

View File

@ -1,16 +1,18 @@
<div class="chart topo-viz-node {{unless this.allocations.length "is-empty"}}" {{did-insert this.reloadNode}}>
<p>
<strong>{{@node.node.name}}</strong>
<span class="bumper-left">{{this.count}} Allocs</span>
<span class="bumper-left is-faded">{{@node.memory}} MiB, {{@node.cpu}} Mhz</span>
</p>
{{#unless @isDense}}
<p>
<strong>{{@node.node.name}}</strong>
<span class="bumper-left">{{this.count}} Allocs</span>
<span class="bumper-left is-faded">{{@node.memory}} MiB, {{@node.cpu}} Mhz</span>
</p>
{{/unless}}
<svg class="chart" height="{{this.totalHeight}}px" {{did-insert this.render}} {{did-update this.updateRender}} {{window-resize this.render}}>
<defs>
<clipPath id="{{this.maskId}}">
<rect class="mask" x="0" y="0" width="{{this.dimensionsWidth}}px" height="{{this.maskHeight}}px" rx="2px" ry="2px" />
</clipPath>
</defs>
<rect class="node-background" width="100%" height="{{this.totalHeight}}px" rx="2px" ry="2px" />
<rect class="node-background {{if @node.isSelected "is-selected"}} {{if @isDense "is-interactive"}}" width="100%" height="{{this.totalHeight}}px" rx="2px" ry="2px" {{on "click" this.selectNode}} />
{{#if this.allocations.length}}
<g
class="dimensions {{if this.activeAllocation "is-active"}}"

View File

@ -24,9 +24,67 @@
</div>
</div>
<div class="boxed-section">
<div class="boxed-section-head">{{if this.activeAllocation "Allocation" "Cluster"}} Details</div>
<div class="boxed-section-head">
{{#if this.activeNode}}Client{{else if this.activeAllocation}}Allocation{{else}}Cluster{{/if}} Details
</div>
<div class="boxed-section-body">
{{#if this.activeAllocation}}
{{#if this.activeNode}}
<div class="dashboard-metric">
<p class="metric">{{this.activeNode.allocations.length}} <span class="metric-label">Allocations</span></p>
</div>
<div class="dashboard-metric">
<h3 class="pair">
<strong>Client:</strong>
<LinkTo @route="clients.client" @model={{this.activeNode.node}}>
{{this.activeNode.node.shortId}}
</LinkTo>
</h3>
<p><strong>Name:</strong> {{this.activeNode.node.name}}</p>
<p><strong>Address:</strong> {{this.activeNode.node.httpAddr}}</p>
</div>
<div class="dashboard-metric with-divider">
<p class="metric">{{this.nodeUtilization.totalMemoryFormatted}} <span class="metric-units">{{this.nodeUtilization.totalMemoryUnits}}</span> <span class="metric-label">of memory</span></p>
<div class="columns graphic">
<div class="column">
<div class="inline-chart" data-test-percentage-bar>
<progress
class="progress is-danger is-small"
value="{{this.nodeUtilization.reservedMemoryPercent}}"
max="1">
{{this.nodeUtilization.reservedMemoryPercent}}
</progress>
</div>
</div>
<div class="column is-minimum">
<span class="nowrap" data-test-percentage>{{format-percentage this.nodeUtilization.reservedMemoryPercent total=1}}</span>
</div>
</div>
<div class="annotation" data-test-absolute-value>
<strong>{{format-bytes this.nodeUtilization.totalReservedMemory}}</strong> / {{format-bytes this.nodeUtilization.totalMemory}} reserved
</div>
</div>
<div class="dashboard-metric">
<p class="metric">{{this.nodeUtilization.totalCPU}} <span class="metric-units">Mhz</span> <span class="metric-label">of CPU</span></p>
<div class="columns graphic">
<div class="column">
<div class="inline-chart" data-test-percentage-bar>
<progress
class="progress is-info is-small"
value="{{this.nodeUtilization.reservedCPUPercent}}"
max="1">
{{this.nodeUtilization.reservedCPUPercent}}
</progress>
</div>
</div>
<div class="column is-minimum">
<span class="nowrap" data-test-percentage>{{format-percentage this.nodeUtilization.reservedCPUPercent total=1}}</span>
</div>
</div>
<div class="annotation" data-test-absolute-value>
<strong>{{this.nodeUtilization.totalReservedCPU}} Mhz</strong> / {{this.nodeUtilization.totalCPU}} Mhz reserved
</div>
</div>
{{else if this.activeAllocation}}
<div class="dashboard-metric">
<h3 class="pair">
<strong>Allocation:</strong>
@ -121,7 +179,11 @@
</div>
</div>
<div class="column">
<TopoViz @nodes={{this.model.nodes}} @allocations={{this.model.allocations}} @onAllocationSelect={{action this.setAllocation}} />
<TopoViz
@nodes={{this.model.nodes}}
@allocations={{this.model.allocations}}
@onAllocationSelect={{action this.setAllocation}}
@onNodeSelect={{action this.setNode}} />
</div>
</div>
</section>