diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index eea9b2c84f9e..6b9e98e7228d 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -2,7 +2,6 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action, set } from '@ember/object'; import { run } from '@ember/runloop'; -import { task } from 'ember-concurrency'; import { scaleLinear } from 'd3-scale'; import { extent, deviation, mean } from 'd3-array'; import { line, curveBasis } from 'd3-shape'; diff --git a/ui/app/components/topo-viz/datacenter.js b/ui/app/components/topo-viz/datacenter.js index 7ebe7f66b898..0750fc1eb08c 100644 --- a/ui/app/components/topo-viz/datacenter.js +++ b/ui/app/components/topo-viz/datacenter.js @@ -1,10 +1,12 @@ import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; export default class TopoVizDatacenter extends Component { - @tracked scheduledAllocations = []; - @tracked aggregatedNodeResources = { cpu: 0, memory: 0 }; + get scheduledAllocations() { + return this.args.datacenter.nodes.reduce( + (all, node) => all.concat(node.allocations.filterBy('allocation.isScheduled')), + [] + ); + } get aggregatedAllocationResources() { return this.scheduledAllocations.reduce( @@ -17,14 +19,8 @@ export default class TopoVizDatacenter extends Component { ); } - @action - loadAllocations() { - this.scheduledAllocations = this.args.datacenter.nodes.reduce( - (all, node) => all.concat(node.allocations.filterBy('allocation.isScheduled')), - [] - ); - - this.aggregatedNodeResources = this.args.datacenter.nodes.reduce( + get aggregatedNodeResources() { + return this.args.datacenter.nodes.reduce( (totals, node) => { totals.cpu += node.cpu; totals.memory += node.memory; @@ -32,7 +28,5 @@ export default class TopoVizDatacenter extends Component { }, { cpu: 0, memory: 0 } ); - - this.args.onLoad && this.args.onLoad(); } } diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index 1722842d5463..7a367a3a5c89 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -2,15 +2,14 @@ + @withSpacing={{true}} as |dc|> + @onNodeSelect={{this.showNodeDetails}} /> {{#if this.activeAllocation}} diff --git a/ui/app/templates/components/topo-viz/datacenter.hbs b/ui/app/templates/components/topo-viz/datacenter.hbs index 2798fbe91c8f..0a50a1f5a8d1 100644 --- a/ui/app/templates/components/topo-viz/datacenter.hbs +++ b/ui/app/templates/components/topo-viz/datacenter.hbs @@ -1,5 +1,5 @@ -
-
+
+
{{@datacenter.name}} {{this.scheduledAllocations.length}} Allocs {{@datacenter.nodes.length}} Nodes diff --git a/ui/app/templates/components/topo-viz/node.hbs b/ui/app/templates/components/topo-viz/node.hbs index 0e4eb98cd70d..fc02614a23c9 100644 --- a/ui/app/templates/components/topo-viz/node.hbs +++ b/ui/app/templates/components/topo-viz/node.hbs @@ -1,4 +1,4 @@ -
+
{{#unless @isDense}}

{{#if @node.node.isDraining}} diff --git a/ui/tests/integration/components/topo-viz/datacenter-test.js b/ui/tests/integration/components/topo-viz/datacenter-test.js new file mode 100644 index 000000000000..3480aae88141 --- /dev/null +++ b/ui/tests/integration/components/topo-viz/datacenter-test.js @@ -0,0 +1,160 @@ +import { find } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; +import { create } from 'ember-cli-page-object'; +import sinon from 'sinon'; +import faker from 'nomad-ui/mirage/faker'; +import topoVizDatacenterPageObject from 'nomad-ui/tests/pages/components/topo-viz/datacenter'; + +const TopoVizDatacenter = create(topoVizDatacenterPageObject()); + +const nodeGen = (name, datacenter, memory, cpu, allocations = []) => ({ + datacenter, + memory, + cpu, + node: { name }, + allocations: allocations.map(alloc => ({ + memory: alloc.memory, + cpu: alloc.cpu, + memoryPercent: alloc.memory / memory, + cpuPercent: alloc.cpu / cpu, + allocation: { + id: faker.random.uuid(), + isScheduled: true, + }, + })), +}); + +// Used in Array#reduce to sum by a property common to an array of objects +const sumBy = prop => (sum, obj) => (sum += obj[prop]); + +module('Integration | Component | TopoViz::Datacenter', function(hooks) { + setupRenderingTest(hooks); + + const commonProps = props => ({ + isSingleColumn: true, + isDense: false, + heightScale: () => 50, + onAllocationSelect: sinon.spy(), + onNodeSelect: sinon.spy(), + ...props, + }); + + const commonTemplate = hbs` + + `; + + test('presents as a div with a label and a FlexMasonry with a collection of nodes', async function(assert) { + this.setProperties( + commonProps({ + datacenter: { + name: 'dc1', + nodes: [nodeGen('node-1', 'dc1', 1000, 500)], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(TopoVizDatacenter.isPresent); + assert.equal(TopoVizDatacenter.nodes.length, this.datacenter.nodes.length); + + await componentA11yAudit(this.element, assert); + }); + + test('datacenter stats are an aggregate of node stats', async function(assert) { + this.setProperties( + commonProps({ + datacenter: { + name: 'dc1', + nodes: [ + nodeGen('node-1', 'dc1', 1000, 500, [ + { memory: 100, cpu: 300 }, + { memory: 200, cpu: 50 }, + ]), + nodeGen('node-2', 'dc1', 1500, 100, [ + { memory: 50, cpu: 80 }, + { memory: 100, cpu: 20 }, + ]), + nodeGen('node-3', 'dc1', 2000, 300), + nodeGen('node-4', 'dc1', 3000, 200), + ], + }, + }) + ); + + await this.render(commonTemplate); + + const allocs = this.datacenter.nodes.reduce( + (allocs, node) => allocs.concat(node.allocations), + [] + ); + const memoryReserved = allocs.reduce(sumBy('memory'), 0); + const cpuReserved = allocs.reduce(sumBy('cpu'), 0); + const memoryTotal = this.datacenter.nodes.reduce(sumBy('memory'), 0); + const cpuTotal = this.datacenter.nodes.reduce(sumBy('cpu'), 0); + + assert.ok(TopoVizDatacenter.label.includes(this.datacenter.name)); + assert.ok(TopoVizDatacenter.label.includes(`${this.datacenter.nodes.length} Nodes`)); + assert.ok(TopoVizDatacenter.label.includes(`${allocs.length} Allocs`)); + assert.ok(TopoVizDatacenter.label.includes(`${memoryReserved}/${memoryTotal} MiB`)); + assert.ok(TopoVizDatacenter.label.includes(`${cpuReserved}/${cpuTotal} Mhz`)); + }); + + test('when @isSingleColumn is true, the FlexMasonry layout gets one column, otherwise it gets two', async function(assert) { + this.setProperties( + commonProps({ + isSingleColumn: true, + datacenter: { + name: 'dc1', + nodes: [nodeGen('node-1', 'dc1', 1000, 500), nodeGen('node-2', 'dc1', 1000, 500)], + }, + }) + ); + + await this.render(commonTemplate); + + assert.ok(find('[data-test-flex-masonry].flex-masonry-columns-1')); + + this.set('isSingleColumn', false); + assert.ok(find('[data-test-flex-masonry].flex-masonry-columns-2')); + }); + + test('args get passed down to the TopViz::Node children', async function(assert) { + const heightSpy = sinon.spy(); + this.setProperties( + commonProps({ + isDense: true, + heightScale: (...args) => { + heightSpy(...args); + return 50; + }, + datacenter: { + name: 'dc1', + nodes: [nodeGen('node-1', 'dc1', 1000, 500, [{ memory: 100, cpu: 300 }])], + }, + }) + ); + + await this.render(commonTemplate); + + TopoVizDatacenter.nodes[0].as(async TopoVizNode => { + assert.notOk(TopoVizNode.labelIsPresent); + assert.ok(heightSpy.calledWith(this.datacenter.nodes[0].memory)); + + await TopoVizNode.selectNode(); + assert.ok(this.onNodeSelect.calledWith(this.datacenter.nodes[0])); + + await TopoVizNode.memoryRects[0].select(); + assert.ok(this.onAllocationSelect.calledWith(this.datacenter.nodes[0].allocations[0])); + }); + }); +}); diff --git a/ui/tests/pages/components/topo-viz/datacenter.js b/ui/tests/pages/components/topo-viz/datacenter.js new file mode 100644 index 000000000000..1388eeead539 --- /dev/null +++ b/ui/tests/pages/components/topo-viz/datacenter.js @@ -0,0 +1,9 @@ +import { collection, text } from 'ember-cli-page-object'; +import TopoVizNode from './node'; + +export default scope => ({ + scope, + + label: text('[data-test-topo-viz-datacenter-label]'), + nodes: collection('[data-test-topo-viz-node]', TopoVizNode()), +}); diff --git a/ui/tests/pages/components/topo-viz/node.js b/ui/tests/pages/components/topo-viz/node.js index 5dac302a768d..665940ebf5ad 100644 --- a/ui/tests/pages/components/topo-viz/node.js +++ b/ui/tests/pages/components/topo-viz/node.js @@ -1,4 +1,4 @@ -import { attribute, collection, clickable, hasClass, text } from 'ember-cli-page-object'; +import { attribute, collection, clickable, hasClass, isPresent, text } from 'ember-cli-page-object'; const allocationRect = { select: clickable(), @@ -15,6 +15,7 @@ export default scope => ({ scope, label: text('[data-test-label]'), + labelIsPresent: isPresent('[data-test-label]'), statusIcon: attribute('class', '[data-test-status-icon] .icon'), statusIconLabel: attribute('aria-label', '[data-test-status-icon]'),