- {{#if @model}}
+ {{#if this.summaries}}
+ @placeholder="Search {{this.summaries.length}} {{pluralize "recommendation" this.summaries.length}}..." />
{{/if}}
- {{#if this.system.shouldShowNamespaces}}
-
- {{/if}}
{{#if this.filteredSummaries}}
{{outlet}}
diff --git a/ui/app/utils/qp-serialize.js b/ui/app/utils/qp-serialize.js
index af2d5769e..7717efc28 100644
--- a/ui/app/utils/qp-serialize.js
+++ b/ui/app/utils/qp-serialize.js
@@ -1,7 +1,10 @@
import { computed } from '@ember/object';
// An unattractive but robust way to encode query params
-export const serialize = arr => (arr.length ? JSON.stringify(arr) : '');
+export const serialize = val => {
+ if (typeof val === 'string' || typeof val === 'number') return val;
+ return val.length ? JSON.stringify(val) : '';
+};
export const deserialize = str => {
try {
diff --git a/ui/mirage/config.js b/ui/mirage/config.js
index 2cee56d13..fd2da7139 100644
--- a/ui/mirage/config.js
+++ b/ui/mirage/config.js
@@ -57,11 +57,12 @@ export default function() {
const json = this.serialize(jobs.all());
const namespace = queryParams.namespace || 'default';
return json
- .filter(job =>
- namespace === 'default'
+ .filter(job => {
+ if (namespace === '*') return true;
+ return namespace === 'default'
? !job.NamespaceID || job.NamespaceID === namespace
- : job.NamespaceID === namespace
- )
+ : job.NamespaceID === namespace;
+ })
.map(job => filterKeys(job, 'TaskGroups', 'NamespaceID'));
})
);
@@ -262,29 +263,33 @@ export default function() {
const json = this.serialize(csiVolumes.all());
const namespace = queryParams.namespace || 'default';
- return json.filter(volume =>
- namespace === 'default'
+ return json.filter(volume => {
+ if (namespace === '*') return true;
+ return namespace === 'default'
? !volume.NamespaceID || volume.NamespaceID === namespace
- : volume.NamespaceID === namespace
- );
+ : volume.NamespaceID === namespace;
+ });
})
);
this.get(
'/volume/:id',
- withBlockingSupport(function({ csiVolumes }, { params }) {
+ withBlockingSupport(function({ csiVolumes }, { params, queryParams }) {
if (!params.id.startsWith('csi/')) {
return new Response(404, {}, null);
}
const id = params.id.replace(/^csi\//, '');
- const volume = csiVolumes.find(id);
+ const volume = csiVolumes.all().models.find(volume => {
+ const volumeIsDefault = !volume.namespaceId || volume.namespaceId === 'default';
+ const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default';
+ return (
+ volume.id === id &&
+ (volume.namespaceId === queryParams.namespace || (volumeIsDefault && qpIsDefault))
+ );
+ });
- if (!volume) {
- return new Response(404, {}, null);
- }
-
- return this.serialize(volume);
+ return volume ? this.serialize(volume) : new Response(404, {}, null);
})
);
@@ -318,15 +323,11 @@ export default function() {
return this.serialize(records);
}
- return new Response(501, {}, null);
+ return this.serialize([{ Name: 'default' }]);
});
this.get('/namespace/:id', function({ namespaces }, { params }) {
- if (namespaces.all().length) {
- return this.serialize(namespaces.find(params.id));
- }
-
- return new Response(501, {}, null);
+ return this.serialize(namespaces.find(params.id));
});
this.get('/agent/members', function({ agents, regions }) {
diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js
index e6b5c1da2..8d2423f86 100644
--- a/ui/mirage/scenarios/default.js
+++ b/ui/mirage/scenarios/default.js
@@ -39,6 +39,7 @@ export default function(server) {
// Scenarios
function smallCluster(server) {
+ server.create('feature', { name: 'Dynamic Application Sizing' });
server.createList('agent', 3);
server.createList('node', 5);
server.createList('job', 5, { createRecommendations: true });
diff --git a/ui/stories/components/filter-facets.stories.js b/ui/stories/components/filter-facets.stories.js
new file mode 100644
index 000000000..b85848bc6
--- /dev/null
+++ b/ui/stories/components/filter-facets.stories.js
@@ -0,0 +1,207 @@
+import hbs from 'htmlbars-inline-precompile';
+
+export default {
+ title: 'Components|Filter Facets',
+};
+
+let options1 = [
+ { key: 'option-1', label: 'Option One' },
+ { key: 'option-2', label: 'Option Two' },
+ { key: 'option-3', label: 'Option Three' },
+ { key: 'option-4', label: 'Option Four' },
+ { key: 'option-5', label: 'Option Five' },
+];
+
+let selection1 = ['option-2', 'option-4', 'option-5'];
+
+export let MultiSelect = () => {
+ return {
+ template: hbs`
+
Multi-Select Dropdown
+
+
A wrapper around basic-dropdown for creating a list of checkboxes and tracking the state thereof.
+ `,
+ context: {
+ options1,
+ selection1,
+ },
+ };
+};
+
+export let SingleSelect = () => ({
+ template: hbs`
+
Single-Select Dropdown
+
+ `,
+ context: {
+ options1,
+ selection: 'option-2',
+ },
+});
+
+export let RightAligned = () => {
+ return {
+ template: hbs`
+
Multi-Select Dropdown right-aligned
+
+
+
+ `,
+ context: {
+ options1,
+ selection1,
+ },
+ };
+};
+
+export let ManyOptionsMulti = () => {
+ return {
+ template: hbs`
+
Multi-Select Dropdown with many options
+
+
+ A strength of the multi-select-dropdown is its simple presentation. It is quick to select options and it is quick to remove options.
+ However, this strength becomes a weakness when there are too many options. Since the selection isn't pinned in any way, removing a selection
+ can become an adventure of scrolling up and down. Also since the selection isn't pinned, this component can't support search, since search would
+ entirely mask the selection.
+
+ `,
+ context: {
+ optionsMany: Array(100)
+ .fill(null)
+ .map((_, i) => ({ label: `Option ${i}`, key: `option-${i}` })),
+ selectionMany: [],
+ },
+ };
+};
+
+export let ManyOptionsSingle = () => {
+ return {
+ template: hbs`
+
Single-Select Dropdown with many options
+
+
+ Single select supports search at a certain option threshold via Ember Power Select.
+
+ `,
+ context: {
+ optionsMany: Array(100)
+ .fill(null)
+ .map((_, i) => ({ label: `Option ${i}`, key: `option-${i}` })),
+ selection: 'option-1',
+ },
+ };
+};
+
+export let Bar = () => {
+ return {
+ template: hbs`
+
Multi-Select Dropdown bar
+
+
+
+
+
+
Single-Select Dropdown bar
+
+
+
+
+
+
Mixed Dropdown bar
+
+
+
+
+
+
+ Since this is a core component for faceted search, it makes sense to letruct an arrangement of multi-select dropdowns.
+ Do this by wrapping all the options in a .button-bar container.
+
+ `,
+ context: {
+ optionsDatacenter: [
+ { key: 'pdx-1', label: 'pdx-1' },
+ { key: 'jfk-1', label: 'jfk-1' },
+ { key: 'jfk-2', label: 'jfk-2' },
+ { key: 'muc-1', label: 'muc-1' },
+ ],
+ selectionDatacenter: ['jfk-1'],
+ selectionDatacenterSingle: 'jfk-1',
+
+ optionsType: [
+ { key: 'batch', label: 'Batch' },
+ { key: 'service', label: 'Service' },
+ { key: 'system', label: 'System' },
+ { key: 'periodic', label: 'Periodic' },
+ { key: 'parameterized', label: 'Parameterized' },
+ ],
+ selectionType: ['system', 'service'],
+ selectionTypeSingle: 'system',
+
+ optionsStatus: [
+ { key: 'pending', label: 'Pending' },
+ { key: 'running', label: 'Running' },
+ { key: 'dead', label: 'Dead' },
+ ],
+ selectionStatus: [],
+ selectionStatusSingle: 'dead',
+ },
+ };
+};
diff --git a/ui/stories/components/multi-select-dropdown.stories.js b/ui/stories/components/multi-select-dropdown.stories.js
deleted file mode 100644
index e20d0e84e..000000000
--- a/ui/stories/components/multi-select-dropdown.stories.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import hbs from 'htmlbars-inline-precompile';
-
-export default {
- title: 'Components|Multi-Select Dropdown',
-};
-
-let options1 = [
- { key: 'option-1', label: 'Option One' },
- { key: 'option-2', label: 'Option Two' },
- { key: 'option-3', label: 'Option Three' },
- { key: 'option-4', label: 'Option Four' },
- { key: 'option-5', label: 'Option Five' },
-];
-
-let selection1 = ['option-2', 'option-4', 'option-5'];
-
-export let Standard = () => {
- return {
- template: hbs`
-
Multi-Select Dropdown
-
-
A wrapper around basic-dropdown for creating a list of checkboxes and tracking the state thereof.
- `,
- context: {
- options1,
- selection1,
- },
- };
-};
-
-export let RightAligned = () => {
- return {
- template: hbs`
-
Multi-Select Dropdown right-aligned
-
-
-
- `,
- context: {
- options1,
- selection1,
- },
- };
-};
-
-export let ManyOptions = () => {
- return {
- template: hbs`
-
Multi-Select Dropdown with many options
-
-
- A strength of the multi-select-dropdown is its simple presentation. It is quick to select options and it is quick to remove options.
- However, this strength becomes a weakness when there are too many options. Since the selection isn't pinned in any way, removing a selection
- can become an adventure of scrolling up and down. Also since the selection isn't pinned, this component can't support search, since search would
- entirely mask the selection.
-
- `,
- context: {
- optionsMany: Array(100)
- .fill(null)
- .map((_, i) => ({ label: `Option ${i}`, key: `option-${i}` })),
- selectionMany: [],
- },
- };
-};
-
-export let Bar = () => {
- return {
- template: hbs`
-
Multi-Select Dropdown bar
-
-
-
-
-
-
- Since this is a core component for faceted search, it makes sense to letruct an arrangement of multi-select dropdowns.
- Do this by wrapping all the options in a .button-bar container.
-
- `,
- context: {
- optionsDatacenter: [
- { key: 'pdx-1', label: 'pdx-1' },
- { key: 'jfk-1', label: 'jfk-1' },
- { key: 'jfk-2', label: 'jfk-2' },
- { key: 'muc-1', label: 'muc-1' },
- ],
- selectionDatacenter: ['jfk-1', 'jfk-2'],
-
- optionsType: [
- { key: 'batch', label: 'Batch' },
- { key: 'service', label: 'Service' },
- { key: 'system', label: 'System' },
- { key: 'periodic', label: 'Periodic' },
- { key: 'parameterized', label: 'Parameterized' },
- ],
- selectionType: ['system', 'service'],
-
- optionsStatus: [
- { key: 'pending', label: 'Pending' },
- { key: 'running', label: 'Running' },
- { key: 'dead', label: 'Dead' },
- ],
- selectionStatus: [],
- },
- };
-};
diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js
index 07ea9408b..ebbeea015 100644
--- a/ui/tests/acceptance/job-detail-test.js
+++ b/ui/tests/acceptance/job-detail-test.js
@@ -2,13 +2,11 @@
import { currentURL } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
-import { selectChoose } from 'ember-power-select/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
import moment from 'moment';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
import moduleForJob from 'nomad-ui/tests/helpers/module-for-job';
import JobDetail from 'nomad-ui/tests/pages/jobs/detail';
-import JobsList from 'nomad-ui/tests/pages/jobs/list';
moduleForJob('Acceptance | job detail (batch)', 'allocations', () =>
server.create('job', { type: 'batch', shallow: true })
@@ -128,27 +126,6 @@ module('Acceptance | job detail (with namespaces)', function(hooks) {
assert.ok(JobDetail.statFor('namespace').text, 'Namespace included in stats');
});
- test('when switching namespaces, the app redirects to /jobs with the new namespace', async function(assert) {
- const namespace = server.db.namespaces.find(job.namespaceId);
- const otherNamespace = server.db.namespaces.toArray().find(ns => ns !== namespace).name;
-
- await JobDetail.visit({ id: job.id, namespace: namespace.name });
-
- // TODO: Migrate to Page Objects
- await selectChoose('[data-test-namespace-switcher-parent]', otherNamespace);
- assert.equal(currentURL().split('?')[0], '/jobs', 'Navigated to /jobs');
-
- const jobs = server.db.jobs
- .where({ namespace: otherNamespace })
- .sortBy('modifyIndex')
- .reverse();
-
- assert.equal(JobsList.jobs.length, jobs.length, 'Shows the right number of jobs');
- JobsList.jobs.forEach((jobRow, index) => {
- assert.equal(jobRow.name, jobs[index].name, `Job ${index} is right`);
- });
- });
-
test('the exec button state can change between namespaces', async function(assert) {
const job1 = server.create('job', {
status: 'running',
diff --git a/ui/tests/acceptance/jobs-list-test.js b/ui/tests/acceptance/jobs-list-test.js
index 5100fc2f9..538225a43 100644
--- a/ui/tests/acceptance/jobs-list-test.js
+++ b/ui/tests/acceptance/jobs-list-test.js
@@ -59,6 +59,7 @@ module('Acceptance | jobs list', function(hooks) {
const jobRow = JobsList.jobs.objectAt(0);
assert.equal(jobRow.name, job.name, 'Name');
+ assert.notOk(jobRow.hasNamespace);
assert.equal(jobRow.link, `/ui/jobs/${job.id}`, 'Detail Link');
assert.equal(jobRow.status, job.status, 'Status');
assert.equal(jobRow.type, typeForJob(job), 'Type');
@@ -93,41 +94,6 @@ module('Acceptance | jobs list', function(hooks) {
assert.equal(currentURL(), '/jobs');
});
- test('the job run button state can change between namespaces', async function(assert) {
- server.createList('namespace', 2);
- const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id });
- const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id });
-
- window.localStorage.nomadTokenSecret = clientToken.secretId;
-
- const policy = server.create('policy', {
- id: 'something',
- name: 'something',
- rulesJSON: {
- Namespaces: [
- {
- Name: job1.namespaceId,
- Capabilities: ['list-jobs', 'submit-job'],
- },
- {
- Name: job2.namespaceId,
- Capabilities: ['list-jobs'],
- },
- ],
- },
- });
-
- clientToken.policyIds = [policy.id];
- clientToken.save();
-
- await JobsList.visit();
- assert.notOk(JobsList.runJobButton.isDisabled);
-
- const secondNamespace = server.db.namespaces[1];
- await JobsList.visit({ namespace: secondNamespace.id });
- assert.ok(JobsList.runJobButton.isDisabled);
- });
-
test('the anonymous policy is fetched to check whether to show the job run button', async function(assert) {
window.localStorage.removeItem('nomadTokenSecret');
@@ -179,7 +145,18 @@ module('Acceptance | jobs list', function(hooks) {
assert.equal(currentURL(), '/jobs?search=foobar', 'No page query param');
});
- test('when the namespace query param is set, only matching jobs are shown and the namespace value is forwarded to app state', async function(assert) {
+ test('when a cluster has namespaces, each job row includes the job namespace', async function(assert) {
+ server.createList('namespace', 2);
+ server.createList('job', 2);
+ const job = server.db.jobs.sortBy('modifyIndex').reverse()[0];
+
+ await JobsList.visit({ namespace: '*' });
+
+ const jobRow = JobsList.jobs.objectAt(0);
+ assert.equal(jobRow.namespace, job.namespaceId);
+ });
+
+ test('when the namespace query param is set, only matching jobs are shown', async function(assert) {
server.createList('namespace', 2);
const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id });
const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id });
@@ -213,12 +190,30 @@ module('Acceptance | jobs list', function(hooks) {
test('the jobs list page has appropriate faceted search options', async function(assert) {
await JobsList.visit();
+ assert.ok(JobsList.facets.namespace.isHidden, 'Namespace facet not found (no namespaces)');
assert.ok(JobsList.facets.type.isPresent, 'Type facet found');
assert.ok(JobsList.facets.status.isPresent, 'Status facet found');
assert.ok(JobsList.facets.datacenter.isPresent, 'Datacenter facet found');
assert.ok(JobsList.facets.prefix.isPresent, 'Prefix facet found');
});
+ testSingleSelectFacet('Namespace', {
+ facet: JobsList.facets.namespace,
+ paramName: 'namespace',
+ expectedOptions: ['All (*)', 'default', 'namespace-2'],
+ optionToSelect: 'namespace-2',
+ async beforeEach() {
+ server.create('namespace', { id: 'default' });
+ server.create('namespace', { id: 'namespace-2' });
+ server.createList('job', 2, { namespaceId: 'default' });
+ server.createList('job', 2, { namespaceId: 'namespace-2' });
+ await JobsList.visit();
+ },
+ filter(job, selection) {
+ return job.namespaceId === selection;
+ },
+ });
+
testFacet('Type', {
facet: JobsList.facets.type,
paramName: 'type',
@@ -352,7 +347,9 @@ module('Acceptance | jobs list', function(hooks) {
server.createList('namespace', 2);
const namespace = server.db.namespaces[1];
- await JobsList.visit({ namespace: namespace.id });
+ await JobsList.visit();
+ await JobsList.facets.namespace.toggle();
+ await JobsList.facets.namespace.options.objectAt(2).select();
await Layout.gutter.visitStorage();
@@ -369,24 +366,73 @@ module('Acceptance | jobs list', function(hooks) {
},
});
- function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) {
+ async function facetOptions(assert, beforeEach, facet, expectedOptions) {
+ await beforeEach();
+ await facet.toggle();
+
+ let expectation;
+ if (typeof expectedOptions === 'function') {
+ expectation = expectedOptions(server.db.jobs);
+ } else {
+ expectation = expectedOptions;
+ }
+
+ assert.deepEqual(
+ facet.options.map(option => option.label.trim()),
+ expectation,
+ 'Options for facet are as expected'
+ );
+ }
+
+ function testSingleSelectFacet(
+ label,
+ { facet, paramName, beforeEach, filter, expectedOptions, optionToSelect }
+ ) {
test(`the ${label} facet has the correct options`, async function(assert) {
+ await facetOptions(assert, beforeEach, facet, expectedOptions);
+ });
+
+ test(`the ${label} facet filters the jobs list by ${label}`, async function(assert) {
await beforeEach();
await facet.toggle();
- let expectation;
- if (typeof expectedOptions === 'function') {
- expectation = expectedOptions(server.db.jobs);
- } else {
- expectation = expectedOptions;
- }
+ const option = facet.options.findOneBy('label', optionToSelect);
+ const selection = option.key;
+ await option.select();
- assert.deepEqual(
- facet.options.map(option => option.label.trim()),
- expectation,
- 'Options for facet are as expected'
+ const expectedJobs = server.db.jobs
+ .filter(job => filter(job, selection))
+ .sortBy('modifyIndex')
+ .reverse();
+
+ JobsList.jobs.forEach((job, index) => {
+ assert.equal(
+ job.id,
+ expectedJobs[index].id,
+ `Job at ${index} is ${expectedJobs[index].id}`
+ );
+ });
+ });
+
+ test(`selecting an option in the ${label} facet updates the ${paramName} query param`, async function(assert) {
+ await beforeEach();
+ await facet.toggle();
+
+ const option = facet.options.objectAt(1);
+ const selection = option.key;
+ await option.select();
+
+ assert.ok(
+ currentURL().includes(`${paramName}=${selection}`),
+ 'URL has the correct query param key and value'
);
});
+ }
+
+ function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) {
+ test(`the ${label} facet has the correct options`, async function(assert) {
+ await facetOptions(assert, beforeEach, facet, expectedOptions);
+ });
test(`the ${label} facet filters the jobs list by ${label}`, async function(assert) {
let option;
@@ -452,9 +498,8 @@ module('Acceptance | jobs list', function(hooks) {
await option2.toggle();
selection.push(option2.key);
- assert.equal(
- currentURL(),
- `/jobs?${paramName}=${encodeURIComponent(JSON.stringify(selection))}`,
+ assert.ok(
+ currentURL().includes(encodeURIComponent(JSON.stringify(selection))),
'URL has the correct query param key and value'
);
});
diff --git a/ui/tests/acceptance/namespaces-test.js b/ui/tests/acceptance/namespaces-test.js
deleted file mode 100644
index 52900b29c..000000000
--- a/ui/tests/acceptance/namespaces-test.js
+++ /dev/null
@@ -1,174 +0,0 @@
-import { currentURL } from '@ember/test-helpers';
-import { module, test } from 'qunit';
-import { setupApplicationTest } from 'ember-qunit';
-import { setupMirage } from 'ember-cli-mirage/test-support';
-import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
-import { selectChoose } from 'ember-power-select/test-support';
-import JobsList from 'nomad-ui/tests/pages/jobs/list';
-import ClientsList from 'nomad-ui/tests/pages/clients/list';
-import Allocation from 'nomad-ui/tests/pages/allocations/detail';
-import PluginsList from 'nomad-ui/tests/pages/storage/plugins/list';
-import VolumesList from 'nomad-ui/tests/pages/storage/volumes/list';
-
-module('Acceptance | namespaces (disabled)', function(hooks) {
- setupApplicationTest(hooks);
- setupMirage(hooks);
-
- hooks.beforeEach(function() {
- server.create('agent');
- server.create('node');
- server.createList('job', 5, { createAllocations: false });
- });
-
- test('the namespace switcher is not in the gutter menu', async function(assert) {
- await JobsList.visit();
- assert.notOk(JobsList.namespaceSwitcher.isPresent, 'No namespace switcher found');
- });
-
- test('the jobs request is made with no query params', async function(assert) {
- await JobsList.visit();
-
- const request = server.pretender.handledRequests.findBy('url', '/v1/jobs');
- assert.equal(request.queryParams.namespace, undefined, 'No namespace query param');
- });
-});
-
-module('Acceptance | namespaces (enabled)', function(hooks) {
- setupApplicationTest(hooks);
- setupMirage(hooks);
-
- hooks.beforeEach(function() {
- server.createList('namespace', 3);
- server.create('agent');
- server.create('node');
- server.createList('job', 5);
- });
-
- hooks.afterEach(function() {
- window.localStorage.clear();
- });
-
- test('it passes an accessibility audit', async function(assert) {
- await JobsList.visit();
- await a11yAudit(assert);
- });
-
- test('the namespace switcher lists all namespaces', async function(assert) {
- const namespaces = server.db.namespaces;
-
- await JobsList.visit();
-
- assert.ok(JobsList.namespaceSwitcher.isPresent, 'Namespace switcher found');
- await JobsList.namespaceSwitcher.open();
- // TODO this selector should be scoped to only the namespace switcher options,
- // but ember-wormhole makes that difficult.
- assert.equal(
- JobsList.namespaceSwitcher.options.length,
- namespaces.length,
- 'All namespaces are in the switcher'
- );
- assert.equal(
- JobsList.namespaceSwitcher.options.objectAt(0).label,
- 'Namespace: default',
- 'The first namespace is always the default one'
- );
-
- const sortedNamespaces = namespaces.slice(1).sortBy('name');
- JobsList.namespaceSwitcher.options.forEach((option, index) => {
- // Default Namespace handled separately
- if (index === 0) return;
-
- const namespace = sortedNamespaces[index - 1];
- assert.equal(
- option.label,
- `Namespace: ${namespace.name}`,
- `index ${index}: ${namespace.name}`
- );
- });
- });
-
- test('changing the namespace sets the namespace in localStorage', async function(assert) {
- const namespace = server.db.namespaces[1];
-
- await JobsList.visit();
- await selectChoose('[data-test-namespace-switcher-parent]', namespace.name);
-
- assert.equal(
- window.localStorage.nomadActiveNamespace,
- namespace.id,
- 'Active namespace was set'
- );
- });
-
- test('changing the namespace refreshes the jobs list when on the jobs page', async function(assert) {
- const namespace = server.db.namespaces[1];
-
- await JobsList.visit();
-
- let requests = server.pretender.handledRequests.filter(req => req.url.startsWith('/v1/jobs'));
- assert.equal(requests.length, 1, 'First request to jobs');
- assert.equal(
- requests[0].queryParams.namespace,
- undefined,
- 'Namespace query param is defaulted to "default"/undefined'
- );
-
- // TODO: handle this with Page Objects
- await selectChoose('[data-test-namespace-switcher-parent]', namespace.name);
-
- requests = server.pretender.handledRequests.filter(req => req.url.startsWith('/v1/jobs'));
- assert.equal(requests.length, 2, 'Second request to jobs');
- assert.equal(
- requests[1].queryParams.namespace,
- namespace.name,
- 'Namespace query param on second request'
- );
- });
-
- test('changing the namespace in the clients hierarchy navigates to the jobs page', async function(assert) {
- const namespace = server.db.namespaces[1];
-
- await ClientsList.visit();
- await selectChoose('[data-test-namespace-switcher-parent]', namespace.name);
-
- assert.equal(currentURL(), `/jobs?namespace=${namespace.name}`);
- });
-
- test('changing the namespace in the allocations hierarchy navigates to the jobs page', async function(assert) {
- const namespace = server.db.namespaces[1];
- const allocation = server.create('allocation', { job: server.db.jobs[0] });
-
- await Allocation.visit({ id: allocation.id });
- await selectChoose('[data-test-namespace-switcher-parent]', namespace.name);
-
- assert.equal(currentURL(), `/jobs?namespace=${namespace.name}`);
- });
-
- test('changing the namespace in the storage hierarchy navigates to the volumes page', async function(assert) {
- const namespace = server.db.namespaces[1];
-
- await PluginsList.visit();
- await selectChoose('[data-test-namespace-switcher-parent]', namespace.name);
-
- assert.equal(currentURL(), `/csi/volumes?namespace=${namespace.name}`);
- });
-
- test('changing the namespace refreshes the volumes list when on the volumes page', async function(assert) {
- const namespace = server.db.namespaces[1];
-
- await VolumesList.visit();
-
- let requests = server.pretender.handledRequests.filter(req =>
- req.url.startsWith('/v1/volumes')
- );
- assert.equal(requests.length, 1);
- assert.equal(requests[0].queryParams.namespace, undefined);
-
- // TODO: handle this with Page Objects
- await selectChoose('[data-test-namespace-switcher-parent]', namespace.name);
-
- requests = server.pretender.handledRequests.filter(req => req.url.startsWith('/v1/volumes'));
- assert.equal(requests.length, 2);
- assert.equal(requests[1].queryParams.namespace, namespace.name);
- });
-});
diff --git a/ui/tests/acceptance/optimize-test.js b/ui/tests/acceptance/optimize-test.js
index d645d6963..435d9ae68 100644
--- a/ui/tests/acceptance/optimize-test.js
+++ b/ui/tests/acceptance/optimize-test.js
@@ -359,7 +359,7 @@ module('Acceptance | optimize', function(hooks) {
window.localStorage.nomadTokenSecret = clientToken.secretId;
await Optimize.visit();
- assert.equal(currentURL(), '/jobs');
+ assert.equal(currentURL(), '/jobs?namespace=default');
assert.ok(Layout.gutter.optimize.isHidden);
});
@@ -444,39 +444,6 @@ module('Acceptance | optimize search and facets', function(hooks) {
assert.ok(Optimize.recommendationSummaries[0].isActive);
});
- test('turning off the namespaces toggle narrows summaries to only the current namespace and changes an active summary if it has become filtered out', async function(assert) {
- server.create('job', {
- name: 'pppppp',
- createRecommendations: true,
- groupsCount: 1,
- groupTaskCount: 4,
- namespaceId: server.db.namespaces[1].id,
- });
-
- // Ensure this job’s recommendations are sorted to the top of the table
- const futureSubmitTime = (Date.now() + 10000) * 1000000;
- server.db.recommendations.update({ submitTime: futureSubmitTime });
-
- server.create('job', {
- name: 'oooooo',
- createRecommendations: true,
- groupsCount: 1,
- groupTaskCount: 6,
- namespaceId: server.db.namespaces[0].id,
- });
-
- await Optimize.visit();
-
- assert.ok(Optimize.allNamespacesToggle.isActive);
-
- await Optimize.allNamespacesToggle.toggle();
-
- assert.equal(Optimize.recommendationSummaries.length, 1);
- assert.ok(Optimize.recommendationSummaries[0].slug.startsWith('ooo'));
- assert.ok(currentURL().includes('all-namespaces=false'));
- assert.equal(Optimize.card.slug.jobName, 'oooooo');
- });
-
test('the namespaces toggle doesn’t show when there aren’t namespaces', async function(assert) {
server.db.namespaces.remove();
@@ -488,7 +455,7 @@ module('Acceptance | optimize search and facets', function(hooks) {
await Optimize.visit();
- assert.ok(Optimize.allNamespacesToggle.isHidden);
+ assert.ok(Optimize.facets.namespace.isHidden);
});
test('processing a summary moves to the next one in the sorted list', async function(assert) {
@@ -547,12 +514,28 @@ module('Acceptance | optimize search and facets', function(hooks) {
await Optimize.visit();
+ assert.ok(Optimize.facets.namespace.isPresent, 'Namespace facet found');
assert.ok(Optimize.facets.type.isPresent, 'Type facet found');
assert.ok(Optimize.facets.status.isPresent, 'Status facet found');
assert.ok(Optimize.facets.datacenter.isPresent, 'Datacenter facet found');
assert.ok(Optimize.facets.prefix.isPresent, 'Prefix facet found');
});
+ testSingleSelectFacet('Namespace', {
+ facet: Optimize.facets.namespace,
+ paramName: 'namespace',
+ expectedOptions: ['All (*)', 'default', 'namespace-1'],
+ optionToSelect: 'namespace-1',
+ async beforeEach() {
+ server.createList('job', 2, { namespaceId: 'default', createRecommendations: true });
+ server.createList('job', 2, { namespaceId: 'namespace-1', createRecommendations: true });
+ await Optimize.visit();
+ },
+ filter(taskGroup, selection) {
+ return taskGroup.job.namespaceId === selection;
+ },
+ });
+
testFacet('Type', {
facet: Optimize.facets.type,
paramName: 'type',
@@ -683,24 +666,73 @@ module('Acceptance | optimize search and facets', function(hooks) {
selection.find(prefix => taskGroup.job.name.startsWith(prefix)),
});
- function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) {
+ async function facetOptions(assert, beforeEach, facet, expectedOptions) {
+ await beforeEach();
+ await facet.toggle();
+
+ let expectation;
+ if (typeof expectedOptions === 'function') {
+ expectation = expectedOptions(server.db.jobs);
+ } else {
+ expectation = expectedOptions;
+ }
+
+ assert.deepEqual(
+ facet.options.map(option => option.label.trim()),
+ expectation,
+ 'Options for facet are as expected'
+ );
+ }
+
+ function testSingleSelectFacet(
+ label,
+ { facet, paramName, beforeEach, filter, expectedOptions, optionToSelect }
+ ) {
test(`the ${label} facet has the correct options`, async function(assert) {
+ await facetOptions.call(this, assert, beforeEach, facet, expectedOptions);
+ });
+
+ test(`the ${label} facet filters the jobs list by ${label}`, async function(assert) {
await beforeEach();
await facet.toggle();
- let expectation;
- if (typeof expectedOptions === 'function') {
- expectation = expectedOptions(server.db.jobs);
- } else {
- expectation = expectedOptions;
- }
+ const option = facet.options.findOneBy('label', optionToSelect);
+ const selection = option.key;
+ await option.select();
- assert.deepEqual(
- facet.options.map(option => option.label.trim()),
- expectation,
- 'Options for facet are as expected'
+ const sortedRecommendations = server.db.recommendations.sortBy('submitTime').reverse();
+
+ const recommendationTaskGroups = server.schema.tasks
+ .find(sortedRecommendations.mapBy('taskId').uniq())
+ .models.mapBy('taskGroup')
+ .uniqBy('id')
+ .filter(group => filter(group, selection));
+
+ Optimize.recommendationSummaries.forEach((summary, index) => {
+ const group = recommendationTaskGroups[index];
+ assert.equal(summary.slug, `${group.job.name} / ${group.name}`);
+ });
+ });
+
+ test(`selecting an option in the ${label} facet updates the ${paramName} query param`, async function(assert) {
+ await beforeEach();
+ await facet.toggle();
+
+ const option = facet.options.objectAt(1);
+ const selection = option.key;
+ await option.select();
+
+ assert.ok(
+ currentURL().includes(`${paramName}=${selection}`),
+ 'URL has the correct query param key and value'
);
});
+ }
+
+ function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) {
+ test(`the ${label} facet has the correct options`, async function(assert) {
+ await facetOptions.call(this, assert, beforeEach, facet, expectedOptions);
+ });
test(`the ${label} facet filters the recommendation summaries by ${label}`, async function(assert) {
let option;
diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js
index 8a842c808..4a8a92fd4 100644
--- a/ui/tests/acceptance/task-detail-test.js
+++ b/ui/tests/acceptance/task-detail-test.js
@@ -298,11 +298,7 @@ module('Acceptance | task detail (different namespace)', function(hooks) {
const job = server.db.jobs.find(jobId);
await Layout.breadcrumbFor('jobs.index').visit();
- assert.equal(
- currentURL(),
- '/jobs?namespace=other-namespace',
- 'Jobs breadcrumb links correctly'
- );
+ assert.equal(currentURL(), '/jobs?namespace=default', 'Jobs breadcrumb links correctly');
await Task.visit({ id: allocation.id, name: task.name });
await Layout.breadcrumbFor('jobs.job.index').visit();
diff --git a/ui/tests/acceptance/token-test.js b/ui/tests/acceptance/token-test.js
index 1c88bc8d0..9a1f3545c 100644
--- a/ui/tests/acceptance/token-test.js
+++ b/ui/tests/acceptance/token-test.js
@@ -148,23 +148,6 @@ module('Acceptance | tokens', function(hooks) {
assert.notOk(find('[data-test-job-row]'), 'No jobs found');
});
- test('when namespaces are enabled, setting or clearing a token refetches namespaces available with new permissions', async function(assert) {
- const { secretId } = clientToken;
-
- server.createList('namespace', 2);
- await Tokens.visit();
-
- const requests = server.pretender.handledRequests;
-
- assert.equal(requests.filter(req => req.url === '/v1/namespaces').length, 1);
-
- await Tokens.secret(secretId).submit();
- assert.equal(requests.filter(req => req.url === '/v1/namespaces').length, 2);
-
- await Tokens.clear();
- assert.equal(requests.filter(req => req.url === '/v1/namespaces').length, 3);
- });
-
test('when the ott query parameter is present upon application load it’s exchanged for a token', async function(assert) {
const { oneTimeSecret, secretId } = managementToken;
diff --git a/ui/tests/acceptance/volume-detail-test.js b/ui/tests/acceptance/volume-detail-test.js
index 6b36b000e..2f937356e 100644
--- a/ui/tests/acceptance/volume-detail-test.js
+++ b/ui/tests/acceptance/volume-detail-test.js
@@ -209,7 +209,7 @@ module('Acceptance | volume detail (with namespaces)', function(hooks) {
});
test('/csi/volumes/:id detail ribbon includes the namespace of the volume', async function(assert) {
- await VolumeDetail.visit({ id: volume.id });
+ await VolumeDetail.visit({ id: volume.id, namespace: volume.namespaceId });
assert.ok(VolumeDetail.hasNamespace);
assert.ok(VolumeDetail.namespace.includes(volume.namespaceId || 'default'));
diff --git a/ui/tests/acceptance/volumes-list-test.js b/ui/tests/acceptance/volumes-list-test.js
index a00057387..193d115f0 100644
--- a/ui/tests/acceptance/volumes-list-test.js
+++ b/ui/tests/acceptance/volumes-list-test.js
@@ -82,6 +82,7 @@ module('Acceptance | volumes list', function(hooks) {
const nodeHealthStr = volume.nodesHealthy > 0 ? 'Healthy' : 'Unhealthy';
assert.equal(volumeRow.name, volume.id);
+ assert.notOk(volumeRow.hasNamespace);
assert.equal(volumeRow.schedulable, volume.schedulable ? 'Schedulable' : 'Unschedulable');
assert.equal(volumeRow.controllerHealth, controllerHealthStr);
assert.equal(
@@ -93,18 +94,19 @@ module('Acceptance | volumes list', function(hooks) {
});
test('each volume row should link to the corresponding volume', async function(assert) {
- const volume = server.create('csi-volume');
+ const [, secondNamespace] = server.createList('namespace', 2);
+ const volume = server.create('csi-volume', { namespaceId: secondNamespace.id });
- await VolumesList.visit();
+ await VolumesList.visit({ namespace: '*' });
await VolumesList.volumes.objectAt(0).clickName();
- assert.equal(currentURL(), `/csi/volumes/${volume.id}`);
+ assert.equal(currentURL(), `/csi/volumes/${volume.id}?namespace=${secondNamespace.id}`);
- await VolumesList.visit();
- assert.equal(currentURL(), '/csi/volumes');
+ await VolumesList.visit({ namespace: '*' });
+ assert.equal(currentURL(), '/csi/volumes?namespace=*');
await VolumesList.volumes.objectAt(0).clickRow();
- assert.equal(currentURL(), `/csi/volumes/${volume.id}`);
+ assert.equal(currentURL(), `/csi/volumes/${volume.id}?namespace=${secondNamespace.id}`);
});
test('when there are no volumes, there is an empty message', async function(assert) {
@@ -138,6 +140,16 @@ module('Acceptance | volumes list', function(hooks) {
assert.equal(currentURL(), '/csi/volumes?search=foobar');
});
+ test('when the cluster has namespaces, each volume row includes the volume namespace', async function(assert) {
+ server.createList('namespace', 2);
+ const volume = server.create('csi-volume');
+
+ await VolumesList.visit({ namespace: '*' });
+
+ const volumeRow = VolumesList.volumes.objectAt(0);
+ assert.equal(volumeRow.namespace, volume.namespaceId);
+ });
+
test('when the namespace query param is set, only matching volumes are shown and the namespace value is forwarded to app state', async function(assert) {
server.createList('namespace', 2);
const volume1 = server.create('csi-volume', { namespaceId: server.db.namespaces[0].id });
@@ -159,7 +171,9 @@ module('Acceptance | volumes list', function(hooks) {
server.createList('namespace', 2);
const namespace = server.db.namespaces[1];
- await VolumesList.visit({ namespace: namespace.id });
+ await VolumesList.visit();
+ await VolumesList.facets.namespace.toggle();
+ await VolumesList.facets.namespace.options.objectAt(2).select();
await Layout.gutter.visitJobs();
@@ -185,4 +199,79 @@ module('Acceptance | volumes list', function(hooks) {
await VolumesList.visit();
},
});
+
+ testSingleSelectFacet('Namespace', {
+ facet: VolumesList.facets.namespace,
+ paramName: 'namespace',
+ expectedOptions: ['All (*)', 'default', 'namespace-2'],
+ optionToSelect: 'namespace-2',
+ async beforeEach() {
+ server.create('namespace', { id: 'default' });
+ server.create('namespace', { id: 'namespace-2' });
+ server.createList('csi-volume', 2, { namespaceId: 'default' });
+ server.createList('csi-volume', 2, { namespaceId: 'namespace-2' });
+ await VolumesList.visit();
+ },
+ filter(volume, selection) {
+ return volume.namespaceId === selection;
+ },
+ });
+
+ function testSingleSelectFacet(
+ label,
+ { facet, paramName, beforeEach, filter, expectedOptions, optionToSelect }
+ ) {
+ test(`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.jobs);
+ } else {
+ expectation = expectedOptions;
+ }
+
+ assert.deepEqual(
+ facet.options.map(option => option.label.trim()),
+ expectation,
+ 'Options for facet are as expected'
+ );
+ });
+
+ test(`the ${label} facet filters the volumes list by ${label}`, async function(assert) {
+ await beforeEach();
+ await facet.toggle();
+
+ const option = facet.options.findOneBy('label', optionToSelect);
+ const selection = option.key;
+ await option.select();
+
+ const expectedVolumes = server.db.csiVolumes
+ .filter(volume => filter(volume, selection))
+ .sortBy('id');
+
+ VolumesList.volumes.forEach((volume, index) => {
+ assert.equal(
+ volume.name,
+ expectedVolumes[index].name,
+ `Volume at ${index} is ${expectedVolumes[index].name}`
+ );
+ });
+ });
+
+ test(`selecting an option in the ${label} facet updates the ${paramName} query param`, async function(assert) {
+ await beforeEach();
+ await facet.toggle();
+
+ const option = facet.options.objectAt(1);
+ const selection = option.key;
+ await option.select();
+
+ assert.ok(
+ currentURL().includes(`${paramName}=${selection}`),
+ 'URL has the correct query param key and value'
+ );
+ });
+ }
});
diff --git a/ui/tests/helpers/setup-ability.js b/ui/tests/helpers/setup-ability.js
index 41f56f3a7..c2edd14b7 100644
--- a/ui/tests/helpers/setup-ability.js
+++ b/ui/tests/helpers/setup-ability.js
@@ -1,9 +1,11 @@
export default ability => hooks => {
hooks.beforeEach(function() {
this.ability = this.owner.lookup(`ability:${ability}`);
+ this.can = this.owner.lookup('service:can');
});
hooks.afterEach(function() {
delete this.ability;
+ delete this.can;
});
};
diff --git a/ui/tests/integration/components/multi-select-dropdown-test.js b/ui/tests/integration/components/multi-select-dropdown-test.js
index 82e2dbac2..1592395f0 100644
--- a/ui/tests/integration/components/multi-select-dropdown-test.js
+++ b/ui/tests/integration/components/multi-select-dropdown-test.js
@@ -30,10 +30,10 @@ module('Integration | Component | multi-select dropdown', function(hooks) {
const commonTemplate = hbs`
+ @label={{this.label}}
+ @options={{this.options}}
+ @selection={{this.selection}}
+ @onSelect={{this.onSelect}} />
`;
test('component is initially closed', async function(assert) {
diff --git a/ui/tests/integration/components/single-select-dropdown-test.js b/ui/tests/integration/components/single-select-dropdown-test.js
new file mode 100644
index 000000000..3d509de75
--- /dev/null
+++ b/ui/tests/integration/components/single-select-dropdown-test.js
@@ -0,0 +1,80 @@
+import { findAll, find, render } from '@ember/test-helpers';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers';
+import sinon from 'sinon';
+import hbs from 'htmlbars-inline-precompile';
+import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
+
+module('Integration | Component | single-select dropdown', function(hooks) {
+ setupRenderingTest(hooks);
+
+ const commonProperties = () => ({
+ label: 'Type',
+ selection: 'nomad',
+ options: [
+ { key: 'consul', label: 'Consul' },
+ { key: 'nomad', label: 'Nomad' },
+ { key: 'terraform', label: 'Terraform' },
+ { key: 'packer', label: 'Packer' },
+ { key: 'vagrant', label: 'Vagrant' },
+ { key: 'vault', label: 'Vault' },
+ ],
+ onSelect: sinon.spy(),
+ });
+
+ const commonTemplate = hbs`
+
+ `;
+
+ test('component shows label and selection in the trigger', async function(assert) {
+ const props = commonProperties();
+ this.setProperties(props);
+ await render(commonTemplate);
+
+ assert.ok(find('.ember-power-select-trigger').textContent.includes(props.label));
+ assert.ok(
+ find('.ember-power-select-trigger').textContent.includes(
+ props.options.findBy('key', props.selection).label
+ )
+ );
+ assert.notOk(find('[data-test-dropdown-options]'));
+
+ await componentA11yAudit(this.element, assert);
+ });
+
+ test('all options are shown in the dropdown', async function(assert) {
+ const props = commonProperties();
+ this.setProperties(props);
+ await render(commonTemplate);
+
+ await clickTrigger('[data-test-single-select-dropdown]');
+
+ assert.equal(
+ findAll('.ember-power-select-option').length,
+ props.options.length,
+ 'All options are shown'
+ );
+ findAll('.ember-power-select-option').forEach((optionEl, index) => {
+ assert.equal(
+ optionEl.querySelector('.dropdown-label').textContent.trim(),
+ props.options[index].label
+ );
+ });
+ });
+
+ test('selecting an option calls `onSelect` with the key for the selected option', async function(assert) {
+ const props = commonProperties();
+ this.setProperties(props);
+ await render(commonTemplate);
+
+ const option = props.options.findBy('key', 'terraform');
+ await selectChoose('[data-test-single-select-dropdown]', option.label);
+
+ assert.ok(props.onSelect.calledWith(option.key));
+ });
+});
diff --git a/ui/tests/pages/clients/list.js b/ui/tests/pages/clients/list.js
index c0d04319a..652a777dc 100644
--- a/ui/tests/pages/clients/list.js
+++ b/ui/tests/pages/clients/list.js
@@ -11,7 +11,7 @@ import {
visitable,
} from 'ember-cli-page-object';
-import facet from 'nomad-ui/tests/pages/components/facet';
+import { multiFacet } from 'nomad-ui/tests/pages/components/facet';
import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select';
export default create({
@@ -72,9 +72,9 @@ export default create({
},
facets: {
- class: facet('[data-test-class-facet]'),
- state: facet('[data-test-state-facet]'),
- datacenter: facet('[data-test-datacenter-facet]'),
- volume: facet('[data-test-volume-facet]'),
+ class: multiFacet('[data-test-class-facet]'),
+ state: multiFacet('[data-test-state-facet]'),
+ datacenter: multiFacet('[data-test-datacenter-facet]'),
+ volume: multiFacet('[data-test-volume-facet]'),
},
});
diff --git a/ui/tests/pages/components/facet.js b/ui/tests/pages/components/facet.js
index 5dce211cd..a4d737184 100644
--- a/ui/tests/pages/components/facet.js
+++ b/ui/tests/pages/components/facet.js
@@ -1,17 +1,38 @@
-import { isPresent, clickable, collection, text, attribute } from 'ember-cli-page-object';
+import { clickable, collection, text, attribute } from 'ember-cli-page-object';
+import { selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers';
-export default scope => ({
+export const multiFacet = scope => ({
scope,
- isPresent: isPresent(),
-
toggle: clickable('[data-test-dropdown-trigger]'),
options: collection('[data-test-dropdown-option]', {
- testContainer: '#ember-testing',
+ testContainer: '#ember-testing .ember-basic-dropdown-content',
resetScope: true,
label: text(),
key: attribute('data-test-dropdown-option'),
toggle: clickable('label'),
}),
});
+
+export const singleFacet = scope => ({
+ scope,
+
+ async toggle() {
+ await clickTrigger(this.scope);
+ },
+
+ options: collection('.ember-power-select-option', {
+ testContainer: '#ember-testing .ember-basic-dropdown-content',
+ resetScope: true,
+ label: text('[data-test-dropdown-option]'),
+ key: attribute('data-test-dropdown-option', '[data-test-dropdown-option]'),
+ async select() {
+ // __parentTreeNode is clearly meant to be private in the page object API,
+ // but it seems better to use that and keep the API symmetry across singleFacet
+ // and multiFacet compared to moving select to the parent.
+ const parentScope = this.__parentTreeNode.scope;
+ await selectChoose(parentScope, this.label);
+ },
+ }),
+});
diff --git a/ui/tests/pages/jobs/list.js b/ui/tests/pages/jobs/list.js
index b1eb7faf1..da4e60140 100644
--- a/ui/tests/pages/jobs/list.js
+++ b/ui/tests/pages/jobs/list.js
@@ -10,7 +10,7 @@ import {
visitable,
} from 'ember-cli-page-object';
-import facet from 'nomad-ui/tests/pages/components/facet';
+import { multiFacet, singleFacet } from 'nomad-ui/tests/pages/components/facet';
import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select';
export default create({
@@ -31,12 +31,14 @@ export default create({
jobs: collection('[data-test-job-row]', {
id: attribute('data-test-job-row'),
name: text('[data-test-job-name]'),
+ namespace: text('[data-test-job-namespace]'),
link: attribute('href', '[data-test-job-name] a'),
status: text('[data-test-job-status]'),
type: text('[data-test-job-type]'),
priority: text('[data-test-job-priority]'),
taskGroups: text('[data-test-job-task-groups]'),
+ hasNamespace: isPresent('[data-test-job-namespace]'),
clickRow: clickable(),
clickName: clickable('[data-test-job-name] a'),
}),
@@ -58,22 +60,13 @@ export default create({
gotoClients: clickable('[data-test-error-clients-link]'),
},
- namespaceSwitcher: {
- isPresent: isPresent('[data-test-namespace-switcher-parent]'),
- open: clickable('[data-test-namespace-switcher-parent] .ember-power-select-trigger'),
- options: collection('.ember-power-select-option', {
- testContainer: '#ember-testing',
- resetScope: true,
- label: text(),
- }),
- },
-
pageSizeSelect: pageSizeSelect(),
facets: {
- type: facet('[data-test-type-facet]'),
- status: facet('[data-test-status-facet]'),
- datacenter: facet('[data-test-datacenter-facet]'),
- prefix: facet('[data-test-prefix-facet]'),
+ namespace: singleFacet('[data-test-namespace-facet]'),
+ type: multiFacet('[data-test-type-facet]'),
+ status: multiFacet('[data-test-status-facet]'),
+ datacenter: multiFacet('[data-test-datacenter-facet]'),
+ prefix: multiFacet('[data-test-prefix-facet]'),
},
});
diff --git a/ui/tests/pages/layout.js b/ui/tests/pages/layout.js
index e9d3de2c2..2532a933e 100644
--- a/ui/tests/pages/layout.js
+++ b/ui/tests/pages/layout.js
@@ -50,14 +50,6 @@ export default create({
gutter: {
scope: '[data-test-gutter-menu]',
- namespaceSwitcher: {
- scope: '[data-test-namespace-switcher-parent]',
- isPresent: isPresent(),
- open: clickable('.ember-power-select-trigger'),
- options: collection('.ember-power-select-option', {
- label: text(),
- }),
- },
visitJobs: clickable('[data-test-gutter-link="jobs"]'),
optimize: {
diff --git a/ui/tests/pages/optimize.js b/ui/tests/pages/optimize.js
index ce351ef31..35fbddc4c 100644
--- a/ui/tests/pages/optimize.js
+++ b/ui/tests/pages/optimize.js
@@ -10,8 +10,7 @@ import {
} from 'ember-cli-page-object';
import recommendationCard from 'nomad-ui/tests/pages/components/recommendation-card';
-import facet from 'nomad-ui/tests/pages/components/facet';
-import toggle from 'nomad-ui/tests/pages/components/toggle';
+import { multiFacet, singleFacet } from 'nomad-ui/tests/pages/components/facet';
export default create({
visit: visitable('/optimize'),
@@ -22,14 +21,13 @@ export default create({
},
facets: {
- type: facet('[data-test-type-facet]'),
- status: facet('[data-test-status-facet]'),
- datacenter: facet('[data-test-datacenter-facet]'),
- prefix: facet('[data-test-prefix-facet]'),
+ namespace: singleFacet('[data-test-namespace-facet]'),
+ type: multiFacet('[data-test-type-facet]'),
+ status: multiFacet('[data-test-status-facet]'),
+ datacenter: multiFacet('[data-test-datacenter-facet]'),
+ prefix: multiFacet('[data-test-prefix-facet]'),
},
- allNamespacesToggle: toggle('[data-test-all-namespaces-toggle]'),
-
card: recommendationCard,
recommendationSummaries: collection('[data-test-recommendation-summary-row]', {
diff --git a/ui/tests/pages/storage/plugins/plugin/allocations.js b/ui/tests/pages/storage/plugins/plugin/allocations.js
index e3f2924f9..bd553fbe4 100644
--- a/ui/tests/pages/storage/plugins/plugin/allocations.js
+++ b/ui/tests/pages/storage/plugins/plugin/allocations.js
@@ -1,7 +1,7 @@
import { clickable, create, isPresent, text, visitable } from 'ember-cli-page-object';
import allocations from 'nomad-ui/tests/pages/components/allocations';
-import facet from 'nomad-ui/tests/pages/components/facet';
+import { multiFacet } from 'nomad-ui/tests/pages/components/facet';
import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select';
export default create({
@@ -22,7 +22,7 @@ export default create({
pageSizeSelect: pageSizeSelect(),
facets: {
- health: facet('[data-test-health-facet]'),
- type: facet('[data-test-type-facet]'),
+ health: multiFacet('[data-test-health-facet]'),
+ type: multiFacet('[data-test-type-facet]'),
},
});
diff --git a/ui/tests/pages/storage/volumes/list.js b/ui/tests/pages/storage/volumes/list.js
index 3c14f6264..d69e39b6f 100644
--- a/ui/tests/pages/storage/volumes/list.js
+++ b/ui/tests/pages/storage/volumes/list.js
@@ -9,6 +9,7 @@ import {
} from 'ember-cli-page-object';
import error from 'nomad-ui/tests/pages/components/error';
+import { singleFacet } from 'nomad-ui/tests/pages/components/facet';
import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select';
export default create({
@@ -20,12 +21,14 @@ export default create({
volumes: collection('[data-test-volume-row]', {
name: text('[data-test-volume-name]'),
+ namespace: text('[data-test-volume-namespace]'),
schedulable: text('[data-test-volume-schedulable]'),
controllerHealth: text('[data-test-volume-controller-health]'),
nodeHealth: text('[data-test-volume-node-health]'),
provider: text('[data-test-volume-provider]'),
allocations: text('[data-test-volume-allocations]'),
+ hasNamespace: isPresent('[data-test-volume-namespace]'),
clickRow: clickable(),
clickName: clickable('[data-test-volume-name] a'),
}),
@@ -41,13 +44,7 @@ export default create({
error: error(),
pageSizeSelect: pageSizeSelect(),
- namespaceSwitcher: {
- isPresent: isPresent('[data-test-namespace-switcher-parent]'),
- open: clickable('[data-test-namespace-switcher-parent] .ember-power-select-trigger'),
- options: collection('.ember-power-select-option', {
- testContainer: '#ember-testing',
- resetScope: true,
- label: text(),
- }),
+ facets: {
+ namespace: singleFacet('[data-test-namespace-facet]'),
},
});
diff --git a/ui/tests/unit/abilities/allocation-test.js b/ui/tests/unit/abilities/allocation-test.js
index 174db388c..27eb574de 100644
--- a/ui/tests/unit/abilities/allocation-test.js
+++ b/ui/tests/unit/abilities/allocation-test.js
@@ -15,7 +15,7 @@ module('Unit | Ability | allocation', function(hooks) {
this.owner.register('service:token', mockToken);
- assert.ok(this.ability.canExec);
+ assert.ok(this.can.can('exec allocation'));
});
test('it permits alloc exec for management tokens', function(assert) {
@@ -26,15 +26,12 @@ module('Unit | Ability | allocation', function(hooks) {
this.owner.register('service:token', mockToken);
- assert.ok(this.ability.canExec);
+ assert.ok(this.can.can('exec allocation'));
});
test('it permits alloc exec for client tokens with a policy that has namespace alloc-exec', function(assert) {
const mockSystem = Service.extend({
aclEnabled: true,
- activeNamespace: {
- name: 'aNamespace',
- },
});
const mockToken = Service.extend({
@@ -57,15 +54,12 @@ module('Unit | Ability | allocation', function(hooks) {
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
- assert.ok(this.ability.canExec);
+ assert.ok(this.can.can('exec allocation', null, { namespace: 'aNamespace' }));
});
test('it permits alloc exec for client tokens with a policy that has default namespace alloc-exec and no capabilities for active namespace', function(assert) {
const mockSystem = Service.extend({
aclEnabled: true,
- activeNamespace: {
- name: 'anotherNamespace',
- },
});
const mockToken = Service.extend({
@@ -92,15 +86,12 @@ module('Unit | Ability | allocation', function(hooks) {
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
- assert.ok(this.ability.canExec);
+ assert.ok(this.can.can('exec allocation', null, { namespace: 'anotherNamespace' }));
});
test('it blocks alloc exec for client tokens with a policy that has no alloc-exec capability', function(assert) {
const mockSystem = Service.extend({
aclEnabled: true,
- activeNamespace: {
- name: 'aNamespace',
- },
});
const mockToken = Service.extend({
@@ -123,15 +114,12 @@ module('Unit | Ability | allocation', function(hooks) {
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
- assert.notOk(this.ability.canExec);
+ assert.ok(this.can.cannot('exec allocation', null, { namespace: 'aNamespace' }));
});
test('it handles globs in namespace names', function(assert) {
const mockSystem = Service.extend({
aclEnabled: true,
- activeNamespace: {
- name: 'aNamespace',
- },
});
const mockToken = Service.extend({
@@ -174,27 +162,17 @@ module('Unit | Ability | allocation', function(hooks) {
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
- const systemService = this.owner.lookup('service:system');
-
- systemService.set('activeNamespace.name', 'production-web');
- assert.notOk(this.ability.canExec);
-
- systemService.set('activeNamespace.name', 'production-api');
- assert.ok(this.ability.canExec);
-
- systemService.set('activeNamespace.name', 'production-other');
- assert.ok(this.ability.canExec);
-
- systemService.set('activeNamespace.name', 'something-suffixed');
- assert.ok(this.ability.canExec);
-
- systemService.set('activeNamespace.name', 'something-more-suffixed');
- assert.notOk(
- this.ability.canExec,
+ assert.ok(this.can.cannot('exec allocation', null, { namespace: 'production-web' }));
+ assert.ok(this.can.can('exec allocation', null, { namespace: 'production-api' }));
+ assert.ok(this.can.can('exec allocation', null, { namespace: 'production-other' }));
+ assert.ok(this.can.can('exec allocation', null, { namespace: 'something-suffixed' }));
+ assert.ok(
+ this.can.cannot('exec allocation', null, { namespace: 'something-more-suffixed' }),
'expected the namespace with the greatest number of matched characters to be chosen'
);
-
- systemService.set('activeNamespace.name', '000-abc-999');
- assert.ok(this.ability.canExec, 'expected to be able to match against more than one wildcard');
+ assert.ok(
+ this.can.can('exec allocation', null, { namespace: '000-abc-999' }),
+ 'expected to be able to match against more than one wildcard'
+ );
});
});
diff --git a/ui/tests/unit/abilities/job-test.js b/ui/tests/unit/abilities/job-test.js
index b7a874a02..437c1afa4 100644
--- a/ui/tests/unit/abilities/job-test.js
+++ b/ui/tests/unit/abilities/job-test.js
@@ -32,9 +32,6 @@ module('Unit | Ability | job', function(hooks) {
test('it permits job run for client tokens with a policy that has namespace submit-job', function(assert) {
const mockSystem = Service.extend({
aclEnabled: true,
- activeNamespace: {
- name: 'aNamespace',
- },
});
const mockToken = Service.extend({
@@ -57,15 +54,12 @@ module('Unit | Ability | job', function(hooks) {
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
- assert.ok(this.ability.canRun);
+ assert.ok(this.can.can('run job', null, { namespace: 'aNamespace' }));
});
test('it permits job run for client tokens with a policy that has default namespace submit-job and no capabilities for active namespace', function(assert) {
const mockSystem = Service.extend({
aclEnabled: true,
- activeNamespace: {
- name: 'anotherNamespace',
- },
});
const mockToken = Service.extend({
@@ -92,15 +86,12 @@ module('Unit | Ability | job', function(hooks) {
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
- assert.ok(this.ability.canRun);
+ assert.ok(this.can.can('run job', null, { namespace: 'anotherNamespace' }));
});
test('it blocks job run for client tokens with a policy that has no submit-job capability', function(assert) {
const mockSystem = Service.extend({
aclEnabled: true,
- activeNamespace: {
- name: 'aNamespace',
- },
});
const mockToken = Service.extend({
@@ -123,7 +114,7 @@ module('Unit | Ability | job', function(hooks) {
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
- assert.notOk(this.ability.canRun);
+ assert.ok(this.can.cannot('run job', null, { namespace: 'aNamespace' }));
});
test('job scale requires a client token with the submit-job or scale-job capability', function(assert) {
@@ -142,9 +133,6 @@ module('Unit | Ability | job', function(hooks) {
const mockSystem = Service.extend({
aclEnabled: true,
- activeNamespace: {
- name: 'aNamespace',
- },
});
const mockToken = Service.extend({
@@ -157,24 +145,21 @@ module('Unit | Ability | job', function(hooks) {
this.owner.register('service:token', mockToken);
const tokenService = this.owner.lookup('service:token');
- assert.notOk(this.ability.canScale);
+ assert.ok(this.can.cannot('scale job', null, { namespace: 'aNamespace' }));
tokenService.set('selfTokenPolicies', makePolicies('aNamespace', 'scale-job'));
- assert.ok(this.ability.canScale);
+ assert.ok(this.can.can('scale job', null, { namespace: 'aNamespace' }));
tokenService.set('selfTokenPolicies', makePolicies('aNamespace', 'submit-job'));
- assert.ok(this.ability.canScale);
+ assert.ok(this.can.can('scale job', null, { namespace: 'aNamespace' }));
tokenService.set('selfTokenPolicies', makePolicies('bNamespace', 'scale-job'));
- assert.notOk(this.ability.canScale);
+ assert.ok(this.can.cannot('scale job', null, { namespace: 'aNamespace' }));
});
test('it handles globs in namespace names', function(assert) {
const mockSystem = Service.extend({
aclEnabled: true,
- activeNamespace: {
- name: 'aNamespace',
- },
});
const mockToken = Service.extend({
@@ -217,27 +202,17 @@ module('Unit | Ability | job', function(hooks) {
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
- const systemService = this.owner.lookup('service:system');
-
- systemService.set('activeNamespace.name', 'production-web');
- assert.notOk(this.ability.canRun);
-
- systemService.set('activeNamespace.name', 'production-api');
- assert.ok(this.ability.canRun);
-
- systemService.set('activeNamespace.name', 'production-other');
- assert.ok(this.ability.canRun);
-
- systemService.set('activeNamespace.name', 'something-suffixed');
- assert.ok(this.ability.canRun);
-
- systemService.set('activeNamespace.name', 'something-more-suffixed');
- assert.notOk(
- this.ability.canRun,
+ assert.ok(this.can.cannot('run job', null, { namespace: 'production-web' }));
+ assert.ok(this.can.can('run job', null, { namespace: 'production-api' }));
+ assert.ok(this.can.can('run job', null, { namespace: 'production-other' }));
+ assert.ok(this.can.can('run job', null, { namespace: 'something-suffixed' }));
+ assert.ok(
+ this.can.cannot('run job', null, { namespace: 'something-more-suffixed' }),
'expected the namespace with the greatest number of matched characters to be chosen'
);
-
- systemService.set('activeNamespace.name', '000-abc-999');
- assert.ok(this.ability.canRun, 'expected to be able to match against more than one wildcard');
+ assert.ok(
+ this.can.can('run job', null, { namespace: '000-abc-999' }),
+ 'expected to be able to match against more than one wildcard'
+ );
});
});
diff --git a/ui/tests/unit/adapters/volume-test.js b/ui/tests/unit/adapters/volume-test.js
index fdb3606c9..683716a4e 100644
--- a/ui/tests/unit/adapters/volume-test.js
+++ b/ui/tests/unit/adapters/volume-test.js
@@ -58,20 +58,6 @@ module('Unit | Adapter | Volume', function(hooks) {
assert.deepEqual(pretender.handledRequests.mapBy('url'), ['/v1/volumes?type=csi']);
});
- test('When a namespace is set in localStorage and the volume endpoint is queried, the namespace is in the query string', async function(assert) {
- const { pretender } = this.server;
-
- window.localStorage.nomadActiveNamespace = 'some-namespace';
- await this.initializeUI();
-
- this.subject().query(this.store, { modelName: 'volume' }, { type: 'csi' }, null, {});
- await settled();
-
- assert.deepEqual(pretender.handledRequests.mapBy('url'), [
- '/v1/volumes?namespace=some-namespace&type=csi',
- ]);
- });
-
test('When the volume has a namespace other than default, it is in the URL', async function(assert) {
const { pretender } = this.server;
const volumeName = 'csi/volume-1';