diff --git a/ui/app/controllers/jobs/job/allocations.js b/ui/app/controllers/jobs/job/allocations.js index 2b1861ea8..d6ec836c4 100644 --- a/ui/app/controllers/jobs/job/allocations.js +++ b/ui/app/controllers/jobs/job/allocations.js @@ -12,10 +12,10 @@ import classic from 'ember-classic-decorator'; @classic export default class AllocationsController extends Controller.extend( - Sortable, - Searchable, - WithNamespaceResetting -) { + Sortable, + Searchable, + WithNamespaceResetting + ) { queryParams = [ { currentPage: 'page', @@ -38,15 +38,11 @@ export default class AllocationsController extends Controller.extend( { qpTaskGroup: 'taskGroup', }, - { - qpJobVersion: 'jobVersion', - }, ]; qpStatus = ''; qpClient = ''; qpTaskGroup = ''; - qpJobVersion = ''; currentPage = 1; pageSize = 25; @@ -60,20 +56,16 @@ export default class AllocationsController extends Controller.extend( return ['shortId', 'name', 'taskGroupName']; } - @computed( - 'model.allocations.[]', - 'selectionStatus', - 'selectionClient', - 'selectionTaskGroup', - 'selectionJobVersion' - ) + @computed('model.allocations.[]') get allocations() { - const allocations = this.get('model.allocations') || []; - const { selectionStatus, selectionClient, selectionTaskGroup, selectionJobVersion } = this; + return this.get('model.allocations') || []; + } - if (!allocations.length) return allocations; + @computed('allocations.[]', 'selectionStatus', 'selectionClient', 'selectionTaskGroup') + get filteredAllocations() { + const { selectionStatus, selectionClient, selectionTaskGroup } = this; - return allocations.filter(alloc => { + return this.allocations.filter(alloc => { if (selectionStatus.length && !selectionStatus.includes(alloc.clientStatus)) { return false; } @@ -83,21 +75,17 @@ export default class AllocationsController extends Controller.extend( if (selectionTaskGroup.length && !selectionTaskGroup.includes(alloc.taskGroupName)) { return false; } - if (selectionJobVersion.length && !selectionJobVersion.includes(alloc.jobVersion)) { - return false; - } return true; }); } + @alias('filteredAllocations') listToSort; + @alias('listSorted') listToSearch; + @alias('listSearched') sortedAllocations; + @selection('qpStatus') selectionStatus; @selection('qpClient') selectionClient; @selection('qpTaskGroup') selectionTaskGroup; - @selection('qpJobVersion') selectionJobVersion; - - @alias('allocations') listToSort; - @alias('listSorted') listToSearch; - @alias('listSearched') sortedAllocations; @action gotoAllocation(allocation) { @@ -106,8 +94,7 @@ export default class AllocationsController extends Controller.extend( get optionsAllocationStatus() { return [ - { key: 'queued', label: 'Queued' }, - { key: 'starting', label: 'Starting' }, + { key: 'pending', label: 'Pending' }, { key: 'running', label: 'Running' }, { key: 'complete', label: 'Complete' }, { key: 'failed', label: 'Failed' }, @@ -132,7 +119,7 @@ export default class AllocationsController extends Controller.extend( get optionsTaskGroups() { const taskGroups = Array.from(new Set(this.model.allocations.mapBy('taskGroupName'))).compact(); - // Update query param when the list of clients changes. + // Update query param when the list of task groups changes. scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set('qpTaskGroup', serialize(intersection(taskGroups, this.selectionTaskGroup))); @@ -141,19 +128,6 @@ export default class AllocationsController extends Controller.extend( return taskGroups.sort().map(tg => ({ key: tg, label: tg })); } - @computed('model.allocations.[]', 'selectionJobVersion') - get optionsJobVersions() { - const jobVersions = Array.from(new Set(this.model.allocations.mapBy('jobVersion'))).compact(); - - // Update query param when the list of clients changes. - scheduleOnce('actions', () => { - // eslint-disable-next-line ember/no-side-effects - this.set('qpJobVersion', serialize(intersection(jobVersions, this.selectionJobVersion))); - }); - - return jobVersions.sort().map(jv => ({ key: jv, label: jv })); - } - setFacetQueryParam(queryParam, selection) { this.set(queryParam, serialize(selection)); } diff --git a/ui/app/templates/jobs/job/allocations.hbs b/ui/app/templates/jobs/job/allocations.hbs index 22f3aaac8..2dff6a224 100644 --- a/ui/app/templates/jobs/job/allocations.hbs +++ b/ui/app/templates/jobs/job/allocations.hbs @@ -1,7 +1,7 @@ {{page-title "Job " this.job.name " allocations"}}
- {{#if this.model.allocations.length}} + {{#if this.allocations.length}}
-
diff --git a/ui/tests/acceptance/job-allocations-test.js b/ui/tests/acceptance/job-allocations-test.js index 7c86653c3..73fcc7207 100644 --- a/ui/tests/acceptance/job-allocations-test.js +++ b/ui/tests/acceptance/job-allocations-test.js @@ -124,4 +124,151 @@ module('Acceptance | job allocations', function(hooks) { assert.ok(Allocations.error.isPresent, 'Error message is shown'); assert.equal(Allocations.error.title, 'Not Found', 'Error message is for 404'); }); + + testFacet('Status', { + facet: Allocations.facets.status, + paramName: 'status', + expectedOptions: ['Pending', 'Running', 'Complete', 'Failed', 'Lost'], + async beforeEach() { + ['pending', 'running', 'complete', 'failed', 'lost'].forEach(s => { + server.createList('allocation', 5, { clientStatus: s }); + }); + await Allocations.visit({ id: job.id }); + }, + filter: (alloc, selection) => alloc.jobId == job.id && selection.includes(alloc.clientStatus), + }); + + testFacet('Client', { + facet: Allocations.facets.client, + paramName: 'client', + expectedOptions(allocs) { + return Array.from( + new Set( + allocs + .filter(alloc => alloc.jobId == job.id) + .mapBy('nodeId') + .map(id => id.split('-')[0]) + ) + ).sort(); + }, + async beforeEach() { + server.createList('node', 5); + server.createList('allocation', 20); + + await Allocations.visit({ id: job.id }); + }, + filter: (alloc, selection) => + alloc.jobId == job.id && selection.includes(alloc.nodeId.split('-')[0]), + }); + + testFacet('Task Group', { + facet: Allocations.facets.taskGroup, + paramName: 'taskGroup', + expectedOptions(allocs) { + return Array.from( + new Set(allocs.filter(alloc => alloc.jobId == job.id).mapBy('taskGroup')) + ).sort(); + }, + async beforeEach() { + job = server.create('job', { + type: 'service', + status: 'running', + groupsCount: 5, + }); + + await Allocations.visit({ id: job.id }); + }, + filter: (alloc, selection) => alloc.jobId == job.id && selection.includes(alloc.taskGroup), + }); }); + +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(); + + Allocations.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(); + + Allocations.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(), + `/jobs/${job.id}/allocations?${paramName}=${encodeURIComponent(JSON.stringify(selection))}`, + 'URL has the correct query param key and value' + ); + }); +} diff --git a/ui/tests/pages/jobs/job/allocations.js b/ui/tests/pages/jobs/job/allocations.js index adb48aa70..8b15cb932 100644 --- a/ui/tests/pages/jobs/job/allocations.js +++ b/ui/tests/pages/jobs/job/allocations.js @@ -11,6 +11,7 @@ import { import allocations from 'nomad-ui/tests/pages/components/allocations'; import error from 'nomad-ui/tests/pages/components/error'; +import { multiFacet } from 'nomad-ui/tests/pages/components/facet'; export default create({ visit: visitable('/jobs/:id/allocations'), @@ -22,6 +23,12 @@ export default create({ ...allocations(), + facets: { + status: multiFacet('[data-test-allocation-status-facet]'), + client: multiFacet('[data-test-allocation-client-facet]'), + taskGroup: multiFacet('[data-test-allocation-task-group-facet]'), + }, + isEmpty: isPresent('[data-test-empty-allocations-list]'), emptyState: { headline: text('[data-test-empty-allocations-list-headline]'),