diff --git a/ui/app/controllers/allocations/allocation/index.js b/ui/app/controllers/allocations/allocation/index.js index 4d6697c1404..7ccfd9b91b7 100644 --- a/ui/app/controllers/allocations/allocation/index.js +++ b/ui/app/controllers/allocations/allocation/index.js @@ -1,6 +1,6 @@ -import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; +import { alias } from '@ember/object/computed'; import Sortable from 'nomad-ui/mixins/sortable'; import { lazyClick } from 'nomad-ui/helpers/lazy-click'; @@ -18,6 +18,9 @@ export default Controller.extend(Sortable, { listToSort: alias('model.states'), sortedStates: alias('listSorted'), + // Set in the route + preempter: null, + actions: { gotoTask(allocation, task) { this.transitionToRoute('allocations.allocation.task', task); diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index 26dfc9b64da..9044fd1f7e9 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -10,6 +10,7 @@ export default Controller.extend(Sortable, Searchable, { searchTerm: 'search', sortProperty: 'sort', sortDescending: 'desc', + onlyPreemptions: 'preemptions', }, currentPage: 1, @@ -20,10 +21,25 @@ export default Controller.extend(Sortable, Searchable, { searchProps: computed(() => ['shortId', 'name']), - listToSort: alias('model.allocations'), + onlyPreemptions: false, + + visibleAllocations: computed( + 'model.allocations.[]', + 'preemptions.[]', + 'onlyPreemptions', + function() { + return this.onlyPreemptions ? this.preemptions : this.model.allocations; + } + ), + + listToSort: alias('visibleAllocations'), listToSearch: alias('listSorted'), sortedAllocations: alias('listSearched'), + preemptions: computed('model.allocations.@each.wasPreempted', function() { + return this.model.allocations.filterBy('wasPreempted'); + }), + sortedEvents: computed('model.events.@each.time', function() { return this.get('model.events') .sortBy('time') @@ -38,5 +54,9 @@ export default Controller.extend(Sortable, Searchable, { gotoAllocation(allocation) { this.transitionToRoute('allocations.allocation', allocation); }, + + setPreemptionFilter(value) { + this.set('onlyPreemptions', value); + }, }, }); diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index b4563bf3969..f93f19175ce 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -3,7 +3,7 @@ import { computed } from '@ember/object'; import { equal } from '@ember/object/computed'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; -import { belongsTo } from 'ember-data/relationships'; +import { belongsTo, hasMany } from 'ember-data/relationships'; import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; import intersection from 'lodash.intersection'; import shortUUIDProperty from '../utils/properties/short-uuid'; @@ -46,6 +46,10 @@ export default Model.extend({ previousAllocation: belongsTo('allocation', { inverse: 'nextAllocation' }), nextAllocation: belongsTo('allocation', { inverse: 'previousAllocation' }), + preemptedAllocations: hasMany('allocation', { inverse: 'preemptedByAllocation' }), + preemptedByAllocation: belongsTo('allocation', { inverse: 'preemptedAllocations' }), + wasPreempted: attr('boolean'), + followUpEvaluation: belongsTo('evaluation'), statusClass: computed('clientStatus', function() { @@ -88,9 +92,11 @@ export default Model.extend({ 'clientStatus', 'followUpEvaluation.content', function() { - return !this.get('nextAllocation.content') && - !this.get('followUpEvaluation.content') && - this.clientStatus === 'failed'; + return ( + !this.get('nextAllocation.content') && + !this.get('followUpEvaluation.content') && + this.clientStatus === 'failed' + ); } ), }); diff --git a/ui/app/models/job-plan.js b/ui/app/models/job-plan.js index 8f9c10345f1..1ddc3d4b807 100644 --- a/ui/app/models/job-plan.js +++ b/ui/app/models/job-plan.js @@ -1,8 +1,10 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; +import { hasMany } from 'ember-data/relationships'; export default Model.extend({ diff: attr(), failedTGAllocs: fragmentArray('placement-failure', { defaultValue: () => [] }), + preemptions: hasMany('allocation'), }); diff --git a/ui/app/routes/allocations/allocation/index.js b/ui/app/routes/allocations/allocation/index.js new file mode 100644 index 00000000000..ebe985da28f --- /dev/null +++ b/ui/app/routes/allocations/allocation/index.js @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; + +export default Route.extend({ + setupController(controller, model) { + // Suppress the preemptedByAllocation fetch error in the event it's a 404 + if (model) { + const setPreempter = () => controller.set('preempter', model.preemptedByAllocation); + model.preemptedByAllocation.then(setPreempter, setPreempter); + } + + return this._super(...arguments); + }, +}); diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 08da6e6c44c..a629451d974 100644 --- a/ui/app/serializers/allocation.js +++ b/ui/app/serializers/allocation.js @@ -45,6 +45,10 @@ export default ApplicationSerializer.extend({ hash.NextAllocationID = hash.NextAllocation ? hash.NextAllocation : null; hash.FollowUpEvaluationID = hash.FollowupEvalID ? hash.FollowupEvalID : null; + hash.PreemptedAllocationIDs = hash.PreemptedAllocations || []; + hash.PreemptedByAllocationID = hash.PreemptedByAllocation || null; + hash.WasPreempted = !!hash.PreemptedByAllocationID; + return this._super(typeHash, hash); }, }); diff --git a/ui/app/serializers/job-plan.js b/ui/app/serializers/job-plan.js index 19f9556d304..e44e47d6f96 100644 --- a/ui/app/serializers/job-plan.js +++ b/ui/app/serializers/job-plan.js @@ -1,5 +1,6 @@ import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; +import { get } from '@ember/object'; export default ApplicationSerializer.extend({ normalize(typeHash, hash) { @@ -7,6 +8,7 @@ export default ApplicationSerializer.extend({ hash.FailedTGAllocs = Object.keys(failures).map(key => { return assign({ Name: key }, failures[key] || {}); }); + hash.PreemptionIDs = (get(hash, 'Annotations.PreemptedAllocs') || []).mapBy('ID'); return this._super(...arguments); }, }); diff --git a/ui/app/styles/components/badge.scss b/ui/app/styles/components/badge.scss index 60f4f2c74b5..98087d9d86c 100644 --- a/ui/app/styles/components/badge.scss +++ b/ui/app/styles/components/badge.scss @@ -4,6 +4,7 @@ line-height: 1; border-radius: $radius; padding: 0.25em 0.75em; + border: none; @each $name, $pair in $colors { $color: nth($pair, 1); @@ -43,3 +44,7 @@ background: lighten($grey-blue, 10%); } } + +button.badge { + cursor: pointer; +} diff --git a/ui/app/styles/components/page-layout.scss b/ui/app/styles/components/page-layout.scss index 26bcf732e9e..b654dfb1645 100644 --- a/ui/app/styles/components/page-layout.scss +++ b/ui/app/styles/components/page-layout.scss @@ -36,7 +36,6 @@ &.is-right { margin-left: $gutter-width; - overflow-x: auto; } @media #{$mq-hidden-gutter} { diff --git a/ui/app/styles/core/table.scss b/ui/app/styles/core/table.scss index 63822653b30..0cd6a925db0 100644 --- a/ui/app/styles/core/table.scss +++ b/ui/app/styles/core/table.scss @@ -14,6 +14,10 @@ } } + &.is-isolated { + margin-bottom: 0; + } + &.with-foot { margin-bottom: 0; border-bottom-left-radius: 0; diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 19d499dc5a7..09df050e2c4 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -87,4 +87,74 @@ {{/if}} + + {{#if model.wasPreempted}} +
+
Preempted By
+
+ {{#if (not preempter)}} +
+

Allocation is gone

+

This allocation has been stopped and garbage collected.

+
+ {{else}} +
+
+ + + {{preempter.clientStatus}} + + + + {{preempter.name}} + {{#link-to "allocations.allocation" preempter data-test-allocation-id}}{{preempter.shortId}}{{/link-to}} + + Job + {{#link-to "jobs.job" preempter.job (query-params jobNamespace=preempter.job.namespace.id) data-test-job-link}}{{preempter.job.name}}{{/link-to}} + + Priority + {{preempter.job.priority}} + + Client + {{#link-to "clients.client" preempter.node data-test-client-link}}{{preempter.node.shortId}}{{/link-to}} + + Reserved CPU + {{preempter.resources.cpu}} MHz + + Reserved Memory + {{preempter.resources.memory}} MiB + +
+
+ {{/if}} +
+
+ {{/if}} + + {{#if (and model.preemptedAllocations.isFulfilled model.preemptedAllocations.length)}} +
+
Preempted Allocations
+
+ {{#list-table + source=model.preemptedAllocations + class="allocations is-isolated" as |t|}} + {{#t.head}} + + ID + Task Group + Created + Modified + Status + Version + Node + CPU + Memory + {{/t.head}} + {{#t.body as |row|}} + {{allocation-row allocation=row.model context="job" data-test-allocation=row.model.id}} + {{/t.body}} + {{/list-table}} +
+
+ {{/if}} diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 99e36f6975e..7e87fee34b9 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -95,7 +95,17 @@
-
Allocations {{model.allocations.length}}
+
+ Allocations + + {{#if preemptions.length}} + + {{/if}} +
{{search-box searchTerm=(mut searchTerm) onChange=(action resetPagination) diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index 1bd6c4ec326..04bfdaf5873 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -9,6 +9,11 @@ {{x-icon "history" class="is-faded"}} {{/if}} + {{#if allocation.wasPreempted}} + + {{x-icon "boot" class="is-faded"}} + + {{/if}} {{#link-to "allocations.allocation" allocation class="is-primary"}} diff --git a/ui/app/templates/components/job-editor.hbs b/ui/app/templates/components/job-editor.hbs index 8f85ebc5859..52969285c5d 100644 --- a/ui/app/templates/components/job-editor.hbs +++ b/ui/app/templates/components/job-editor.hbs @@ -88,6 +88,34 @@ {{/if}}
+ {{#if (and planOutput.preemptions.isFulfilled planOutput.preemptions.length)}} +
+
+ Preemptions (if you choose to run this job, these allocations will be stopped) +
+
+ {{#list-table + source=planOutput.preemptions + class="allocations is-isolated" as |t|}} + {{#t.head}} + + ID + Task Group + Created + Modified + Status + Version + Node + CPU + Memory + {{/t.head}} + {{#t.body as |row|}} + {{allocation-row allocation=row.model context="job"}} + {{/t.body}} + {{/list-table}} +
+
+ {{/if}}
diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index 125ea5dd3fc..ba36082fda6 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -124,6 +124,20 @@ export default Factory.extend({ }, }), + preempted: trait({ + afterCreate(allocation, server) { + const preempter = server.create('allocation', { preemptedAllocations: [allocation.id] }); + allocation.update({ preemptedByAllocation: preempter.id }); + }, + }), + + preempter: trait({ + afterCreate(allocation, server) { + const preempted = server.create('allocation', { preemptedByAllocation: allocation.id }); + allocation.update({ preemptedAllocations: [preempted.id] }); + }, + }), + afterCreate(allocation, server) { Ember.assert( '[Mirage] No jobs! make sure jobs are created before allocations', diff --git a/ui/public/images/icons/boot.svg b/ui/public/images/icons/boot.svg new file mode 100644 index 00000000000..116bd024593 --- /dev/null +++ b/ui/public/images/icons/boot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index 21b613e2827..3e5ca598066 100644 --- a/ui/tests/acceptance/allocation-detail-test.js +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -199,3 +199,106 @@ module('Acceptance | allocation detail (not running)', function(hooks) { ); }); }); + +module('Acceptance | allocation detail (preemptions)', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function() { + server.create('agent'); + node = server.create('node'); + job = server.create('job', { createAllocations: false }); + }); + + test('shows a dedicated section to the allocation that preempted this allocation', async function(assert) { + allocation = server.create('allocation', 'preempted'); + const preempter = server.schema.find('allocation', allocation.preemptedByAllocation); + const preempterJob = server.schema.find('job', preempter.jobId); + const preempterClient = server.schema.find('node', preempter.nodeId); + + await Allocation.visit({ id: allocation.id }); + assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown'); + assert.equal(Allocation.preempter.status, preempter.clientStatus, 'Preempter status matches'); + assert.equal(Allocation.preempter.name, preempter.name, 'Preempter name matches'); + assert.equal( + Allocation.preempter.priority, + preempterJob.priority, + 'Preempter priority matches' + ); + + await Allocation.preempter.visit(); + assert.equal( + currentURL(), + `/allocations/${preempter.id}`, + 'Clicking the preempter id navigates to the preempter allocation detail page' + ); + + await Allocation.visit({ id: allocation.id }); + await Allocation.preempter.visitJob(); + assert.equal( + currentURL(), + `/jobs/${preempterJob.id}`, + 'Clicking the preempter job link navigates to the preempter job page' + ); + + await Allocation.visit({ id: allocation.id }); + await Allocation.preempter.visitClient(); + assert.equal( + currentURL(), + `/clients/${preempterClient.id}`, + 'Clicking the preempter client link navigates to the preempter client page' + ); + }); + + test('shows a dedicated section to the allocations this allocation preempted', async function(assert) { + allocation = server.create('allocation', 'preempter'); + await Allocation.visit({ id: allocation.id }); + assert.ok(Allocation.preempted, 'The allocations this allocation preempted are shown'); + }); + + test('each preempted allocation in the table lists basic allocation information', async function(assert) { + allocation = server.create('allocation', 'preempter'); + await Allocation.visit({ id: allocation.id }); + + const preemption = allocation.preemptedAllocations + .map(id => server.schema.find('allocation', id)) + .sortBy('modifyIndex') + .reverse()[0]; + const preemptionRow = Allocation.preemptions.objectAt(0); + + assert.equal( + Allocation.preemptions.length, + allocation.preemptedAllocations.length, + 'The preemptions table has a row for each preempted allocation' + ); + + assert.equal(preemptionRow.shortId, preemption.id.split('-')[0], 'Preemption short id'); + assert.equal( + preemptionRow.createTime, + moment(preemption.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'), + 'Preemption create time' + ); + assert.equal( + preemptionRow.modifyTime, + moment(preemption.modifyTime / 1000000).fromNow(), + 'Preemption modify time' + ); + assert.equal(preemptionRow.status, preemption.clientStatus, 'Client status'); + assert.equal(preemptionRow.jobVersion, preemption.jobVersion, 'Job Version'); + assert.equal( + preemptionRow.client, + server.db.nodes.find(preemption.nodeId).id.split('-')[0], + 'Node ID' + ); + + await preemptionRow.visitClient(); + assert.equal(currentURL(), `/clients/${preemption.nodeId}`, 'Node links to node page'); + }); + + test('when an allocation both preempted allocations and was preempted itself, both preemptions sections are shown', async function(assert) { + allocation = server.create('allocation', 'preempter', 'preempted'); + await Allocation.visit({ id: allocation.id }); + assert.ok(Allocation.preempted, 'The allocations this allocation preempted are shown'); + assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown'); + }); +}); diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js index 5750ee6e2c3..2307731ed94 100644 --- a/ui/tests/acceptance/client-detail-test.js +++ b/ui/tests/acceptance/client-detail-test.js @@ -12,6 +12,8 @@ import Jobs from 'nomad-ui/tests/pages/jobs/list'; let node; +const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation; + module('Acceptance | client detail', function(hooks) { setupApplicationTest(hooks); setupMirage(hooks); @@ -24,6 +26,7 @@ module('Acceptance | client detail', function(hooks) { server.create('agent'); server.create('job', { createAllocations: false }); server.createList('allocation', 3, { nodeId: node.id, clientStatus: 'running' }); + server.create('allocation', 'preempted', { nodeId: node.id, clientStatus: 'running' }); }); test('/clients/:id should have a breadcrumb trail linking back to clients', async function(assert) { @@ -219,6 +222,65 @@ module('Acceptance | client detail', function(hooks) { ); }); + test('the allocation section should show the count of preempted allocations on the client', async function(assert) { + const allocations = server.db.allocations.where({ nodeId: node.id }); + + await ClientDetail.visit({ id: node.id }); + + assert.equal( + ClientDetail.allocationFilter.allCount, + allocations.length, + 'All filter/badge shows all allocations count' + ); + assert.ok( + ClientDetail.allocationFilter.preemptionsCount.startsWith( + allocations.filter(wasPreemptedFilter).length + ), + 'Preemptions filter/badge shows preempted allocations count' + ); + }); + + test('clicking the preemption badge filters the allocations table and sets a query param', async function(assert) { + const allocations = server.db.allocations.where({ nodeId: node.id }); + + await ClientDetail.visit({ id: node.id }); + await ClientDetail.allocationFilter.preemptions(); + + assert.equal( + ClientDetail.allocations.length, + allocations.filter(wasPreemptedFilter).length, + 'Only preempted allocations are shown' + ); + assert.equal( + currentURL(), + `/clients/${node.id}?preemptions=true`, + 'Filter is persisted in the URL' + ); + }); + + test('clicking the total allocations badge resets the filter and removes the query param', async function(assert) { + const allocations = server.db.allocations.where({ nodeId: node.id }); + + await ClientDetail.visit({ id: node.id }); + await ClientDetail.allocationFilter.preemptions(); + await ClientDetail.allocationFilter.all(); + + assert.equal(ClientDetail.allocations.length, allocations.length, 'All allocations are shown'); + assert.equal(currentURL(), `/clients/${node.id}`, 'Filter is persisted in the URL'); + }); + + test('navigating directly to the client detail page with the preemption query param set will filter the allocations table', async function(assert) { + const allocations = server.db.allocations.where({ nodeId: node.id }); + + await ClientDetail.visit({ id: node.id, preemptions: true }); + + assert.equal( + ClientDetail.allocations.length, + allocations.filter(wasPreemptedFilter).length, + 'Only preempted allocations are shown' + ); + }); + test('/clients/:id should list all attributes for the node', async function(assert) { await ClientDetail.visit({ id: node.id }); diff --git a/ui/tests/integration/allocation-row-test.js b/ui/tests/integration/allocation-row-test.js index bd3c48f210f..70b816ac68b 100644 --- a/ui/tests/integration/allocation-row-test.js +++ b/ui/tests/integration/allocation-row-test.js @@ -123,6 +123,23 @@ module('Integration | Component | allocation row', function(hooks) { }); }); + test('Allocation row shows an icon indicator when it was preempted', async function(assert) { + const allocId = this.server.create('allocation', 'preempted').id; + + const allocation = await this.store.findRecord('allocation', allocId); + await settled(); + + this.setProperties({ allocation, context: 'job' }); + await render(hbs` + {{allocation-row + allocation=allocation + context=context}} + `); + await settled(); + + assert.ok(find('[data-test-icon="preemption"]'), 'Preempted icon is shown'); + }); + test('when an allocation is not running, the utilization graphs are omitted', function(assert) { this.setProperties({ context: 'job', diff --git a/ui/tests/pages/allocations/detail.js b/ui/tests/pages/allocations/detail.js index 54ce5accaed..f1e5348f253 100644 --- a/ui/tests/pages/allocations/detail.js +++ b/ui/tests/pages/allocations/detail.js @@ -8,6 +8,8 @@ import { visitable, } from 'ember-cli-page-object'; +import allocations from 'nomad-ui/tests/pages/components/allocations'; + export default create({ visit: visitable('/allocations/:id'), @@ -51,6 +53,24 @@ export default create({ isEmpty: isPresent('[data-test-empty-tasks-list]'), + wasPreempted: isPresent('[data-test-was-preempted]'), + preempter: { + scope: '[data-test-was-preempted]', + + status: text('[data-test-allocation-status]'), + name: text('[data-test-allocation-name]'), + priority: text('[data-test-job-priority]'), + reservedCPU: text('[data-test-allocation-cpu]'), + reservedMemory: text('[data-test-allocation-memory]'), + + visit: clickable('[data-test-allocation-id]'), + visitJob: clickable('[data-test-job-link]'), + visitClient: clickable('[data-test-client-link]'), + }, + + preempted: isPresent('[data-test-preemptions]'), + ...allocations('[data-test-preemptions] [data-test-allocation]', 'preemptions'), + error: { isShown: isPresent('[data-test-error]'), title: text('[data-test-error-title]'), diff --git a/ui/tests/pages/clients/detail.js b/ui/tests/pages/clients/detail.js index c4b98d9ded3..c0db54b74b2 100644 --- a/ui/tests/pages/clients/detail.js +++ b/ui/tests/pages/clients/detail.js @@ -45,6 +45,13 @@ export default create({ ...allocations(), + allocationFilter: { + preemptions: clickable('[data-test-filter-preemptions]'), + all: clickable('[data-test-filter-all]'), + preemptionsCount: text('[data-test-filter-preemptions]'), + allCount: text('[data-test-filter-all]'), + }, + attributesTable: isPresent('[data-test-attributes]'), metaTable: isPresent('[data-test-meta]'), emptyMetaMessage: isPresent('[data-test-empty-meta-message]'), diff --git a/ui/tests/pages/components/allocations.js b/ui/tests/pages/components/allocations.js index 29cfb3ffca0..99307e5c782 100644 --- a/ui/tests/pages/components/allocations.js +++ b/ui/tests/pages/components/allocations.js @@ -1,8 +1,8 @@ import { attribute, collection, clickable, isPresent, text } from 'ember-cli-page-object'; -export default function(selector = '[data-test-allocation]') { +export default function(selector = '[data-test-allocation]', propKey = 'allocations') { return { - allocations: collection(selector, { + [propKey]: collection(selector, { id: attribute('data-test-allocation'), shortId: text('[data-test-short-id]'), createTime: text('[data-test-create-time]'), diff --git a/ui/tests/unit/serializers/allocation-test.js b/ui/tests/unit/serializers/allocation-test.js index db928953152..9036f136df6 100644 --- a/ui/tests/unit/serializers/allocation-test.js +++ b/ui/tests/unit/serializers/allocation-test.js @@ -44,6 +44,7 @@ module('Unit | Serializer | Allocation', function(hooks) { failed: false, }, ], + wasPreempted: false, }, relationships: { followUpEvaluation: { @@ -55,6 +56,12 @@ module('Unit | Serializer | Allocation', function(hooks) { previousAllocation: { data: null, }, + preemptedAllocations: { + data: [], + }, + preemptedByAllocation: { + data: null, + }, job: { data: { id: '["test-summary","test-namespace"]', @@ -108,6 +115,71 @@ module('Unit | Serializer | Allocation', function(hooks) { failed: true, }, ], + wasPreempted: false, + }, + relationships: { + followUpEvaluation: { + data: null, + }, + nextAllocation: { + data: null, + }, + previousAllocation: { + data: null, + }, + preemptedAllocations: { + data: [], + }, + preemptedByAllocation: { + data: null, + }, + job: { + data: { + id: '["test-summary","test-namespace"]', + type: 'job', + }, + }, + }, + }, + }, + }, + + { + name: 'With preemptions', + in: { + ID: 'test-allocation', + JobID: 'test-summary', + Name: 'test-summary[1]', + Namespace: 'test-namespace', + TaskGroup: 'test-group', + CreateTime: +sampleDate * 1000000, + ModifyTime: +sampleDate * 1000000, + TaskStates: { + task: { + State: 'running', + Failed: false, + }, + }, + PreemptedByAllocation: 'preempter-allocation', + PreemptedAllocations: ['preempted-one-allocation', 'preempted-two-allocation'], + }, + out: { + data: { + id: 'test-allocation', + type: 'allocation', + attributes: { + taskGroupName: 'test-group', + name: 'test-summary[1]', + modifyTime: sampleDate, + createTime: sampleDate, + states: [ + { + name: 'task', + state: 'running', + failed: false, + }, + ], + wasPreempted: true, }, relationships: { followUpEvaluation: { @@ -119,6 +191,18 @@ module('Unit | Serializer | Allocation', function(hooks) { previousAllocation: { data: null, }, + preemptedAllocations: { + data: [ + { id: 'preempted-one-allocation', type: 'allocation' }, + { id: 'preempted-two-allocation', type: 'allocation' }, + ], + }, + preemptedByAllocation: { + data: { + id: 'preempter-allocation', + type: 'allocation', + }, + }, job: { data: { id: '["test-summary","test-namespace"]', diff --git a/ui/tests/unit/serializers/job-plan-test.js b/ui/tests/unit/serializers/job-plan-test.js index d56690c3ebb..994f811c24e 100644 --- a/ui/tests/unit/serializers/job-plan-test.js +++ b/ui/tests/unit/serializers/job-plan-test.js @@ -38,7 +38,11 @@ module('Unit | Serializer | JobPlan', function(hooks) { }, ], }, - relationships: {}, + relationships: { + preemptions: { + data: [], + }, + }, }, }, }, @@ -78,7 +82,57 @@ module('Unit | Serializer | JobPlan', function(hooks) { }, ], }, - relationships: {}, + relationships: { + preemptions: { + data: [], + }, + }, + }, + }, + }, + + { + name: 'With preemptions', + in: { + ID: 'test-plan', + Diff: { + Arbitrary: 'Value', + }, + FailedTGAllocs: { + task: { + NodesAvailable: 10, + }, + }, + Annotations: { + PreemptedAllocs: [ + { ID: 'preemption-one-allocation' }, + { ID: 'preemption-two-allocation' }, + ], + }, + }, + out: { + data: { + id: 'test-plan', + type: 'job-plan', + attributes: { + diff: { + Arbitrary: 'Value', + }, + failedTGAllocs: [ + { + name: 'task', + nodesAvailable: 10, + }, + ], + }, + relationships: { + preemptions: { + data: [ + { id: 'preemption-one-allocation', type: 'allocation' }, + { id: 'preemption-two-allocation', type: 'allocation' }, + ], + }, + }, }, }, },