Files
nomad/ui/tests/acceptance/actions-test.js
Phil Renaud 1976202cd6 Feature: Dynamic Host Volumes in the UI (#25224)
* 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
2025-03-10 14:46:02 -04:00

410 lines
12 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
// @ts-check
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import Tokens from 'nomad-ui/tests/pages/settings/tokens';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
import percySnapshot from '@percy/ember';
import Actions from 'nomad-ui/tests/pages/jobs/job/actions';
import { triggerEvent, visit, click } from '@ember/test-helpers';
import faker from 'nomad-ui/mirage/faker';
module('Acceptance | actions', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(async function () {
faker.seed(1);
window.localStorage.clear();
server.create('agent');
server.create('node-pool');
server.create('node');
const actionsJob = server.create('job', {
createAllocations: true,
resourceSpec: Array(2).fill('M: 257, C: 500'),
groupAllocCount: 5,
groupTaskCount: 2,
shallow: false,
name: 'actionable-job',
id: 'actionable-job',
namespaceId: 'default',
type: 'service',
activeDeployment: false,
noDeployments: true,
allocStatusDistribution: {
running: 1,
},
noFailedPlacements: true,
status: 'running',
withActions: true,
});
// A third task group in the Actions job with a single task/alloc
const actionsGroup = server.create('task-group', {
jobId: actionsJob.id,
name: 'actionable-group',
taskCount: 1,
});
// make sure the allocation generated by that group is running
server.schema.allocations.findBy({ taskGroup: actionsGroup.name }).update({
clientStatus: 'running',
});
// Set its task state to running
server.schema.allocations
.all()
.filter((x) => x.taskGroup === actionsGroup.name)
.models[0].taskStates.models[0]?.update({
state: 'running',
});
});
test('Actions show up on the Job Index page, permissions allowing', async function (assert) {
assert.expect(8);
let managementToken = server.create('token', {
type: 'management',
name: 'Management Token',
});
let clientReaderToken = server.create('token', {
type: 'client',
name: "N. O'DeReader",
});
const allocExecPolicy = server.create('policy', {
id: 'alloc-exec',
rules: `
namespace "*" {
policy = "read"
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
}
`,
rulesJSON: {
Namespaces: [
{
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
Name: '*',
},
],
},
});
let allocExecToken = server.create('token', {
type: 'client',
name: 'Alloc Exec Token',
policyIds: [allocExecPolicy.id],
});
await Actions.visitIndex({ id: 'actionable-job' });
// no actions dropdown by default
assert.notOk(Actions.hasTitleActions, 'No actions dropdown by default');
await Tokens.visit();
const { secretId } = managementToken;
await Tokens.secret(secretId).submit();
await Actions.visitIndex({ id: 'actionable-job' });
assert.ok(
Actions.hasTitleActions,
'Management token sees actions dropdown'
);
assert.ok(Actions.taskRowActions.length, 'Task row has actions dropdowns');
await a11yAudit(assert);
// Sign out and sign back in as a token without alloc exec
await Tokens.visit();
await Tokens.clear();
await Tokens.secret(clientReaderToken.secretId).submit();
await Actions.visitIndex({ id: 'actionable-job' });
assert.notOk(
Actions.hasTitleActions,
'Basic client token does not see actions dropdown'
);
assert.notOk(
Actions.taskRowActions.length,
'Basic client token does not see task row actions dropdowns'
);
// Sign out and sign back in as a token with alloc exec
await Tokens.visit();
await Tokens.clear();
await Tokens.secret(allocExecToken.secretId).submit();
await Actions.visitIndex({ id: 'actionable-job' });
assert.ok(
Actions.hasTitleActions,
'Alloc exec token sees actions dropdown'
);
assert.ok(
Actions.taskRowActions.length,
'Alloc exec token sees task row actions dropdowns'
);
});
// Running actions test
test('Running actions and notifications', async function (assert) {
assert.expect(20);
let managementToken = server.create('token', {
type: 'management',
name: 'Management Token',
});
await Tokens.visit();
const { secretId } = managementToken;
await Tokens.secret(secretId).submit();
await Actions.visitIndex({ id: 'actionable-job' });
assert.ok(
Actions.hasTitleActions,
'Management token sees actions dropdown'
);
// Open the dropdown
await Actions.titleActions.click();
assert.equal(Actions.titleActions.expandedValue, 'true');
assert.equal(
Actions.titleActions.actions.length,
5,
'5 actions show up in the dropdown'
);
assert.equal(
Actions.titleActions.multiAllocActions.length,
4,
'4 actions in the dropdown have multiple allocs to run against'
);
assert.equal(
Actions.titleActions.singleAllocActions.length,
1,
'1 action in the dropdown has a single alloc to run against'
);
assert.equal(
Actions.titleActions.multiAllocActions[0].button[0].expanded,
'false',
"The first action's dropdown is not expanded"
);
assert.notOk(
Actions.titleActions.multiAllocActions[0].showsDisclosureContent,
"The first action's dropdown subcontent does not yet exist"
);
await Actions.titleActions.actions[0].click();
assert.equal(
Actions.titleActions.multiAllocActions[0].button[0].expanded,
'true',
"The first action's dropdown is expanded"
);
assert.ok(
Actions.titleActions.multiAllocActions[0].showsDisclosureContent,
"The first action's dropdown subcontent exists"
);
await percySnapshot(assert, {
percyCSS: `
.allocation-row td { display: none; }
`,
});
// run on a random alloc
await Actions.titleActions.multiAllocActions[0].subActions[0].click();
assert.ok(Actions.flyout.isPresent);
assert.equal(
Actions.flyout.instances.length,
1,
'A sidebar instance pops up upon running an action'
);
assert.ok(
Actions.flyout.instances[0].code.includes('Message Received'),
'The instance contains the message from the action'
);
assert.ok(
Actions.flyout.instances[0].statusBadge.includes('Complete'),
'The instance contains the status of the action'
);
await Actions.flyout.close();
// Type the escape key: the Helios dropdown doesn't automatically close on click-away events
// as defined by clickable in the page object here, so we should explicitly make sure it's closed.
await triggerEvent('.job-page-header .actions-dropdown', 'keyup', {
key: 'Escape',
});
assert.notOk(Actions.flyout.isPresent);
assert.equal(Actions.titleActions.expandedValue, 'false');
await Actions.titleActions.click();
await Actions.titleActions.multiAllocActions[0].button[0].click();
await Actions.titleActions.multiAllocActions[0].subActions[1].click();
assert.ok(Actions.flyout.isPresent);
// 2 assets, the second of which has multiple peer allocs within it
assert.equal(
Actions.flyout.instances.length,
2,
'Running on all allocs in the group (1) results in 2 total instances'
);
assert.ok(
Actions.flyout.instances[0].hasPeers,
'The first instance has peers'
);
assert.notOk(
Actions.flyout.instances[1].hasPeers,
'The second instance does not have peers'
);
await Actions.flyout.close();
// Type the escape key: the Helios dropdown doesn't automatically close on click-away events
// as defined by clickable in the page object here, so we should explicitly make sure it's closed.
await triggerEvent('.job-page-header .actions-dropdown', 'keyup', {
key: 'Escape',
});
await Actions.titleActions.click();
await Actions.titleActions.singleAllocActions[0].button[0].click();
assert.equal(
Actions.flyout.instances.length,
3,
'Running on an orphan alloc results in 1 further action instance'
);
await percySnapshot(assert);
});
test('Running actions from a task row', async function (assert) {
let managementToken = server.create('token', {
type: 'management',
name: 'Management Token',
});
await Tokens.visit();
const { secretId } = managementToken;
await Tokens.secret(secretId).submit();
await Actions.visitAllocs({ id: 'actionable-job' });
// Get the number of rows; each of them should have an actions dropdown
const job = server.schema.jobs.find('actionable-job');
const numberOfTaskRows = server.schema.allocations
.all()
.models.filter((a) => a.jobId === job.name)
.map((a) => a.taskStates.models)
.flat().length;
assert.equal(
Actions.taskRowActions.length,
numberOfTaskRows,
'Each task row has an actions dropdown'
);
await Actions.taskRowActions[0].click();
assert.equal(
Actions.taskRowActions[0].actions.length,
1,
'Actions within a task row actions dropdown are shown'
);
await Actions.taskRowActions[0].actions[0].click();
assert.ok(Actions.flyout.isPresent);
assert.equal(
Actions.flyout.instances.length,
1,
'A sidebar instance pops up upon running an action'
);
assert.ok(
Actions.flyout.instances[0].code.includes('Message Received'),
'The instance contains the message from the action'
);
});
test('Actions flyout gets dynamic actions list', async function (assert) {
assert.expect(8);
let managementToken = server.create('token', {
type: 'management',
name: 'Management Token',
});
await Tokens.visit();
const { secretId } = managementToken;
await Tokens.secret(secretId).submit();
await Actions.visitIndex({ id: 'actionable-job' });
// Run an action to open the flyout; observe the dropdown there
await Actions.titleActions.click();
await Actions.titleActions.singleAllocActions[0].button[0].click();
// Is flyout open?
assert.ok(Actions.flyout.isPresent, 'Flyout is open');
// Is there a dropdown in the flyout?
assert.ok(Actions.flyout.actions.isPresent, 'Flyout has actions dropdown');
// Close the flyout go to the Jobs page
await Actions.flyout.close();
await visit('/jobs');
assert.notOk(Actions.flyout.isPresent, 'Flyout is closed');
// Global button should be present
assert.ok(Actions.globalButton.isPresent, 'Global button is present');
// click it
await Actions.globalButton.click();
// actions flyout should be open
assert.ok(Actions.flyout.isPresent, 'Flyout is open');
// it shouldn't have a dropdown in it
assert.notOk(
Actions.flyout.actions.isPresent,
'Flyout has no actions dropdown'
);
await Actions.flyout.close();
// head back into the job, and into a task
await Actions.visitIndex({ id: 'actionable-job' });
await click('[data-test-task-group="actionable-group"] a');
await click('.task-name');
// Click global button
await Actions.globalButton.click();
// Dropdown present
assert.ok(
Actions.flyout.actions.isPresent,
'Flyout has actions dropdown on task page'
);
await percySnapshot(assert, {
percyCSS: `
g.tick { visibility: hidden; }
.recent-events-table td {
display: none;
}
.inline-definitions { visibility: hidden; }
`,
});
// Clear finished actions and take a snapshot
await click('button[data-test-clear-finished-actions]');
await percySnapshot('Cleared actions/flyout open state', {
percyCSS: `
g.tick { visibility: hidden; }
.recent-events-table td {
display: none;
}
.inline-definitions { visibility: hidden; }
`,
});
// Close flyout; global button is no longer present
await Actions.flyout.close();
assert.notOk(
Actions.globalButton.isPresent,
'Global button is not present after flyout close'
);
});
});