2020-05-21 05:29:59 +00:00
|
|
|
import { currentURL, waitUntil, settled } from '@ember/test-helpers';
|
2018-05-13 03:01:51 +00:00
|
|
|
import { assign } from '@ember/polyfills';
|
2019-03-13 00:04:16 +00:00
|
|
|
import { module, test } from 'qunit';
|
|
|
|
import { setupApplicationTest } from 'ember-qunit';
|
2019-09-26 18:47:07 +00:00
|
|
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
2020-07-28 17:59:14 +00:00
|
|
|
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
2021-03-29 23:16:48 +00:00
|
|
|
import { formatBytes, formatHertz } from 'nomad-ui/utils/units';
|
2017-11-30 23:08:31 +00:00
|
|
|
import moment from 'moment';
|
2018-07-10 18:28:52 +00:00
|
|
|
import ClientDetail from 'nomad-ui/tests/pages/clients/detail';
|
|
|
|
import Clients from 'nomad-ui/tests/pages/clients/list';
|
2018-07-11 19:35:39 +00:00
|
|
|
import Jobs from 'nomad-ui/tests/pages/jobs/list';
|
2020-12-10 17:51:22 +00:00
|
|
|
import Layout from 'nomad-ui/tests/pages/layout';
|
2017-09-19 14:47:10 +00:00
|
|
|
|
|
|
|
let node;
|
2020-01-31 05:28:37 +00:00
|
|
|
let managementToken;
|
|
|
|
let clientToken;
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2019-04-22 23:39:07 +00:00
|
|
|
const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation;
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
function nonSearchPOSTS() {
|
2021-10-22 14:51:31 +00:00
|
|
|
return server.pretender.handledRequests
|
|
|
|
.reject(request => request.url.includes('fuzzy'))
|
|
|
|
.filterBy('method', 'POST');
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
}
|
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
module('Acceptance | client detail', function(hooks) {
|
|
|
|
setupApplicationTest(hooks);
|
2019-03-13 01:09:19 +00:00
|
|
|
setupMirage(hooks);
|
2019-03-13 00:04:16 +00:00
|
|
|
|
|
|
|
hooks.beforeEach(function() {
|
2021-10-22 14:51:31 +00:00
|
|
|
window.localStorage.clear();
|
|
|
|
|
2018-05-30 18:20:59 +00:00
|
|
|
server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' });
|
2017-09-19 14:47:10 +00:00
|
|
|
node = server.db.nodes[0];
|
|
|
|
|
2020-01-31 05:28:37 +00:00
|
|
|
managementToken = server.create('token');
|
|
|
|
clientToken = server.create('token');
|
|
|
|
|
|
|
|
window.localStorage.nomadTokenSecret = managementToken.secretId;
|
|
|
|
|
2017-09-19 14:47:10 +00:00
|
|
|
// Related models
|
|
|
|
server.create('agent');
|
|
|
|
server.create('job', { createAllocations: false });
|
2019-05-09 17:26:55 +00:00
|
|
|
server.createList('allocation', 3);
|
|
|
|
server.create('allocation', 'preempted');
|
|
|
|
|
|
|
|
// Force all allocations into the running state so now allocation rows are missing
|
|
|
|
// CPU/Mem runtime metrics
|
|
|
|
server.schema.allocations.all().models.forEach(allocation => {
|
|
|
|
allocation.update({ clientStatus: 'running' });
|
|
|
|
});
|
2019-03-13 00:04:16 +00:00
|
|
|
});
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2020-07-28 17:59:14 +00:00
|
|
|
test('it passes an accessibility audit', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
2020-08-25 15:56:02 +00:00
|
|
|
await a11yAudit(assert);
|
2020-07-28 17:59:14 +00:00
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id should have a breadcrumb trail linking back to clients', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2019-07-17 20:02:58 +00:00
|
|
|
assert.equal(document.title, `Client ${node.name} - Nomad`);
|
|
|
|
|
2017-10-06 03:11:17 +00:00
|
|
|
assert.equal(
|
2020-12-10 17:51:22 +00:00
|
|
|
Layout.breadcrumbFor('clients.index').text,
|
2017-12-20 01:25:35 +00:00
|
|
|
'Clients',
|
|
|
|
'First breadcrumb says clients'
|
|
|
|
);
|
|
|
|
assert.equal(
|
2020-12-10 17:51:22 +00:00
|
|
|
Layout.breadcrumbFor('clients.client').text,
|
2021-12-16 15:28:46 +00:00
|
|
|
`Client ${node.id.split('-')[0]}`,
|
|
|
|
'Second breadcrumb is a titled breadcrumb saying the node short id'
|
2017-10-06 03:11:17 +00:00
|
|
|
);
|
2020-12-10 17:51:22 +00:00
|
|
|
await Layout.breadcrumbFor('clients.index').visit();
|
2017-10-28 01:23:41 +00:00
|
|
|
assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients');
|
2017-09-19 14:47:10 +00:00
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id should list immediate details for the node in the title', async function(assert) {
|
2019-12-13 03:14:40 +00:00
|
|
|
node = server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible', drain: false });
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.ok(ClientDetail.title.includes(node.name), 'Title includes name');
|
2019-12-05 00:54:36 +00:00
|
|
|
assert.ok(ClientDetail.clientId.includes(node.id), 'Title includes id');
|
|
|
|
assert.equal(
|
|
|
|
ClientDetail.statusLight.objectAt(0).id,
|
|
|
|
node.status,
|
|
|
|
'Title includes status light'
|
|
|
|
);
|
2017-10-06 03:11:17 +00:00
|
|
|
});
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id should list additional detail for the node below the title', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2018-05-13 03:01:51 +00:00
|
|
|
assert.ok(
|
2018-07-10 18:28:52 +00:00
|
|
|
ClientDetail.statusDefinition.includes(node.status),
|
2017-10-06 03:11:17 +00:00
|
|
|
'Status is in additional details'
|
|
|
|
);
|
|
|
|
assert.ok(
|
2018-07-10 18:28:52 +00:00
|
|
|
ClientDetail.statusDecorationClass.includes(`node-${node.status}`),
|
2017-10-06 03:11:17 +00:00
|
|
|
'Status is decorated with a status class'
|
|
|
|
);
|
2018-05-13 03:01:51 +00:00
|
|
|
assert.ok(
|
2018-07-10 18:28:52 +00:00
|
|
|
ClientDetail.addressDefinition.includes(node.httpAddr),
|
2018-03-11 17:54:56 +00:00
|
|
|
'Address is in additional details'
|
2017-10-06 03:11:17 +00:00
|
|
|
);
|
2018-05-13 03:01:51 +00:00
|
|
|
assert.ok(
|
2018-07-10 18:28:52 +00:00
|
|
|
ClientDetail.datacenterDefinition.includes(node.datacenter),
|
2017-10-06 03:11:17 +00:00
|
|
|
'Datacenter is in additional details'
|
|
|
|
);
|
|
|
|
});
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id should include resource utilization graphs', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-09-19 23:33:51 +00:00
|
|
|
|
|
|
|
assert.equal(ClientDetail.resourceCharts.length, 2, 'Two resource utilization graphs');
|
|
|
|
assert.equal(ClientDetail.resourceCharts.objectAt(0).name, 'CPU', 'First chart is CPU');
|
|
|
|
assert.equal(ClientDetail.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory');
|
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id should list all allocations on the node', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length;
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2017-10-06 03:11:17 +00:00
|
|
|
|
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
ClientDetail.allocations.length,
|
2017-10-06 03:11:17 +00:00
|
|
|
allocationsCount,
|
|
|
|
`Allocations table lists all ${allocationsCount} associated allocations`
|
|
|
|
);
|
|
|
|
});
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2021-12-17 22:38:04 +00:00
|
|
|
test('/clients/:id should show empty message if there are no allocations on the node', async function(assert) {
|
|
|
|
const emptyNode = server.create('node');
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: emptyNode.id });
|
|
|
|
|
|
|
|
assert.true(ClientDetail.emptyAllocations.isVisible, 'Empty message is visible');
|
|
|
|
assert.equal(ClientDetail.emptyAllocations.headline, 'No Allocations');
|
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('each allocation should have high-level details for the allocation', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
const allocation = server.db.allocations
|
|
|
|
.where({ nodeId: node.id })
|
|
|
|
.sortBy('modifyIndex')
|
|
|
|
.reverse()[0];
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
const allocStats = server.db.clientAllocationStats.find(allocation.id);
|
|
|
|
const taskGroup = server.db.taskGroups.findBy({
|
|
|
|
name: allocation.taskGroup,
|
|
|
|
jobId: allocation.jobId,
|
|
|
|
});
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
|
2020-10-12 22:26:54 +00:00
|
|
|
const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0);
|
|
|
|
const memoryUsed = tasks.reduce((sum, task) => sum + task.resources.MemoryMB, 0);
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
const allocationRow = ClientDetail.allocations.objectAt(0);
|
|
|
|
|
2018-07-24 02:06:41 +00:00
|
|
|
assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short ID');
|
2018-07-19 20:30:08 +00:00
|
|
|
assert.equal(
|
|
|
|
allocationRow.createTime,
|
2019-02-01 17:19:14 +00:00
|
|
|
moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'),
|
2018-07-19 20:30:08 +00:00
|
|
|
'Allocation create time'
|
|
|
|
);
|
2017-10-06 03:11:17 +00:00
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
allocationRow.modifyTime,
|
2018-07-19 20:30:08 +00:00
|
|
|
moment(allocation.modifyTime / 1000000).fromNow(),
|
2017-11-30 23:08:31 +00:00
|
|
|
'Allocation modify time'
|
2017-10-18 02:19:02 +00:00
|
|
|
);
|
2018-07-10 18:28:52 +00:00
|
|
|
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');
|
2020-02-12 19:49:28 +00:00
|
|
|
assert.equal(allocationRow.volume, 'Yes', 'Volume');
|
2017-10-18 02:19:02 +00:00
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
allocationRow.cpu,
|
2017-10-19 17:43:33 +00:00
|
|
|
Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed,
|
2017-10-06 03:11:17 +00:00
|
|
|
'CPU %'
|
|
|
|
);
|
2021-03-29 23:16:48 +00:00
|
|
|
const roundedTicks = Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks);
|
2017-10-17 01:47:25 +00:00
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
allocationRow.cpuTooltip,
|
2021-03-29 23:16:48 +00:00
|
|
|
`${formatHertz(roundedTicks, 'MHz')} / ${formatHertz(cpuUsed, 'MHz')}`,
|
2017-10-17 01:47:25 +00:00
|
|
|
'Detailed CPU information is in a tooltip'
|
|
|
|
);
|
2017-10-06 03:11:17 +00:00
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
allocationRow.mem,
|
2017-10-17 01:47:25 +00:00
|
|
|
allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed,
|
2017-10-06 03:11:17 +00:00
|
|
|
'Memory used'
|
|
|
|
);
|
2017-10-17 01:47:25 +00:00
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
allocationRow.memTooltip,
|
2021-03-27 00:02:54 +00:00
|
|
|
`${formatBytes(allocStats.resourceUsage.MemoryStats.RSS)} / ${formatBytes(
|
|
|
|
memoryUsed,
|
|
|
|
'MiB'
|
|
|
|
)}`,
|
2017-10-17 01:47:25 +00:00
|
|
|
'Detailed memory information is in a tooltip'
|
|
|
|
);
|
2017-10-06 03:11:17 +00:00
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('each allocation should show job information even if the job is incomplete and already in the store', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
// First, visit clients to load the allocations for each visible node.
|
|
|
|
// Don't load the job belongsTo of the allocation! Leave it unfulfilled.
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await Clients.visit();
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
// Then, visit jobs to load all jobs, which should implicitly fulfill
|
|
|
|
// the job belongsTo of each allocation pointed at each job.
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await Jobs.visit();
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
// Finally, visit a node to assert that the job name and task group name are
|
|
|
|
// present. This will require reloading the job, since task groups aren't a
|
|
|
|
// part of the jobs list response.
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
const allocationRow = ClientDetail.allocations.objectAt(0);
|
2017-10-06 03:11:17 +00:00
|
|
|
const allocation = server.db.allocations
|
|
|
|
.where({ nodeId: node.id })
|
|
|
|
.sortBy('modifyIndex')
|
|
|
|
.reverse()[0];
|
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name');
|
|
|
|
assert.ok(allocationRow.taskGroup.includes(allocation.taskGroup), 'Task group name');
|
2017-10-06 03:11:17 +00:00
|
|
|
});
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('each allocation should link to the allocation detail page', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
const allocation = server.db.allocations
|
|
|
|
.where({ nodeId: node.id })
|
|
|
|
.sortBy('modifyIndex')
|
|
|
|
.reverse()[0];
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.allocations.objectAt(0).visit();
|
2017-09-19 14:47:10 +00:00
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
currentURL(),
|
|
|
|
`/allocations/${allocation.id}`,
|
|
|
|
'Allocation rows link to allocation detail pages'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('each allocation should link to the job the allocation belongs to', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
const allocation = server.db.allocations.where({ nodeId: node.id })[0];
|
|
|
|
const job = server.db.jobs.find(allocation.jobId);
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.allocations.objectAt(0).visitJob();
|
2017-09-19 14:47:10 +00:00
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
currentURL(),
|
|
|
|
`/jobs/${job.id}`,
|
|
|
|
'Allocation rows link to the job detail page for the allocation'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2019-04-22 23:39:07 +00:00
|
|
|
test('the allocation section should show the count of preempted allocations on the client', async function(assert) {
|
|
|
|
const allocations = server.db.allocations.where({ nodeId: node.id });
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
ClientDetail.allocationFilter.allCount,
|
|
|
|
allocations.length,
|
|
|
|
'All filter/badge shows all allocations count'
|
|
|
|
);
|
|
|
|
assert.ok(
|
|
|
|
ClientDetail.allocationFilter.preemptionsCount.startsWith(
|
|
|
|
allocations.filter(wasPreemptedFilter).length
|
|
|
|
),
|
|
|
|
'Preemptions filter/badge shows preempted allocations count'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('clicking the preemption badge filters the allocations table and sets a query param', async function(assert) {
|
|
|
|
const allocations = server.db.allocations.where({ nodeId: node.id });
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.allocationFilter.preemptions();
|
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
ClientDetail.allocations.length,
|
|
|
|
allocations.filter(wasPreemptedFilter).length,
|
|
|
|
'Only preempted allocations are shown'
|
|
|
|
);
|
|
|
|
assert.equal(
|
|
|
|
currentURL(),
|
|
|
|
`/clients/${node.id}?preemptions=true`,
|
|
|
|
'Filter is persisted in the URL'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('clicking the total allocations badge resets the filter and removes the query param', async function(assert) {
|
|
|
|
const allocations = server.db.allocations.where({ nodeId: node.id });
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.allocationFilter.preemptions();
|
|
|
|
await ClientDetail.allocationFilter.all();
|
|
|
|
|
|
|
|
assert.equal(ClientDetail.allocations.length, allocations.length, 'All allocations are shown');
|
|
|
|
assert.equal(currentURL(), `/clients/${node.id}`, 'Filter is persisted in the URL');
|
|
|
|
});
|
|
|
|
|
|
|
|
test('navigating directly to the client detail page with the preemption query param set will filter the allocations table', async function(assert) {
|
|
|
|
const allocations = server.db.allocations.where({ nodeId: node.id });
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id, preemptions: true });
|
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
ClientDetail.allocations.length,
|
|
|
|
allocations.filter(wasPreemptedFilter).length,
|
|
|
|
'Only preempted allocations are shown'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id should list all attributes for the node', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
2017-10-06 03:11:17 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.ok(ClientDetail.attributesTable, 'Attributes table is on the page');
|
2017-10-06 03:11:17 +00:00
|
|
|
});
|
2017-09-29 00:05:41 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id lists all meta attributes', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
node = server.create('node', 'forceIPv4', 'withMeta');
|
2018-02-07 02:52:16 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-02-07 02:52:16 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.ok(ClientDetail.metaTable, 'Meta attributes table is on the page');
|
|
|
|
assert.notOk(ClientDetail.emptyMetaMessage, 'Meta attributes is not empty');
|
2018-02-27 00:35:41 +00:00
|
|
|
|
|
|
|
const firstMetaKey = Object.keys(node.meta)[0];
|
2018-07-10 18:28:52 +00:00
|
|
|
const firstMetaAttribute = ClientDetail.metaAttributes.objectAt(0);
|
2018-02-27 00:35:41 +00:00
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
firstMetaAttribute.key,
|
2018-02-27 00:35:41 +00:00
|
|
|
firstMetaKey,
|
|
|
|
'Meta attributes for the node are bound to the attributes table'
|
|
|
|
);
|
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
firstMetaAttribute.value,
|
2018-02-27 00:35:41 +00:00
|
|
|
node.meta[firstMetaKey],
|
|
|
|
'Meta attributes for the node are bound to the attributes table'
|
|
|
|
);
|
2018-02-07 02:52:16 +00:00
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id shows an empty message when there is no meta data', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-02-07 02:52:16 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.notOk(ClientDetail.metaTable, 'Meta attributes table is not on the page');
|
|
|
|
assert.ok(ClientDetail.emptyMetaMessage, 'Meta attributes is empty');
|
2018-02-07 02:52:16 +00:00
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('when the node is not found, an error message is shown, but the URL persists', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: 'not-a-real-node' });
|
2017-09-29 00:05:41 +00:00
|
|
|
|
|
|
|
assert.equal(
|
2020-01-20 20:57:01 +00:00
|
|
|
server.pretender.handledRequests
|
|
|
|
.filter(request => !request.url.includes('policy'))
|
|
|
|
.findBy('status', 404).url,
|
2017-09-29 00:05:41 +00:00
|
|
|
'/v1/node/not-a-real-node',
|
2018-03-12 18:26:37 +00:00
|
|
|
'A request to the nonexistent node is made'
|
2017-09-29 00:05:41 +00:00
|
|
|
);
|
2017-10-28 01:23:41 +00:00
|
|
|
assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists');
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.ok(ClientDetail.error.isShown, 'Error message is shown');
|
|
|
|
assert.equal(ClientDetail.error.title, 'Not Found', 'Error message is for 404');
|
2017-09-29 00:05:41 +00:00
|
|
|
});
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id shows the recent events list', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.ok(ClientDetail.hasEvents, 'Client events section exists');
|
2018-05-13 03:01:51 +00:00
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('each node event shows basic node event information', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
const event = server.db.nodeEvents
|
|
|
|
.where({ nodeId: node.id })
|
|
|
|
.sortBy('time')
|
|
|
|
.reverse()[0];
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
const eventRow = ClientDetail.events.objectAt(0);
|
2019-02-01 17:19:14 +00:00
|
|
|
assert.equal(
|
|
|
|
eventRow.time,
|
|
|
|
moment(event.time).format("MMM DD, 'YY HH:mm:ss ZZ"),
|
|
|
|
'Event timestamp'
|
|
|
|
);
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.equal(eventRow.subsystem, event.subsystem, 'Event subsystem');
|
|
|
|
assert.equal(eventRow.message, event.message, 'Event message');
|
2018-05-13 03:01:51 +00:00
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('/clients/:id shows the driver status of every driver for the node', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
// Set the drivers up so health and detection is well tested
|
|
|
|
const nodeDrivers = node.drivers;
|
|
|
|
const undetectedDriver = 'raw_exec';
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
Object.values(nodeDrivers).forEach(driver => {
|
|
|
|
driver.Detected = true;
|
|
|
|
});
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
nodeDrivers[undetectedDriver].Detected = false;
|
|
|
|
node.drivers = nodeDrivers;
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
const drivers = Object.keys(node.drivers)
|
|
|
|
.map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
|
|
|
|
.sortBy('Name');
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
assert.ok(drivers.length > 0, 'Node has drivers');
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-05-13 03:01:51 +00:00
|
|
|
|
|
|
|
drivers.forEach((driver, index) => {
|
2018-07-10 18:28:52 +00:00
|
|
|
const driverHead = ClientDetail.driverHeads.objectAt(index);
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.equal(driverHead.name, driver.Name, `${driver.Name}: Name is correct`);
|
2018-05-13 03:01:51 +00:00
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
driverHead.detected,
|
2018-05-13 03:01:51 +00:00
|
|
|
driver.Detected ? 'Yes' : 'No',
|
|
|
|
`${driver.Name}: Detection is correct`
|
|
|
|
);
|
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
driverHead.lastUpdated,
|
2018-05-13 03:01:51 +00:00
|
|
|
moment(driver.UpdateTime).fromNow(),
|
|
|
|
`${driver.Name}: Last updated shows time since now`
|
|
|
|
);
|
|
|
|
|
|
|
|
if (driver.Name === undetectedDriver) {
|
|
|
|
assert.notOk(
|
2018-07-10 18:28:52 +00:00
|
|
|
driverHead.healthIsShown,
|
2018-05-13 03:01:51 +00:00
|
|
|
`${driver.Name}: No health for the undetected driver`
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
driverHead.health,
|
2018-05-13 03:01:51 +00:00
|
|
|
driver.Healthy ? 'Healthy' : 'Unhealthy',
|
|
|
|
`${driver.Name}: Health is correct`
|
|
|
|
);
|
|
|
|
assert.ok(
|
2018-07-10 18:28:52 +00:00
|
|
|
driverHead.healthClass.includes(driver.Healthy ? 'running' : 'failed'),
|
2018-05-13 03:01:51 +00:00
|
|
|
`${driver.Name}: Swatch with correct class is shown`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('each driver can be opened to see a message and attributes', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
// Only detected drivers can be expanded
|
|
|
|
const nodeDrivers = node.drivers;
|
|
|
|
Object.values(nodeDrivers).forEach(driver => {
|
|
|
|
driver.Detected = true;
|
|
|
|
});
|
|
|
|
node.drivers = nodeDrivers;
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
const driver = Object.keys(node.drivers)
|
|
|
|
.map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
|
|
|
|
.sortBy('Name')[0];
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2019-03-13 00:04:16 +00:00
|
|
|
const driverHead = ClientDetail.driverHeads.objectAt(0);
|
|
|
|
const driverBody = ClientDetail.driverBodies.objectAt(0);
|
2018-05-13 03:01:51 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.notOk(driverBody.descriptionIsShown, 'Driver health description is not shown');
|
|
|
|
assert.notOk(driverBody.attributesAreShown, 'Driver attributes section is not shown');
|
2019-03-14 06:44:53 +00:00
|
|
|
|
|
|
|
await driverHead.toggle();
|
2018-05-13 03:01:51 +00:00
|
|
|
assert.equal(
|
2018-07-10 18:28:52 +00:00
|
|
|
driverBody.description,
|
2018-05-13 03:01:51 +00:00
|
|
|
driver.HealthDescription,
|
|
|
|
'Driver health description is now shown'
|
|
|
|
);
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.ok(driverBody.attributesAreShown, 'Driver attributes section is now shown');
|
2018-05-13 03:01:51 +00:00
|
|
|
});
|
2018-05-30 18:20:59 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('the status light indicates when the node is ineligible for scheduling', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
node = server.create('node', {
|
2019-12-13 15:02:02 +00:00
|
|
|
drain: false,
|
2019-03-13 00:04:16 +00:00
|
|
|
schedulingEligibility: 'ineligible',
|
2021-02-01 18:00:34 +00:00
|
|
|
status: 'ready',
|
2019-03-13 00:04:16 +00:00
|
|
|
});
|
2018-05-30 18:20:59 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-05-30 18:20:59 +00:00
|
|
|
|
2018-07-10 18:28:52 +00:00
|
|
|
assert.equal(
|
|
|
|
ClientDetail.statusLight.objectAt(0).id,
|
|
|
|
'ineligible',
|
2018-05-30 18:20:59 +00:00
|
|
|
'Title status light is in the ineligible state'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('when the node has a drain strategy with a positive deadline, the drain stategy section prints the duration', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
const deadline = 5400000000000; // 1.5 hours in nanoseconds
|
|
|
|
const forceDeadline = moment().add(1, 'd');
|
|
|
|
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: true,
|
|
|
|
schedulingEligibility: 'ineligible',
|
|
|
|
drainStrategy: {
|
|
|
|
Deadline: deadline,
|
|
|
|
ForceDeadline: forceDeadline.toISOString(),
|
|
|
|
IgnoreSystemJobs: false,
|
|
|
|
},
|
|
|
|
});
|
2018-05-30 18:20:59 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-05-30 18:20:59 +00:00
|
|
|
|
|
|
|
assert.ok(
|
2019-12-05 00:54:36 +00:00
|
|
|
ClientDetail.drainDetails.deadline.includes(forceDeadline.fromNow(true)),
|
2018-05-30 18:20:59 +00:00
|
|
|
'Deadline is shown in a human formatted way'
|
|
|
|
);
|
|
|
|
|
2019-12-05 00:54:36 +00:00
|
|
|
assert.equal(
|
|
|
|
ClientDetail.drainDetails.deadlineTooltip,
|
|
|
|
forceDeadline.format("MMM DD, 'YY HH:mm:ss ZZ"),
|
|
|
|
'The tooltip for deadline shows the force deadline as an absolute date'
|
2018-05-30 18:20:59 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
assert.ok(
|
2019-12-05 00:54:36 +00:00
|
|
|
ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'),
|
|
|
|
'Drain System Jobs state is shown'
|
2018-05-30 18:20:59 +00:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('when the node has a drain stategy with no deadline, the drain stategy section mentions that and omits the force deadline', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
const deadline = 0;
|
|
|
|
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: true,
|
|
|
|
schedulingEligibility: 'ineligible',
|
|
|
|
drainStrategy: {
|
|
|
|
Deadline: deadline,
|
|
|
|
ForceDeadline: '0001-01-01T00:00:00Z', // null as a date
|
|
|
|
IgnoreSystemJobs: true,
|
|
|
|
},
|
|
|
|
});
|
2018-05-30 18:20:59 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-05-30 18:20:59 +00:00
|
|
|
|
2019-12-05 00:54:36 +00:00
|
|
|
assert.notOk(ClientDetail.drainDetails.durationIsShown, 'Duration is omitted');
|
|
|
|
|
2018-05-30 18:20:59 +00:00
|
|
|
assert.ok(
|
2019-12-05 00:54:36 +00:00
|
|
|
ClientDetail.drainDetails.deadline.includes('No deadline'),
|
2018-05-30 18:20:59 +00:00
|
|
|
'The value for Deadline is "no deadline"'
|
|
|
|
);
|
|
|
|
|
|
|
|
assert.ok(
|
2019-12-05 00:54:36 +00:00
|
|
|
ClientDetail.drainDetails.drainSystemJobsText.endsWith('No'),
|
|
|
|
'Drain System Jobs state is shown'
|
2018-05-30 18:20:59 +00:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('when the node has a drain stategy with a negative deadline, the drain strategy section shows the force badge', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
const deadline = -1;
|
|
|
|
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: true,
|
|
|
|
schedulingEligibility: 'ineligible',
|
|
|
|
drainStrategy: {
|
|
|
|
Deadline: deadline,
|
|
|
|
ForceDeadline: '0001-01-01T00:00:00Z', // null as a date
|
|
|
|
IgnoreSystemJobs: false,
|
|
|
|
},
|
|
|
|
});
|
2018-05-30 18:20:59 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-05-30 18:20:59 +00:00
|
|
|
|
2019-12-05 00:54:36 +00:00
|
|
|
assert.ok(
|
|
|
|
ClientDetail.drainDetails.forceDrainText.endsWith('Yes'),
|
|
|
|
'Forced Drain is described'
|
2018-05-30 18:20:59 +00:00
|
|
|
);
|
|
|
|
|
2019-12-05 00:54:36 +00:00
|
|
|
assert.ok(ClientDetail.drainDetails.duration.includes('--'), 'Duration is shown but unset');
|
|
|
|
|
|
|
|
assert.ok(ClientDetail.drainDetails.deadline.includes('--'), 'Deadline is shown but unset');
|
|
|
|
|
2018-05-30 18:20:59 +00:00
|
|
|
assert.ok(
|
2019-12-05 00:54:36 +00:00
|
|
|
ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'),
|
|
|
|
'Drain System Jobs state is shown'
|
2018-05-30 18:20:59 +00:00
|
|
|
);
|
|
|
|
});
|
2019-12-07 07:45:00 +00:00
|
|
|
|
|
|
|
test('toggling node eligibility disables the toggle and sends the correct POST request', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: false,
|
|
|
|
schedulingEligibility: 'eligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
server.pretender.post('/v1/node/:id/eligibility', () => [200, {}, ''], true);
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
assert.ok(ClientDetail.eligibilityToggle.isActive);
|
|
|
|
|
|
|
|
ClientDetail.eligibilityToggle.toggle();
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
await waitUntil(() => nonSearchPOSTS());
|
2019-12-07 07:45:00 +00:00
|
|
|
|
|
|
|
assert.ok(ClientDetail.eligibilityToggle.isDisabled);
|
|
|
|
server.pretender.resolve(server.pretender.requestReferences[0].request);
|
|
|
|
|
2020-05-21 05:29:59 +00:00
|
|
|
await settled();
|
|
|
|
|
2019-12-07 07:45:00 +00:00
|
|
|
assert.notOk(ClientDetail.eligibilityToggle.isActive);
|
|
|
|
assert.notOk(ClientDetail.eligibilityToggle.isDisabled);
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
const request = nonSearchPOSTS()[0];
|
2019-12-07 07:45:00 +00:00
|
|
|
assert.equal(request.url, `/v1/node/${node.id}/eligibility`);
|
|
|
|
assert.deepEqual(JSON.parse(request.requestBody), {
|
|
|
|
NodeID: node.id,
|
|
|
|
Eligibility: 'ineligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
ClientDetail.eligibilityToggle.toggle();
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
await waitUntil(() => nonSearchPOSTS().length === 2);
|
2019-12-07 07:45:00 +00:00
|
|
|
server.pretender.resolve(server.pretender.requestReferences[0].request);
|
|
|
|
|
|
|
|
assert.ok(ClientDetail.eligibilityToggle.isActive);
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
const request2 = nonSearchPOSTS()[1];
|
2019-12-07 07:45:00 +00:00
|
|
|
|
|
|
|
assert.equal(request2.url, `/v1/node/${node.id}/eligibility`);
|
|
|
|
assert.deepEqual(JSON.parse(request2.requestBody), {
|
|
|
|
NodeID: node.id,
|
|
|
|
Eligibility: 'eligible',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('starting a drain sends the correct POST request', async function(assert) {
|
|
|
|
let request;
|
|
|
|
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: false,
|
|
|
|
schedulingEligibility: 'eligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
await ClientDetail.drainPopover.submit();
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
request = nonSearchPOSTS().pop();
|
2019-12-07 07:45:00 +00:00
|
|
|
|
|
|
|
assert.equal(request.url, `/v1/node/${node.id}/drain`);
|
|
|
|
assert.deepEqual(
|
|
|
|
JSON.parse(request.requestBody),
|
|
|
|
{
|
|
|
|
NodeID: node.id,
|
|
|
|
DrainSpec: {
|
|
|
|
Deadline: 0,
|
|
|
|
IgnoreSystemJobs: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'Drain with default settings'
|
|
|
|
);
|
|
|
|
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
await ClientDetail.drainPopover.deadlineToggle.toggle();
|
|
|
|
await ClientDetail.drainPopover.submit();
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
request = nonSearchPOSTS().pop();
|
2019-12-07 07:45:00 +00:00
|
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
JSON.parse(request.requestBody),
|
|
|
|
{
|
|
|
|
NodeID: node.id,
|
|
|
|
DrainSpec: {
|
|
|
|
Deadline: 60 * 60 * 1000 * 1000000,
|
|
|
|
IgnoreSystemJobs: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'Drain with deadline toggled'
|
|
|
|
);
|
|
|
|
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
await ClientDetail.drainPopover.deadlineOptions.open();
|
|
|
|
await ClientDetail.drainPopover.deadlineOptions.options[1].choose();
|
|
|
|
await ClientDetail.drainPopover.submit();
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
request = nonSearchPOSTS().pop();
|
2019-12-07 07:45:00 +00:00
|
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
JSON.parse(request.requestBody),
|
|
|
|
{
|
|
|
|
NodeID: node.id,
|
|
|
|
DrainSpec: {
|
|
|
|
Deadline: 4 * 60 * 60 * 1000 * 1000000,
|
|
|
|
IgnoreSystemJobs: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'Drain with non-default preset deadline set'
|
|
|
|
);
|
|
|
|
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
await ClientDetail.drainPopover.deadlineOptions.open();
|
|
|
|
const optionsCount = ClientDetail.drainPopover.deadlineOptions.options.length;
|
|
|
|
await ClientDetail.drainPopover.deadlineOptions.options.objectAt(optionsCount - 1).choose();
|
|
|
|
await ClientDetail.drainPopover.setCustomDeadline('1h40m20s');
|
|
|
|
await ClientDetail.drainPopover.submit();
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
request = nonSearchPOSTS().pop();
|
2019-12-07 07:45:00 +00:00
|
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
JSON.parse(request.requestBody),
|
|
|
|
{
|
|
|
|
NodeID: node.id,
|
|
|
|
DrainSpec: {
|
|
|
|
Deadline: ((1 * 60 + 40) * 60 + 20) * 1000 * 1000000,
|
|
|
|
IgnoreSystemJobs: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'Drain with custom deadline set'
|
|
|
|
);
|
|
|
|
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
await ClientDetail.drainPopover.deadlineToggle.toggle();
|
|
|
|
await ClientDetail.drainPopover.forceDrainToggle.toggle();
|
|
|
|
await ClientDetail.drainPopover.submit();
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
request = nonSearchPOSTS().pop();
|
2019-12-07 07:45:00 +00:00
|
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
JSON.parse(request.requestBody),
|
|
|
|
{
|
|
|
|
NodeID: node.id,
|
|
|
|
DrainSpec: {
|
|
|
|
Deadline: -1,
|
|
|
|
IgnoreSystemJobs: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'Drain with force set'
|
|
|
|
);
|
|
|
|
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
await ClientDetail.drainPopover.systemJobsToggle.toggle();
|
|
|
|
await ClientDetail.drainPopover.submit();
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
request = nonSearchPOSTS().pop();
|
2019-12-07 07:45:00 +00:00
|
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
JSON.parse(request.requestBody),
|
|
|
|
{
|
|
|
|
NodeID: node.id,
|
|
|
|
DrainSpec: {
|
|
|
|
Deadline: -1,
|
|
|
|
IgnoreSystemJobs: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'Drain system jobs unset'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2021-10-22 14:51:31 +00:00
|
|
|
test('starting a drain persists options to localstorage', async function(assert) {
|
|
|
|
const nodes = server.createList('node', 2, {
|
|
|
|
drain: false,
|
|
|
|
schedulingEligibility: 'eligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: nodes[0].id });
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
|
|
|
|
// Change all options to non-default values.
|
|
|
|
await ClientDetail.drainPopover.deadlineToggle.toggle();
|
|
|
|
await ClientDetail.drainPopover.deadlineOptions.open();
|
|
|
|
const optionsCount = ClientDetail.drainPopover.deadlineOptions.options.length;
|
|
|
|
await ClientDetail.drainPopover.deadlineOptions.options.objectAt(optionsCount - 1).choose();
|
|
|
|
await ClientDetail.drainPopover.setCustomDeadline('1h40m20s');
|
|
|
|
await ClientDetail.drainPopover.forceDrainToggle.toggle();
|
|
|
|
await ClientDetail.drainPopover.systemJobsToggle.toggle();
|
|
|
|
|
|
|
|
await ClientDetail.drainPopover.submit();
|
|
|
|
|
|
|
|
const got = JSON.parse(window.localStorage.nomadDrainOptions);
|
|
|
|
const want = {
|
|
|
|
deadlineEnabled: true,
|
|
|
|
customDuration: '1h40m20s',
|
|
|
|
selectedDurationQuickOption: { label: 'Custom', value: 'custom' },
|
|
|
|
drainSystemJobs: false,
|
|
|
|
forceDrain: true,
|
|
|
|
};
|
|
|
|
assert.deepEqual(got, want);
|
|
|
|
|
|
|
|
// Visit another node and check that drain config is persisted.
|
|
|
|
await ClientDetail.visit({ id: nodes[1].id });
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
assert.true(ClientDetail.drainPopover.deadlineToggle.isActive);
|
|
|
|
assert.equal(ClientDetail.drainPopover.customDeadline, '1h40m20s');
|
|
|
|
assert.true(ClientDetail.drainPopover.forceDrainToggle.isActive);
|
|
|
|
assert.false(ClientDetail.drainPopover.systemJobsToggle.isActive);
|
|
|
|
});
|
|
|
|
|
2019-12-07 07:45:00 +00:00
|
|
|
test('the drain popover cancel button closes the popover', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: false,
|
|
|
|
schedulingEligibility: 'eligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
assert.notOk(ClientDetail.drainPopover.isOpen);
|
|
|
|
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
assert.ok(ClientDetail.drainPopover.isOpen);
|
|
|
|
|
|
|
|
await ClientDetail.drainPopover.cancel();
|
|
|
|
assert.notOk(ClientDetail.drainPopover.isOpen);
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
assert.equal(nonSearchPOSTS(), 0);
|
2019-12-07 07:45:00 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
test('toggling eligibility is disabled while a drain is active', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: true,
|
|
|
|
schedulingEligibility: 'ineligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
assert.ok(ClientDetail.eligibilityToggle.isDisabled);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('stopping a drain sends the correct POST request', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: true,
|
|
|
|
schedulingEligibility: 'ineligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
assert.ok(ClientDetail.stopDrainIsPresent);
|
|
|
|
|
|
|
|
await ClientDetail.stopDrain.idle();
|
|
|
|
await ClientDetail.stopDrain.confirm();
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
const request = nonSearchPOSTS()[0];
|
2019-12-07 07:45:00 +00:00
|
|
|
assert.equal(request.url, `/v1/node/${node.id}/drain`);
|
|
|
|
assert.deepEqual(JSON.parse(request.requestBody), {
|
|
|
|
NodeID: node.id,
|
|
|
|
DrainSpec: null,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('when a drain is active, the "drain" popover is labeled as the "update" popover', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: true,
|
|
|
|
schedulingEligibility: 'ineligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
assert.equal(ClientDetail.drainPopover.label, 'Update Drain');
|
|
|
|
});
|
|
|
|
|
|
|
|
test('forcing a drain sends the correct POST request', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: true,
|
|
|
|
schedulingEligibility: 'ineligible',
|
|
|
|
drainStrategy: {
|
|
|
|
Deadline: 0,
|
|
|
|
IgnoreSystemJobs: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.drainDetails.force.idle();
|
|
|
|
await ClientDetail.drainDetails.force.confirm();
|
|
|
|
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
const request = nonSearchPOSTS()[0];
|
2019-12-07 07:45:00 +00:00
|
|
|
assert.equal(request.url, `/v1/node/${node.id}/drain`);
|
|
|
|
assert.deepEqual(JSON.parse(request.requestBody), {
|
|
|
|
NodeID: node.id,
|
|
|
|
DrainSpec: {
|
|
|
|
Deadline: -1,
|
|
|
|
IgnoreSystemJobs: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('when stopping a drain fails, an error is shown', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: true,
|
|
|
|
schedulingEligibility: 'ineligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']);
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.stopDrain.idle();
|
|
|
|
await ClientDetail.stopDrain.confirm();
|
|
|
|
|
|
|
|
assert.ok(ClientDetail.stopDrainError.isPresent);
|
|
|
|
assert.ok(ClientDetail.stopDrainError.title.includes('Stop Drain Error'));
|
|
|
|
|
|
|
|
await ClientDetail.stopDrainError.dismiss();
|
|
|
|
assert.notOk(ClientDetail.stopDrainError.isPresent);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('when starting a drain fails, an error message is shown', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: false,
|
|
|
|
schedulingEligibility: 'eligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']);
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
await ClientDetail.drainPopover.submit();
|
|
|
|
|
|
|
|
assert.ok(ClientDetail.drainError.isPresent);
|
|
|
|
assert.ok(ClientDetail.drainError.title.includes('Drain Error'));
|
|
|
|
|
|
|
|
await ClientDetail.drainError.dismiss();
|
|
|
|
assert.notOk(ClientDetail.drainError.isPresent);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('when updating a drain fails, an error message is shown', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: true,
|
|
|
|
schedulingEligibility: 'ineligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']);
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.drainPopover.toggle();
|
|
|
|
await ClientDetail.drainPopover.submit();
|
|
|
|
|
|
|
|
assert.ok(ClientDetail.drainError.isPresent);
|
|
|
|
assert.ok(ClientDetail.drainError.title.includes('Drain Error'));
|
|
|
|
|
|
|
|
await ClientDetail.drainError.dismiss();
|
|
|
|
assert.notOk(ClientDetail.drainError.isPresent);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('when toggling eligibility fails, an error message is shown', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: false,
|
|
|
|
schedulingEligibility: 'eligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']);
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.eligibilityToggle.toggle();
|
|
|
|
|
|
|
|
assert.ok(ClientDetail.eligibilityError.isPresent);
|
|
|
|
assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error'));
|
|
|
|
|
|
|
|
await ClientDetail.eligibilityError.dismiss();
|
|
|
|
assert.notOk(ClientDetail.eligibilityError.isPresent);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('when navigating away from a client that has an error message to another client, the error is not shown', async function(assert) {
|
|
|
|
node = server.create('node', {
|
|
|
|
drain: false,
|
|
|
|
schedulingEligibility: 'eligible',
|
|
|
|
});
|
|
|
|
|
|
|
|
const node2 = server.create('node');
|
|
|
|
|
|
|
|
server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']);
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
await ClientDetail.eligibilityToggle.toggle();
|
|
|
|
|
|
|
|
assert.ok(ClientDetail.eligibilityError.isPresent);
|
|
|
|
assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error'));
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node2.id });
|
|
|
|
|
|
|
|
assert.notOk(ClientDetail.eligibilityError.isPresent);
|
|
|
|
});
|
2020-01-31 05:28:37 +00:00
|
|
|
|
|
|
|
test('toggling eligibility and node drain are disabled when the active ACL token does not permit node write', async function(assert) {
|
|
|
|
window.localStorage.nomadTokenSecret = clientToken.secretId;
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
assert.ok(ClientDetail.eligibilityToggle.isDisabled);
|
|
|
|
assert.ok(ClientDetail.drainPopover.isDisabled);
|
|
|
|
});
|
2020-02-11 04:36:51 +00:00
|
|
|
|
|
|
|
test('the host volumes table lists all host volumes in alphabetical order by name', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
|
2020-02-15 00:44:28 +00:00
|
|
|
const sortedHostVolumes = Object.keys(node.hostVolumes)
|
|
|
|
.map(key => node.hostVolumes[key])
|
|
|
|
.sortBy('Name');
|
|
|
|
|
2020-02-11 04:36:51 +00:00
|
|
|
assert.ok(ClientDetail.hasHostVolumes);
|
|
|
|
assert.equal(ClientDetail.hostVolumes.length, Object.keys(node.hostVolumes).length);
|
2020-02-15 00:44:28 +00:00
|
|
|
|
|
|
|
ClientDetail.hostVolumes.forEach((volume, index) => {
|
|
|
|
assert.equal(volume.name, sortedHostVolumes[index].Name);
|
|
|
|
});
|
2020-02-11 04:36:51 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
test('each host volume row contains information about the host volume', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
|
|
|
|
const sortedHostVolumes = Object.keys(node.hostVolumes)
|
|
|
|
.map(key => node.hostVolumes[key])
|
|
|
|
.sortBy('Name');
|
|
|
|
|
|
|
|
ClientDetail.hostVolumes[0].as(volume => {
|
|
|
|
const volumeRow = sortedHostVolumes[0];
|
|
|
|
assert.equal(volume.name, volumeRow.Name);
|
|
|
|
assert.equal(volume.path, volumeRow.Path);
|
|
|
|
assert.equal(volume.permissions, volumeRow.ReadOnly ? 'Read' : 'Read/Write');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('the host volumes table is not shown if the client has no host volumes', async function(assert) {
|
|
|
|
node = server.create('node', 'noHostVolumes');
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
|
|
|
|
assert.notOk(ClientDetail.hasHostVolumes);
|
|
|
|
});
|
2021-12-17 21:55:40 +00:00
|
|
|
|
|
|
|
testFacet('Job', {
|
|
|
|
facet: ClientDetail.facets.job,
|
|
|
|
paramName: 'job',
|
|
|
|
expectedOptions(allocs) {
|
|
|
|
return Array.from(new Set(allocs.mapBy('jobId'))).sort();
|
|
|
|
},
|
|
|
|
async beforeEach() {
|
|
|
|
server.createList('job', 5);
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
},
|
|
|
|
filter: (alloc, selection) => selection.includes(alloc.jobId),
|
|
|
|
});
|
|
|
|
|
|
|
|
testFacet('Status', {
|
|
|
|
facet: ClientDetail.facets.status,
|
|
|
|
paramName: 'status',
|
|
|
|
expectedOptions: ['Pending', 'Running', 'Complete', 'Failed', 'Lost'],
|
|
|
|
async beforeEach() {
|
|
|
|
server.createList('job', 5, { createAllocations: false });
|
|
|
|
['pending', 'running', 'complete', 'failed', 'lost'].forEach(s => {
|
|
|
|
server.createList('allocation', 5, { clientStatus: s });
|
|
|
|
});
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
},
|
|
|
|
filter: (alloc, selection) => selection.includes(alloc.clientStatus),
|
|
|
|
});
|
2021-12-17 22:38:04 +00:00
|
|
|
|
|
|
|
test('fiter results with no matches display empty message', async function(assert) {
|
|
|
|
const job = server.create('job', { createAllocations: false });
|
|
|
|
server.create('allocation', { jobId: job.id, clientStatus: 'running' });
|
|
|
|
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
const statusFacet = ClientDetail.facets.status;
|
|
|
|
await statusFacet.toggle();
|
|
|
|
await statusFacet.options.objectAt(0).toggle();
|
|
|
|
|
|
|
|
assert.true(ClientDetail.emptyAllocations.isVisible);
|
|
|
|
assert.equal(ClientDetail.emptyAllocations.headline, 'No Matches');
|
|
|
|
});
|
2018-05-30 18:20:59 +00:00
|
|
|
});
|
2018-07-06 17:50:22 +00:00
|
|
|
|
2019-03-13 00:04:16 +00:00
|
|
|
module('Acceptance | client detail (multi-namespace)', function(hooks) {
|
|
|
|
setupApplicationTest(hooks);
|
2019-03-14 06:44:53 +00:00
|
|
|
setupMirage(hooks);
|
2019-03-13 00:04:16 +00:00
|
|
|
|
|
|
|
hooks.beforeEach(function() {
|
2018-07-06 17:50:22 +00:00
|
|
|
server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' });
|
|
|
|
node = server.db.nodes[0];
|
|
|
|
|
|
|
|
// Related models
|
|
|
|
server.create('namespace');
|
|
|
|
server.create('namespace', { id: 'other-namespace' });
|
|
|
|
|
|
|
|
server.create('agent');
|
|
|
|
|
|
|
|
// Make a job for each namespace, but have both scheduled on the same node
|
|
|
|
server.create('job', { id: 'job-1', namespaceId: 'default', createAllocations: false });
|
2021-12-17 21:55:40 +00:00
|
|
|
server.createList('allocation', 3, {
|
|
|
|
nodeId: node.id,
|
|
|
|
jobId: 'job-1',
|
|
|
|
clientStatus: 'running',
|
|
|
|
});
|
2018-07-06 17:50:22 +00:00
|
|
|
|
|
|
|
server.create('job', { id: 'job-2', namespaceId: 'other-namespace', createAllocations: false });
|
2018-12-10 23:25:48 +00:00
|
|
|
server.createList('allocation', 3, {
|
|
|
|
nodeId: node.id,
|
|
|
|
jobId: 'job-2',
|
|
|
|
clientStatus: 'running',
|
|
|
|
});
|
2019-03-13 00:04:16 +00:00
|
|
|
});
|
2018-07-06 17:50:22 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
test('when the node has allocations on different namespaces, the associated jobs are fetched correctly', async function(assert) {
|
2019-03-13 00:04:16 +00:00
|
|
|
window.localStorage.nomadActiveNamespace = 'other-namespace';
|
2018-07-06 17:50:22 +00:00
|
|
|
|
2019-03-14 06:44:53 +00:00
|
|
|
await ClientDetail.visit({ id: node.id });
|
2018-07-06 17:50:22 +00:00
|
|
|
|
|
|
|
assert.equal(
|
2018-07-11 19:35:39 +00:00
|
|
|
ClientDetail.allocations.length,
|
2018-07-06 17:50:22 +00:00
|
|
|
server.db.allocations.length,
|
|
|
|
'All allocations are scheduled on this node'
|
|
|
|
);
|
|
|
|
assert.ok(
|
|
|
|
server.pretender.handledRequests.findBy('url', '/v1/job/job-1'),
|
|
|
|
'Job One fetched correctly'
|
|
|
|
);
|
|
|
|
assert.ok(
|
|
|
|
server.pretender.handledRequests.findBy('url', '/v1/job/job-2?namespace=other-namespace'),
|
|
|
|
'Job Two fetched correctly'
|
|
|
|
);
|
|
|
|
});
|
2021-12-17 21:55:40 +00:00
|
|
|
|
|
|
|
testFacet('Namespace', {
|
|
|
|
facet: ClientDetail.facets.namespace,
|
|
|
|
paramName: 'namespace',
|
|
|
|
expectedOptions(allocs) {
|
|
|
|
return Array.from(new Set(allocs.mapBy('namespace'))).sort();
|
|
|
|
},
|
|
|
|
async beforeEach() {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
},
|
|
|
|
filter: (alloc, selection) => selection.includes(alloc.namespace),
|
|
|
|
});
|
|
|
|
|
|
|
|
test('facet Namespace | selecting namespace filters job options', async function(assert) {
|
|
|
|
await ClientDetail.visit({ id: node.id });
|
|
|
|
|
|
|
|
const nsFacet = ClientDetail.facets.namespace;
|
|
|
|
const jobFacet = ClientDetail.facets.job;
|
|
|
|
|
|
|
|
// Select both namespaces.
|
|
|
|
await nsFacet.toggle();
|
|
|
|
await nsFacet.options.objectAt(0).toggle();
|
|
|
|
await nsFacet.options.objectAt(1).toggle();
|
|
|
|
await jobFacet.toggle();
|
|
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
jobFacet.options.map(option => option.label.trim()),
|
|
|
|
['job-1', 'job-2']
|
|
|
|
);
|
|
|
|
|
|
|
|
// Select juse one namespace.
|
|
|
|
await nsFacet.toggle();
|
|
|
|
await nsFacet.options.objectAt(1).toggle(); // deselect second option
|
|
|
|
await jobFacet.toggle();
|
|
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
jobFacet.options.map(option => option.label.trim()),
|
|
|
|
['job-1']
|
|
|
|
);
|
|
|
|
});
|
2018-07-06 17:50:22 +00:00
|
|
|
});
|
2021-12-17 21:55:40 +00:00
|
|
|
|
|
|
|
function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) {
|
|
|
|
test(`facet ${label} | the ${label} facet has the correct options`, async function(assert) {
|
|
|
|
await beforeEach();
|
|
|
|
await facet.toggle();
|
|
|
|
|
|
|
|
let expectation;
|
|
|
|
if (typeof expectedOptions === 'function') {
|
|
|
|
expectation = expectedOptions(server.db.allocations);
|
|
|
|
} else {
|
|
|
|
expectation = expectedOptions;
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
facet.options.map(option => option.label.trim()),
|
|
|
|
expectation,
|
|
|
|
'Options for facet are as expected'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test(`facet ${label} | the ${label} facet filters the allocations list by ${label}`, async function(assert) {
|
|
|
|
let option;
|
|
|
|
|
|
|
|
await beforeEach();
|
|
|
|
|
|
|
|
await facet.toggle();
|
|
|
|
option = facet.options.objectAt(0);
|
|
|
|
await option.toggle();
|
|
|
|
|
|
|
|
const selection = [option.key];
|
|
|
|
const expectedAllocs = server.db.allocations
|
|
|
|
.filter(alloc => filter(alloc, selection))
|
|
|
|
.sortBy('modifyIndex')
|
|
|
|
.reverse();
|
|
|
|
|
|
|
|
ClientDetail.allocations.forEach((alloc, index) => {
|
|
|
|
assert.equal(
|
|
|
|
alloc.id,
|
|
|
|
expectedAllocs[index].id,
|
|
|
|
`Allocation at ${index} is ${expectedAllocs[index].id}`
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test(`facet ${label} | selecting multiple options in the ${label} facet results in a broader search`, async function(assert) {
|
|
|
|
const selection = [];
|
|
|
|
|
|
|
|
await beforeEach();
|
|
|
|
await facet.toggle();
|
|
|
|
|
|
|
|
const option1 = facet.options.objectAt(0);
|
|
|
|
const option2 = facet.options.objectAt(1);
|
|
|
|
await option1.toggle();
|
|
|
|
selection.push(option1.key);
|
|
|
|
await option2.toggle();
|
|
|
|
selection.push(option2.key);
|
|
|
|
|
|
|
|
const expectedAllocs = server.db.allocations
|
|
|
|
.filter(alloc => filter(alloc, selection))
|
|
|
|
.sortBy('modifyIndex')
|
|
|
|
.reverse();
|
|
|
|
|
|
|
|
ClientDetail.allocations.forEach((alloc, index) => {
|
|
|
|
assert.equal(
|
|
|
|
alloc.id,
|
|
|
|
expectedAllocs[index].id,
|
|
|
|
`Allocation at ${index} is ${expectedAllocs[index].id}`
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test(`facet ${label} | selecting options in the ${label} facet updates the ${paramName} query param`, async function(assert) {
|
|
|
|
const selection = [];
|
|
|
|
|
|
|
|
await beforeEach();
|
|
|
|
await facet.toggle();
|
|
|
|
|
|
|
|
const option1 = facet.options.objectAt(0);
|
|
|
|
const option2 = facet.options.objectAt(1);
|
|
|
|
await option1.toggle();
|
|
|
|
selection.push(option1.key);
|
|
|
|
await option2.toggle();
|
|
|
|
selection.push(option2.key);
|
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
currentURL(),
|
|
|
|
`/clients/${node.id}?${paramName}=${encodeURIComponent(JSON.stringify(selection))}`,
|
|
|
|
'URL has the correct query param key and value'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|