Job:
+ @query={{hash jobNamespace=this.activeAllocation.job.namespace.id}}>
{{this.activeAllocation.job.name}}
/ {{this.activeAllocation.taskGroupName}}
@@ -157,7 +160,7 @@
Client:
-
+
{{this.activeAllocation.node.shortId}}
@@ -173,10 +176,10 @@
{{else}}
-
{{this.model.nodes.length}} Clients
+
{{this.model.nodes.length}} Clients
-
{{this.scheduledAllocations.length}} Allocations
+
{{this.scheduledAllocations.length}} Allocations
@@ -185,6 +188,7 @@
- {{format-percentage this.reservedMemoryPercent total=1}}
+ {{format-percentage this.reservedMemoryPercent total=1}}
-
+
{{format-bytes this.totalReservedMemory}} / {{format-bytes this.totalMemory}} reserved
@@ -206,6 +210,7 @@
- {{format-percentage this.reservedCPUPercent total=1}}
+ {{format-percentage this.reservedCPUPercent total=1}}
-
+
{{this.totalReservedCPU}} MHz / {{this.totalCPU}} MHz reserved
diff --git a/ui/tests/acceptance/topology-test.js b/ui/tests/acceptance/topology-test.js
index 70e510fb8..ef2d8bc14 100644
--- a/ui/tests/acceptance/topology-test.js
+++ b/ui/tests/acceptance/topology-test.js
@@ -1,12 +1,16 @@
+import { get } from '@ember/object';
+import { currentURL } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
import Topology from 'nomad-ui/tests/pages/topology';
+import { reduceToLargestUnit } from 'nomad-ui/helpers/format-bytes';
import queryString from 'query-string';
-// TODO: Once we settle on the contents of the info panel, the contents
-// should also get acceptance tests.
+const sumResources = (list, dimension) =>
+ list.reduce((agg, val) => agg + (get(val, dimension) || 0), 0);
+
module('Acceptance | topology', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
@@ -30,6 +34,44 @@ module('Acceptance | topology', function(hooks) {
await Topology.visit();
assert.equal(Topology.infoPanelTitle, 'Cluster Details');
assert.notOk(Topology.filteredNodesWarning.isPresent);
+
+ assert.equal(
+ Topology.clusterInfoPanel.nodeCount,
+ `${server.schema.nodes.all().length} Clients`
+ );
+
+ const allocs = server.schema.allocations.all().models;
+ const scheduledAllocs = allocs.filter(alloc =>
+ ['pending', 'running'].includes(alloc.clientStatus)
+ );
+ assert.equal(Topology.clusterInfoPanel.allocCount, `${scheduledAllocs.length} Allocations`);
+
+ const nodeResources = server.schema.nodes.all().models.mapBy('nodeResources');
+ const taskResources = scheduledAllocs
+ .mapBy('taskResources.models')
+ .flat()
+ .mapBy('resources');
+
+ const totalMem = sumResources(nodeResources, 'Memory.MemoryMB');
+ const totalCPU = sumResources(nodeResources, 'Cpu.CpuShares');
+ const reservedMem = sumResources(taskResources, 'Memory.MemoryMB');
+ const reservedCPU = sumResources(taskResources, 'Cpu.CpuShares');
+
+ assert.equal(Topology.clusterInfoPanel.memoryProgressValue, reservedMem / totalMem);
+ assert.equal(Topology.clusterInfoPanel.cpuProgressValue, reservedCPU / totalCPU);
+
+ const [rNum, rUnit] = reduceToLargestUnit(reservedMem * 1024 * 1024);
+ const [tNum, tUnit] = reduceToLargestUnit(totalMem * 1024 * 1024);
+
+ assert.equal(
+ Topology.clusterInfoPanel.memoryAbsoluteValue,
+ `${Math.floor(rNum)} ${rUnit} / ${Math.floor(tNum)} ${tUnit} reserved`
+ );
+
+ assert.equal(
+ Topology.clusterInfoPanel.cpuAbsoluteValue,
+ `${reservedCPU} MHz / ${totalCPU} MHz reserved`
+ );
});
test('all allocations for all namespaces and all clients are queried on load', async function(assert) {
@@ -52,28 +94,126 @@ module('Acceptance | topology', function(hooks) {
});
test('when an allocation is selected, the info panel shows information on the allocation', async function(assert) {
- server.createList('node', 1);
- server.createList('allocation', 5);
+ const nodes = server.createList('node', 5);
+ const job = server.create('job', { createAllocations: false });
+ const taskGroup = server.schema.find('taskGroup', job.taskGroupIds[0]).name;
+ const allocs = server.createList('allocation', 5, {
+ forceRunningClientStatus: true,
+ jobId: job.id,
+ taskGroup,
+ });
- await Topology.visit();
-
- if (Topology.viz.datacenters[0].nodes[0].isEmpty) {
- assert.expect(0);
- } else {
- await Topology.viz.datacenters[0].nodes[0].memoryRects[0].select();
- assert.equal(Topology.infoPanelTitle, 'Allocation Details');
+ // Get the first alloc of the first node that has an alloc
+ const sortedNodes = nodes.sortBy('datacenter');
+ let node, alloc;
+ for (let n of sortedNodes) {
+ alloc = allocs.find(a => a.nodeId === n.id);
+ if (alloc) {
+ node = n;
+ break;
+ }
}
+
+ const dcIndex = nodes
+ .mapBy('datacenter')
+ .uniq()
+ .sort()
+ .indexOf(node.datacenter);
+ const nodeIndex = nodes.filterBy('datacenter', node.datacenter).indexOf(node);
+
+ const reset = async () => {
+ await Topology.visit();
+ await Topology.viz.datacenters[dcIndex].nodes[nodeIndex].memoryRects[0].select();
+ };
+
+ await reset();
+ assert.equal(Topology.infoPanelTitle, 'Allocation Details');
+
+ assert.equal(Topology.allocInfoPanel.id, alloc.id.split('-')[0]);
+
+ const uniqueClients = allocs.mapBy('nodeId').uniq();
+ assert.equal(Topology.allocInfoPanel.siblingAllocs, `Sibling Allocations: ${allocs.length}`);
+ assert.equal(
+ Topology.allocInfoPanel.uniquePlacements,
+ `Unique Client Placements: ${uniqueClients.length}`
+ );
+
+ assert.equal(Topology.allocInfoPanel.job, job.name);
+ assert.ok(Topology.allocInfoPanel.taskGroup.endsWith(alloc.taskGroup));
+ assert.equal(Topology.allocInfoPanel.client, node.id.split('-')[0]);
+
+ await Topology.allocInfoPanel.visitAlloc();
+ assert.equal(currentURL(), `/allocations/${alloc.id}`);
+
+ await reset();
+
+ await Topology.allocInfoPanel.visitJob();
+ assert.equal(currentURL(), `/jobs/${job.id}`);
+
+ await reset();
+
+ await Topology.allocInfoPanel.visitClient();
+ assert.equal(currentURL(), `/clients/${node.id}`);
});
test('when a node is selected, the info panel shows information on the node', async function(assert) {
// A high node count is required for node selection
- server.createList('node', 51);
- server.createList('allocation', 5);
+ const nodes = server.createList('node', 51);
+ const node = nodes.sortBy('datacenter')[0];
+ server.createList('allocation', 5, { forceRunningClientStatus: true });
+
+ const allocs = server.schema.allocations.where({ nodeId: node.id }).models;
await Topology.visit();
await Topology.viz.datacenters[0].nodes[0].selectNode();
assert.equal(Topology.infoPanelTitle, 'Client Details');
+
+ assert.equal(Topology.nodeInfoPanel.id, node.id.split('-')[0]);
+ assert.equal(Topology.nodeInfoPanel.name, `Name: ${node.name}`);
+ assert.equal(Topology.nodeInfoPanel.address, `Address: ${node.httpAddr}`);
+ assert.equal(Topology.nodeInfoPanel.status, `Status: ${node.status}`);
+
+ assert.equal(Topology.nodeInfoPanel.drainingLabel, node.drain ? 'Yes' : 'No');
+ assert.equal(
+ Topology.nodeInfoPanel.eligibleLabel,
+ node.schedulingEligibility === 'eligible' ? 'Yes' : 'No'
+ );
+
+ assert.equal(Topology.nodeInfoPanel.drainingIsAccented, node.drain);
+ assert.equal(
+ Topology.nodeInfoPanel.eligibleIsAccented,
+ node.schedulingEligibility !== 'eligible'
+ );
+
+ const taskResources = allocs
+ .mapBy('taskResources.models')
+ .flat()
+ .mapBy('resources');
+ const reservedMem = sumResources(taskResources, 'Memory.MemoryMB');
+ const reservedCPU = sumResources(taskResources, 'Cpu.CpuShares');
+
+ const totalMem = node.nodeResources.Memory.MemoryMB;
+ const totalCPU = node.nodeResources.Cpu.CpuShares;
+
+ assert.equal(Topology.nodeInfoPanel.memoryProgressValue, reservedMem / totalMem);
+ assert.equal(Topology.nodeInfoPanel.cpuProgressValue, reservedCPU / totalCPU);
+
+ const [rNum, rUnit] = reduceToLargestUnit(reservedMem * 1024 * 1024);
+ const [tNum, tUnit] = reduceToLargestUnit(totalMem * 1024 * 1024);
+
+ assert.equal(
+ Topology.nodeInfoPanel.memoryAbsoluteValue,
+ `${Math.floor(rNum)} ${rUnit} / ${Math.floor(tNum)} ${tUnit} reserved`
+ );
+
+ assert.equal(
+ Topology.nodeInfoPanel.cpuAbsoluteValue,
+ `${reservedCPU} MHz / ${totalCPU} MHz reserved`
+ );
+
+ await Topology.nodeInfoPanel.visitNode();
+ assert.equal(currentURL(), `/clients/${node.id}`);
});
test('when one or more nodes lack the NodeResources property, a warning message is shown', async function(assert) {
diff --git a/ui/tests/integration/components/topo-viz/node-test.js b/ui/tests/integration/components/topo-viz/node-test.js
index c3ffe6675..e56d40645 100644
--- a/ui/tests/integration/components/topo-viz/node-test.js
+++ b/ui/tests/integration/components/topo-viz/node-test.js
@@ -23,15 +23,15 @@ const nodeGen = (name, datacenter, memory, cpu, flags = {}) => ({
},
});
-const allocGen = (node, memory, cpu, isSelected) => ({
+const allocGen = (node, memory, cpu, isScheduled = true) => ({
memory,
cpu,
- isSelected,
+ isSelected: false,
memoryPercent: memory / node.memory,
cpuPercent: cpu / node.cpu,
allocation: {
id: faker.random.uuid(),
- isScheduled: true,
+ isScheduled,
},
});
@@ -66,7 +66,11 @@ module('Integration | Component | TopoViz::Node', function(hooks) {
props({
node: {
...node,
- allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
+ allocations: [
+ allocGen(node, 100, 100),
+ allocGen(node, 250, 250),
+ allocGen(node, 300, 300, false),
+ ],
},
})
);
@@ -74,7 +78,10 @@ module('Integration | Component | TopoViz::Node', function(hooks) {
await this.render(commonTemplate);
assert.ok(TopoVizNode.isPresent);
- assert.ok(TopoVizNode.memoryRects.length);
+ assert.equal(
+ TopoVizNode.memoryRects.length,
+ this.node.allocations.filterBy('allocation.isScheduled').length
+ );
assert.ok(TopoVizNode.cpuRects.length);
await componentA11yAudit(this.element, assert);
@@ -86,7 +93,11 @@ module('Integration | Component | TopoViz::Node', function(hooks) {
props({
node: {
...node,
- allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
+ allocations: [
+ allocGen(node, 100, 100),
+ allocGen(node, 250, 250),
+ allocGen(node, 300, 300, false),
+ ],
},
})
);
@@ -94,7 +105,11 @@ module('Integration | Component | TopoViz::Node', function(hooks) {
await this.render(commonTemplate);
assert.ok(TopoVizNode.label.includes(node.node.name));
- assert.ok(TopoVizNode.label.includes(`${this.node.allocations.length} Allocs`));
+ assert.ok(
+ TopoVizNode.label.includes(
+ `${this.node.allocations.filterBy('allocation.isScheduled').length} Allocs`
+ )
+ );
assert.ok(TopoVizNode.label.includes(`${this.node.memory} MiB`));
assert.ok(TopoVizNode.label.includes(`${this.node.cpu} MHz`));
});
diff --git a/ui/tests/pages/topology.js b/ui/tests/pages/topology.js
index 63e38de22..74f5258bf 100644
--- a/ui/tests/pages/topology.js
+++ b/ui/tests/pages/topology.js
@@ -1,4 +1,4 @@
-import { create, text, visitable } from 'ember-cli-page-object';
+import { attribute, clickable, create, hasClass, text, visitable } from 'ember-cli-page-object';
import TopoViz from 'nomad-ui/tests/pages/components/topo-viz';
import notification from 'nomad-ui/tests/pages/components/notification';
@@ -10,4 +10,54 @@ export default create({
filteredNodesWarning: notification('[data-test-filtered-nodes-warning]'),
viz: TopoViz('[data-test-topo-viz]'),
+
+ clusterInfoPanel: {
+ scope: '[data-test-info-panel]',
+ nodeCount: text('[data-test-node-count]'),
+ allocCount: text('[data-test-alloc-count]'),
+
+ memoryProgressValue: attribute('value', '[data-test-memory-progress-bar]'),
+ memoryAbsoluteValue: text('[data-test-memory-absolute-value]'),
+ cpuProgressValue: attribute('value', '[data-test-cpu-progress-bar]'),
+ cpuAbsoluteValue: text('[data-test-cpu-absolute-value]'),
+ },
+
+ nodeInfoPanel: {
+ scope: '[data-test-info-panel]',
+ allocations: text('[data-test-allocaions]'),
+
+ visitNode: clickable('[data-test-client-link]'),
+
+ id: text('[data-test-client-link]'),
+ name: text('[data-test-name]'),
+ address: text('[data-test-address]'),
+ status: text('[data-test-status]'),
+
+ drainingLabel: text('[data-test-draining]'),
+ drainingIsAccented: hasClass('is-info', '[data-test-draining]'),
+
+ eligibleLabel: text('[data-test-eligible]'),
+ eligibleIsAccented: hasClass('is-warning', '[data-test-eligible]'),
+
+ memoryProgressValue: attribute('value', '[data-test-memory-progress-bar]'),
+ memoryAbsoluteValue: text('[data-test-memory-absolute-value]'),
+ cpuProgressValue: attribute('value', '[data-test-cpu-progress-bar]'),
+ cpuAbsoluteValue: text('[data-test-cpu-absolute-value]'),
+ },
+
+ allocInfoPanel: {
+ scope: '[data-test-info-panel]',
+ id: text('[data-test-id]'),
+ visitAlloc: clickable('[data-test-id]'),
+
+ siblingAllocs: text('[data-test-sibling-allocs]'),
+ uniquePlacements: text('[data-test-unique-placements]'),
+
+ job: text('[data-test-job]'),
+ visitJob: clickable('[data-test-job]'),
+ taskGroup: text('[data-test-task-group]'),
+
+ client: text('[data-test-client]'),
+ visitClient: clickable('[data-test-client]'),
+ },
});