mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +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
410 lines
12 KiB
JavaScript
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'
|
|
);
|
|
});
|
|
});
|