diff --git a/.changelog/11545.txt b/.changelog/11545.txt new file mode 100644 index 00000000000..427dcbcea83 --- /dev/null +++ b/.changelog/11545.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Add filters to the allocation list in the client and task group details pages +``` diff --git a/ui/app/controllers/clients/client/index.js b/ui/app/controllers/clients/client/index.js index 9b0121e088d..bc4eeec79b0 100644 --- a/ui/app/controllers/clients/client/index.js +++ b/ui/app/controllers/clients/client/index.js @@ -1,12 +1,16 @@ /* eslint-disable ember/no-observers */ +/* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */ import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import { action, computed } from '@ember/object'; import { observes } from '@ember-decorators/object'; +import { scheduleOnce } from '@ember/runloop'; import { task } from 'ember-concurrency'; +import intersection from 'lodash.intersection'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error'; +import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize'; import classic from 'ember-classic-decorator'; @classic @@ -27,11 +31,23 @@ export default class ClientController extends Controller.extend(Sortable, Search { onlyPreemptions: 'preemptions', }, + { + qpNamespace: 'namespace', + }, + { + qpJob: 'job', + }, + { + qpStatus: 'status', + }, ]; // Set in the route flagAsDraining = false; + qpNamespace = ''; + qpJob = ''; + qpStatus = ''; currentPage = 1; pageSize = 8; @@ -50,10 +66,32 @@ export default class ClientController extends Controller.extend(Sortable, Search return this.onlyPreemptions ? this.preemptions : this.model.allocations; } - @alias('visibleAllocations') listToSort; + @computed('visibleAllocations.[]', 'selectionNamespace', 'selectionJob', 'selectionStatus') + get filteredAllocations() { + const { selectionNamespace, selectionJob, selectionStatus } = this; + + return this.visibleAllocations.filter(alloc => { + if (selectionNamespace.length && !selectionNamespace.includes(alloc.get('namespace'))) { + return false; + } + if (selectionJob.length && !selectionJob.includes(alloc.get('plainJobId'))) { + return false; + } + if (selectionStatus.length && !selectionStatus.includes(alloc.clientStatus)) { + return false; + } + return true; + }); + } + + @alias('filteredAllocations') listToSort; @alias('listSorted') listToSearch; @alias('listSearched') sortedAllocations; + @selection('qpNamespace') selectionNamespace; + @selection('qpJob') selectionJob; + @selection('qpStatus') selectionStatus; + eligibilityError = null; stopDrainError = null; drainError = null; @@ -147,4 +185,52 @@ export default class ClientController extends Controller.extend(Sortable, Search const error = messageFromAdapterError(err) || 'Could not run drain'; this.set('drainError', error); } + + get optionsAllocationStatus() { + return [ + { key: 'pending', label: 'Pending' }, + { key: 'running', label: 'Running' }, + { key: 'complete', label: 'Complete' }, + { key: 'failed', label: 'Failed' }, + { key: 'lost', label: 'Lost' }, + ]; + } + + @computed('model.allocations.[]', 'selectionJob', 'selectionNamespace') + get optionsJob() { + // Only show options for jobs in the selected namespaces, if any. + const ns = this.selectionNamespace; + const jobs = Array.from( + new Set( + this.model.allocations + .filter(a => ns.length === 0 || ns.includes(a.namespace)) + .mapBy('plainJobId') + ) + ).compact(); + + // Update query param when the list of jobs changes. + scheduleOnce('actions', () => { + // eslint-disable-next-line ember/no-side-effects + this.set('qpJob', serialize(intersection(jobs, this.selectionJob))); + }); + + return jobs.sort().map(job => ({ key: job, label: job })); + } + + @computed('model.allocations.[]', 'selectionNamespace') + get optionsNamespace() { + const ns = Array.from(new Set(this.model.allocations.mapBy('namespace'))).compact(); + + // Update query param when the list of namespaces changes. + scheduleOnce('actions', () => { + // eslint-disable-next-line ember/no-side-effects + this.set('qpNamespace', serialize(intersection(ns, this.selectionNamespace))); + }); + + return ns.sort().map(n => ({ key: n, label: n })); + } + + setFacetQueryParam(queryParam, selection) { + this.set(queryParam, serialize(selection)); + } } diff --git a/ui/app/controllers/jobs/job/task-group.js b/ui/app/controllers/jobs/job/task-group.js index a85c97d30b2..f8a0ec3620f 100644 --- a/ui/app/controllers/jobs/job/task-group.js +++ b/ui/app/controllers/jobs/job/task-group.js @@ -1,10 +1,14 @@ +/* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */ import { inject as service } from '@ember/service'; import { alias, readOnly } from '@ember/object/computed'; import Controller from '@ember/controller'; import { action, computed, get } from '@ember/object'; +import { scheduleOnce } from '@ember/runloop'; +import intersection from 'lodash.intersection'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; +import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize'; import classic from 'ember-classic-decorator'; @classic @@ -29,11 +33,19 @@ export default class TaskGroupController extends Controller.extend( { sortDescending: 'desc', }, + { + qpStatus: 'status', + }, + { + qpClient: 'client', + }, ]; currentPage = 1; @readOnly('userSettings.pageSize') pageSize; + qpStatus = ''; + qpClient = ''; sortProperty = 'modifyIndex'; sortDescending = true; @@ -47,10 +59,29 @@ export default class TaskGroupController extends Controller.extend( return this.get('model.allocations') || []; } - @alias('allocations') listToSort; + @computed('allocations.[]', 'selectionStatus', 'selectionClient') + get filteredAllocations() { + const { selectionStatus, selectionClient } = this; + + return this.allocations.filter(alloc => { + if (selectionStatus.length && !selectionStatus.includes(alloc.clientStatus)) { + return false; + } + if (selectionClient.length && !selectionClient.includes(alloc.get('node.shortId'))) { + return false; + } + + return true; + }); + } + + @alias('filteredAllocations') listToSort; @alias('listSorted') listToSearch; @alias('listSearched') sortedAllocations; + @selection('qpStatus') selectionStatus; + @selection('qpClient') selectionClient; + @computed('model.scaleState.events.@each.time', function() { const events = get(this, 'model.scaleState.events'); if (events) { @@ -83,4 +114,31 @@ export default class TaskGroupController extends Controller.extend( scaleTaskGroup(count) { return this.model.scale(count); } + + get optionsAllocationStatus() { + return [ + { key: 'pending', label: 'Pending' }, + { key: 'running', label: 'Running' }, + { key: 'complete', label: 'Complete' }, + { key: 'failed', label: 'Failed' }, + { key: 'lost', label: 'Lost' }, + ]; + } + + @computed('model.allocations.[]', 'selectionClient') + get optionsClients() { + const clients = Array.from(new Set(this.model.allocations.mapBy('node.shortId'))).compact(); + + // Update query param when the list of clients changes. + scheduleOnce('actions', () => { + // eslint-disable-next-line ember/no-side-effects + this.set('qpClient', serialize(intersection(clients, this.selectionClient))); + }); + + return clients.sort().map(dc => ({ key: dc, label: dc })); + } + + setFacetQueryParam(queryParam, selection) { + this.set(queryParam, serialize(selection)); + } } diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index f2293d3fc86..028ee23bf40 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -23,6 +23,7 @@ export default class Allocation extends Model { @shortUUIDProperty('id') shortId; @belongsTo('job') job; @belongsTo('node') node; + @attr('string') namespace; @attr('string') name; @attr('string') taskGroupName; @fragment('resources') resources; @@ -38,6 +39,11 @@ export default class Allocation extends Model { @attr('string') clientStatus; @attr('string') desiredStatus; + @computed('') + get plainJobId() { + return JSON.parse(this.belongsTo('job').id())[0]; + } + @computed('clientStatus') get statusIndex() { return STATUS_ORDER[this.clientStatus] || 100; diff --git a/ui/app/styles/components/boxed-section.scss b/ui/app/styles/components/boxed-section.scss index bcebd868f41..901dc75036c 100644 --- a/ui/app/styles/components/boxed-section.scss +++ b/ui/app/styles/components/boxed-section.scss @@ -19,6 +19,15 @@ margin-left: auto; } + .is-subsection { + display: flex; + align-items: baseline; + + .is-padded { + padding: 0em 0em 0em 1em; + } + } + .is-fixed-width { display: inline-block; width: 8em; diff --git a/ui/app/templates/clients/client/index.hbs b/ui/app/templates/clients/client/index.hbs index 320bf7a335a..c7096c9afa9 100644 --- a/ui/app/templates/clients/client/index.hbs +++ b/ui/app/templates/clients/client/index.hbs @@ -294,54 +294,97 @@ {{/if}} - +
+ + + + +
-
- - - - - ID - Created - Modified - Status - Job - Version - Volume - CPU - Memory - - - - - -
- +
+ {{#if this.sortedAllocations.length}} + + + + + ID + Created + Modified + Status + Job + Version + Volume + CPU + Memory + + + + + +
+ +
+
+ {{else}} +
+ {{#if (eq this.visibleAllocations.length 0)}} +

No Allocations

+

+ The node doesn't have any allocations. +

+ {{else if this.searchTerm}} +

No Matches

+

No allocations match the term {{this.searchTerm}}

+ {{else if (eq this.sortedAllocations.length 0)}} +

No Matches

+

+ No allocations match your current filter selection. +

+ {{/if}}
- + {{/if}}
diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index 2472c7b1b38..b031c84a786 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -66,16 +66,32 @@
-
Allocations - +
+ + + +
{{#if this.sortedAllocations}} diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js index 58ccb6ed2ef..cfd9042adcb 100644 --- a/ui/tests/acceptance/client-detail-test.js +++ b/ui/tests/acceptance/client-detail-test.js @@ -130,6 +130,15 @@ module('Acceptance | client detail', function(hooks) { ); }); + 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'); + }); + test('each allocation should have high-level details for the allocation', async function(assert) { const allocation = server.db.allocations .where({ nodeId: node.id }) @@ -1000,6 +1009,47 @@ module('Acceptance | client detail', function(hooks) { assert.notOk(ClientDetail.hasHostVolumes); }); + + 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), + }); + + 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'); + }); }); module('Acceptance | client detail (multi-namespace)', function(hooks) { @@ -1018,7 +1068,11 @@ module('Acceptance | client detail (multi-namespace)', function(hooks) { // Make a job for each namespace, but have both scheduled on the same node server.create('job', { id: 'job-1', namespaceId: 'default', createAllocations: false }); - server.createList('allocation', 3, { nodeId: node.id, clientStatus: 'running' }); + server.createList('allocation', 3, { + nodeId: node.id, + jobId: 'job-1', + clientStatus: 'running', + }); server.create('job', { id: 'job-2', namespaceId: 'other-namespace', createAllocations: false }); server.createList('allocation', 3, { @@ -1047,4 +1101,135 @@ module('Acceptance | client detail (multi-namespace)', function(hooks) { 'Job Two fetched correctly' ); }); + + 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'] + ); + }); }); + +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' + ); + }); +} diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js index a4533f532b7..795a227ee23 100644 --- a/ui/tests/acceptance/task-group-detail-test.js +++ b/ui/tests/acceptance/task-group-detail-test.js @@ -582,4 +582,143 @@ module('Acceptance | task group detail', function(hooks) { scaleEvents.filter(ev => ev.count == null).length ); }); + + testFacet('Status', { + facet: TaskGroup.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 TaskGroup.visit({ id: job.id, name: taskGroup.name }); + }, + filter: (alloc, selection) => + alloc.jobId == job.id && + alloc.taskGroup == taskGroup.name && + selection.includes(alloc.clientStatus), + }); + + testFacet('Client', { + facet: TaskGroup.facets.client, + paramName: 'client', + expectedOptions(allocs) { + return Array.from( + new Set( + allocs + .filter(alloc => alloc.jobId == job.id && alloc.taskGroup == taskGroup.name) + .mapBy('nodeId') + .map(id => id.split('-')[0]) + ) + ).sort(); + }, + async beforeEach() { + const nodes = server.createList('node', 3, 'forceIPv4'); + nodes.forEach(node => + server.createList('allocation', 5, { + nodeId: node.id, + jobId: job.id, + taskGroup: taskGroup.name, + }) + ); + await TaskGroup.visit({ id: job.id, name: taskGroup.name }); + }, + filter: (alloc, selection) => + alloc.jobId == job.id && + alloc.taskGroup == taskGroup.name && + selection.includes(alloc.nodeId.split('-')[0]), + }); }); + +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(); + + TaskGroup.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(); + + TaskGroup.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}/${taskGroup.name}?${paramName}=${encodeURIComponent( + JSON.stringify(selection) + )}`, + 'URL has the correct query param key and value' + ); + }); +} diff --git a/ui/tests/pages/clients/detail.js b/ui/tests/pages/clients/detail.js index 70afd69a7d9..bcfe0d2de28 100644 --- a/ui/tests/pages/clients/detail.js +++ b/ui/tests/pages/clients/detail.js @@ -14,6 +14,7 @@ import allocations from 'nomad-ui/tests/pages/components/allocations'; import twoStepButton from 'nomad-ui/tests/pages/components/two-step-button'; import notification from 'nomad-ui/tests/pages/components/notification'; import toggle from 'nomad-ui/tests/pages/components/toggle'; +import { multiFacet } from 'nomad-ui/tests/pages/components/facet'; export default create({ visit: visitable('/clients/:id'), @@ -38,6 +39,12 @@ export default create({ ...allocations(), + emptyAllocations: { + scope: '[data-test-empty-allocations-list]', + headline: text('[data-test-empty-allocations-list-headline]'), + body: text('[data-test-empty-allocations-list-body]'), + }, + allocationFilter: { preemptions: clickable('[data-test-filter-preemptions]'), all: clickable('[data-test-filter-all]'), @@ -45,6 +52,12 @@ export default create({ allCount: text('[data-test-filter-all]'), }, + facets: { + namespace: multiFacet('[data-test-allocation-namespace-facet]'), + job: multiFacet('[data-test-allocation-job-facet]'), + status: multiFacet('[data-test-allocation-status-facet]'), + }, + attributesTable: isPresent('[data-test-attributes]'), metaTable: isPresent('[data-test-meta]'), emptyMetaMessage: isPresent('[data-test-empty-meta-message]'), diff --git a/ui/tests/pages/jobs/job/task-group.js b/ui/tests/pages/jobs/job/task-group.js index 192ea152a2f..28bd08dbd21 100644 --- a/ui/tests/pages/jobs/job/task-group.js +++ b/ui/tests/pages/jobs/job/task-group.js @@ -13,6 +13,7 @@ import error from 'nomad-ui/tests/pages/components/error'; import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select'; import stepperInput from 'nomad-ui/tests/pages/components/stepper-input'; import LifecycleChart from 'nomad-ui/tests/pages/components/lifecycle-chart'; +import { multiFacet } from 'nomad-ui/tests/pages/components/facet'; export default create({ pageSize: 25, @@ -33,6 +34,11 @@ export default create({ isEmpty: isPresent('[data-test-empty-allocations-list]'), + facets: { + status: multiFacet('[data-test-allocation-status-facet]'), + client: multiFacet('[data-test-allocation-client-facet]'), + }, + lifecycleChart: LifecycleChart, hasVolumes: isPresent('[data-test-volumes]'), diff --git a/ui/tests/unit/serializers/allocation-test.js b/ui/tests/unit/serializers/allocation-test.js index 80d58bbd6b7..5250d8336a0 100644 --- a/ui/tests/unit/serializers/allocation-test.js +++ b/ui/tests/unit/serializers/allocation-test.js @@ -35,6 +35,7 @@ module('Unit | Serializer | Allocation', function(hooks) { attributes: { taskGroupName: 'test-group', name: 'test-summary[1]', + namespace: 'test-namespace', modifyTime: sampleDate, createTime: sampleDate, states: [ @@ -102,6 +103,7 @@ module('Unit | Serializer | Allocation', function(hooks) { attributes: { taskGroupName: 'test-group', name: 'test-summary[1]', + namespace: 'test-namespace', modifyTime: sampleDate, createTime: sampleDate, states: [ @@ -172,6 +174,7 @@ module('Unit | Serializer | Allocation', function(hooks) { attributes: { taskGroupName: 'test-group', name: 'test-summary[1]', + namespace: 'test-namespace', modifyTime: sampleDate, createTime: sampleDate, states: [ @@ -259,6 +262,7 @@ module('Unit | Serializer | Allocation', function(hooks) { attributes: { taskGroupName: 'test-group', name: 'test-summary[1]', + namespace: 'test-namespace', modifyTime: sampleDate, createTime: sampleDate, states: [ @@ -332,6 +336,7 @@ module('Unit | Serializer | Allocation', function(hooks) { attributes: { taskGroupName: 'test-group', name: 'test-summary[1]', + namespace: 'test-namespace', modifyTime: sampleDate, createTime: sampleDate, states: [ diff --git a/ui/tests/unit/serializers/volume-test.js b/ui/tests/unit/serializers/volume-test.js index 72822cf4950..792538fc789 100644 --- a/ui/tests/unit/serializers/volume-test.js +++ b/ui/tests/unit/serializers/volume-test.js @@ -260,6 +260,7 @@ module('Unit | Serializer | Volume', function(hooks) { attributes: { createTime: REF_DATE, modifyTime: REF_DATE, + namespace: 'namespace-2', taskGroupName: 'foobar', wasPreempted: false, states: [], @@ -292,6 +293,7 @@ module('Unit | Serializer | Volume', function(hooks) { attributes: { createTime: REF_DATE, modifyTime: REF_DATE, + namespace: 'namespace-2', taskGroupName: 'write-here', wasPreempted: false, states: [], @@ -324,6 +326,7 @@ module('Unit | Serializer | Volume', function(hooks) { attributes: { createTime: REF_DATE, modifyTime: REF_DATE, + namespace: 'namespace-2', taskGroupName: 'look-if-you-must', wasPreempted: false, states: [],