mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 18:35:44 +03:00
* DHV UI init * /csi routes to /storage routes and a routeRedirector util (#25163) * /csi routes to /storage routes and a routeRedirector util * Tests and routes move csi/ to storage/ * Changelog added * [ui] Storage UI overhaul + Dynamic Host Volumes UI (#25226) * Storage index page and DHV model properties * Naive version of a storage overview page * Experimental fetch of alloc data dirs * Fetch ephemeral disks and static host volumes as an ember concurrency task and nice table stylings * Playing nice with section header labels to make eslint happy even though wcag was already cool with it * inlined the storage type explainers and reordered things, plus tooltips and keynav * Bones of a dynamic host volume individual page * Woooo dynamic host volume model, adapter, and serializer with embedded alloc relationships * Couple test fixes * async:false relationship for dhv.hasMany('alloc') to prevent a ton of xhr requests * DHV request type at index routemodel and better serialization * Pagination and searching and query params oh my * Test retrofits for csi volumes * Really fantastic flake gets fixed * DHV detail page acceptance test and a bunch of mirage hooks * Seed so that the actions test has a guaranteed task * removed ephemeral disk and static host volume manual scanning * CapacityBytes and capabilities table added to DHV detail page * Debugging actions flyout test * was becoming clear that faker.seed editing was causing havoc elsewhere so might as well not boil the ocean and just tell this test to do what I want it to * Post-create job gets taskCount instead of count * CSI volumes now get /csi route prefix at detail level * lazyclick method for unused keynav removed * keyboard nav and table-watcher for DHV added * Addressed PR comments, changed up capabilities table and id references, etc. * Capabilities table for DHV and ID in details header * Testfixes for pluginID and capabilities table on DHV page
237 lines
7.7 KiB
JavaScript
237 lines
7.7 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
/* eslint-disable qunit/require-expect */
|
|
import { module, test } from 'qunit';
|
|
import { currentURL } from '@ember/test-helpers';
|
|
import { setupApplicationTest } from 'ember-qunit';
|
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
|
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
|
import moment from 'moment';
|
|
import { formatBytes, formatHertz } from 'nomad-ui/utils/units';
|
|
import VolumeDetail from 'nomad-ui/tests/pages/storage/dynamic-host-volumes/detail';
|
|
import Layout from 'nomad-ui/tests/pages/layout';
|
|
import percySnapshot from '@percy/ember';
|
|
|
|
const assignAlloc = (volume, alloc) => {
|
|
volume.allocations.add(alloc);
|
|
volume.save();
|
|
};
|
|
|
|
module('Acceptance | dynamic host volume detail', function (hooks) {
|
|
setupApplicationTest(hooks);
|
|
setupMirage(hooks);
|
|
|
|
let volume;
|
|
|
|
hooks.beforeEach(function () {
|
|
server.create('node-pool');
|
|
server.create('node');
|
|
server.create('job', {
|
|
name: 'dhv-job',
|
|
});
|
|
volume = server.create('dynamic-host-volume', {
|
|
nodeId: server.db.nodes[0].id,
|
|
});
|
|
});
|
|
|
|
test('it passes an accessibility audit', async function (assert) {
|
|
await VolumeDetail.visit({ id: `${volume.id}@default` });
|
|
await a11yAudit(assert);
|
|
});
|
|
|
|
test('/storage/volumes/:id should have a breadcrumb trail linking back to Volumes and Storage', async function (assert) {
|
|
await VolumeDetail.visit({ id: `${volume.id}@default` });
|
|
|
|
assert.equal(Layout.breadcrumbFor('storage.index').text, 'Storage');
|
|
assert.equal(
|
|
Layout.breadcrumbFor('storage.volumes.dynamic-host-volume').text,
|
|
volume.name
|
|
);
|
|
});
|
|
|
|
test('/storage/volumes/:id should show the volume name in the title', async function (assert) {
|
|
await VolumeDetail.visit({ id: `${volume.id}@default` });
|
|
|
|
assert.equal(document.title, `Dynamic Host Volume ${volume.name} - Nomad`);
|
|
assert.equal(VolumeDetail.title, volume.name);
|
|
});
|
|
|
|
test('/storage/volumes/:id should list additional details for the volume below the title', async function (assert) {
|
|
await VolumeDetail.visit({ id: `${volume.id}@default` });
|
|
assert.ok(VolumeDetail.node.includes(volume.node.name));
|
|
assert.ok(VolumeDetail.plugin.includes(volume.pluginID));
|
|
assert.notOk(
|
|
VolumeDetail.hasNamespace,
|
|
'Namespace is omitted when there is only one namespace'
|
|
);
|
|
assert.equal(VolumeDetail.capacity, 'Capacity 9.54 MiB');
|
|
});
|
|
|
|
test('/storage/volumes/:id should list all allocations the volume is attached to', async function (assert) {
|
|
const allocations = server.createList('allocation', 3);
|
|
allocations.forEach((alloc) => assignAlloc(volume, alloc));
|
|
|
|
await VolumeDetail.visit({ id: `${volume.id}@default` });
|
|
|
|
assert.equal(VolumeDetail.allocations.length, allocations.length);
|
|
allocations
|
|
.sortBy('modifyIndex')
|
|
.reverse()
|
|
.forEach((allocation, idx) => {
|
|
assert.equal(allocation.id, VolumeDetail.allocations.objectAt(idx).id);
|
|
});
|
|
await percySnapshot(assert);
|
|
});
|
|
|
|
test('each allocation should have high-level details for the allocation', async function (assert) {
|
|
const allocation = server.create('allocation', { clientStatus: 'running' });
|
|
assignAlloc(volume, allocation);
|
|
|
|
const allocStats = server.db.clientAllocationStats.find(allocation.id);
|
|
const taskGroup = server.db.taskGroups.findBy({
|
|
name: allocation.taskGroup,
|
|
jobId: allocation.jobId,
|
|
});
|
|
|
|
const tasks = taskGroup.taskIds.map((id) => server.db.tasks.find(id));
|
|
const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0);
|
|
const memoryUsed = tasks.reduce(
|
|
(sum, task) => sum + task.resources.MemoryMB,
|
|
0
|
|
);
|
|
|
|
await VolumeDetail.visit({ id: `${volume.id}@default` });
|
|
VolumeDetail.allocations.objectAt(0).as((allocationRow) => {
|
|
assert.equal(
|
|
allocationRow.shortId,
|
|
allocation.id.split('-')[0],
|
|
'Allocation short ID'
|
|
);
|
|
assert.equal(
|
|
allocationRow.createTime,
|
|
moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'),
|
|
'Allocation create time'
|
|
);
|
|
assert.equal(
|
|
allocationRow.modifyTime,
|
|
moment(allocation.modifyTime / 1000000).fromNow(),
|
|
'Allocation modify time'
|
|
);
|
|
assert.equal(
|
|
allocationRow.status,
|
|
allocation.clientStatus,
|
|
'Client status'
|
|
);
|
|
assert.equal(
|
|
allocationRow.job,
|
|
server.db.jobs.find(allocation.jobId).name,
|
|
'Job name'
|
|
);
|
|
assert.ok(allocationRow.taskGroup, 'Task group name');
|
|
assert.ok(allocationRow.jobVersion, 'Job Version');
|
|
assert.equal(
|
|
allocationRow.client,
|
|
server.db.nodes.find(allocation.nodeId).id.split('-')[0],
|
|
'Node ID'
|
|
);
|
|
assert.equal(
|
|
allocationRow.clientTooltip.substr(0, 15),
|
|
server.db.nodes.find(allocation.nodeId).name.substr(0, 15),
|
|
'Node Name'
|
|
);
|
|
assert.equal(
|
|
allocationRow.cpu,
|
|
Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed,
|
|
'CPU %'
|
|
);
|
|
const roundedTicks = Math.floor(
|
|
allocStats.resourceUsage.CpuStats.TotalTicks
|
|
);
|
|
assert.equal(
|
|
allocationRow.cpuTooltip,
|
|
`${formatHertz(roundedTicks, 'MHz')} / ${formatHertz(cpuUsed, 'MHz')}`,
|
|
'Detailed CPU information is in a tooltip'
|
|
);
|
|
assert.equal(
|
|
allocationRow.mem,
|
|
allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed,
|
|
'Memory used'
|
|
);
|
|
assert.equal(
|
|
allocationRow.memTooltip,
|
|
`${formatBytes(
|
|
allocStats.resourceUsage.MemoryStats.RSS
|
|
)} / ${formatBytes(memoryUsed, 'MiB')}`,
|
|
'Detailed memory information is in a tooltip'
|
|
);
|
|
});
|
|
});
|
|
|
|
test('each allocation should link to the allocation detail page', async function (assert) {
|
|
const allocation = server.create('allocation');
|
|
assignAlloc(volume, allocation);
|
|
|
|
await VolumeDetail.visit({ id: `${volume.id}@default` });
|
|
await VolumeDetail.allocations.objectAt(0).visit();
|
|
|
|
assert.equal(currentURL(), `/allocations/${allocation.id}`);
|
|
});
|
|
|
|
test('when there are no allocations, the table presents an empty state', async function (assert) {
|
|
await VolumeDetail.visit({ id: `${volume.id}@default` });
|
|
|
|
assert.ok(VolumeDetail.allocationsTableIsEmpty);
|
|
assert.equal(VolumeDetail.allocationsEmptyState.headline, 'No Allocations');
|
|
});
|
|
|
|
test('Capabilities table shows access mode and attachment mode', async function (assert) {
|
|
await VolumeDetail.visit({ id: `${volume.id}@default` });
|
|
assert.equal(
|
|
VolumeDetail.capabilities.objectAt(0).accessMode,
|
|
'single-node-writer'
|
|
);
|
|
assert.equal(
|
|
VolumeDetail.capabilities.objectAt(0).attachmentMode,
|
|
'file-system'
|
|
);
|
|
assert.equal(
|
|
VolumeDetail.capabilities.objectAt(1).accessMode,
|
|
'single-node-reader-only'
|
|
);
|
|
assert.equal(
|
|
VolumeDetail.capabilities.objectAt(1).attachmentMode,
|
|
'block-device'
|
|
);
|
|
});
|
|
});
|
|
|
|
// Namespace test: details shows the namespace
|
|
module(
|
|
'Acceptance | dynamic volume detail (with namespaces)',
|
|
function (hooks) {
|
|
setupApplicationTest(hooks);
|
|
setupMirage(hooks);
|
|
|
|
let volume;
|
|
|
|
hooks.beforeEach(function () {
|
|
server.createList('namespace', 2);
|
|
server.create('node-pool');
|
|
server.create('node');
|
|
volume = server.create('dynamic-host-volume');
|
|
});
|
|
|
|
test('/storage/volumes/:id detail ribbon includes the namespace of the volume', async function (assert) {
|
|
await VolumeDetail.visit({ id: `${volume.id}@${volume.namespaceId}` });
|
|
|
|
assert.ok(VolumeDetail.hasNamespace);
|
|
assert.ok(
|
|
VolumeDetail.namespace.includes(volume.namespaceId || 'default')
|
|
);
|
|
});
|
|
}
|
|
);
|