From d50de22b0f84d868b4fdff459f853b8a0da5629d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 24 Apr 2018 17:23:31 -0700 Subject: [PATCH 01/16] New icon for showing history/paper trail layers.svg from Open Iconic --- ui/public/images/icons/history.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 ui/public/images/icons/history.svg diff --git a/ui/public/images/icons/history.svg b/ui/public/images/icons/history.svg new file mode 100644 index 00000000000..714265f25c6 --- /dev/null +++ b/ui/public/images/icons/history.svg @@ -0,0 +1,3 @@ + + + From 0344715ece74c9a711dcbb82f6a3a8c01a057218 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 25 Apr 2018 15:50:15 -0700 Subject: [PATCH 02/16] Modeling alloc reschedule events --- ui/app/models/allocation.js | 7 +++++++ ui/app/models/node.js | 2 +- ui/app/models/reschedule-event.js | 12 ++++++++++++ ui/app/serializers/allocation.js | 7 +++++++ ui/app/serializers/reschedule-event.js | 19 +++++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 ui/app/models/reschedule-event.js create mode 100644 ui/app/serializers/reschedule-event.js diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 6d85ae85f57..6348ee5ca5b 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -37,6 +37,12 @@ export default Model.extend({ return STATUS_ORDER[this.get('clientStatus')] || 100; }), + // When allocations are server-side rescheduled, a paper trail + // is left linking all reschedule attempts. + previousAllocation: belongsTo('allocation', { inverse: 'nextAllocation' }), + nextAllocation: belongsTo('allocation', { inverse: 'previousAllocation' }), + previousNode: belongsTo('node', { inverse: null }), + statusClass: computed('clientStatus', function() { const classMap = { pending: 'is-pending', @@ -67,4 +73,5 @@ export default Model.extend({ }, states: fragmentArray('task-state'), + rescheduleEvents: fragmentArray('reschedule-event'), }); diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 74feacb4eac..4b5f34a44b9 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -36,5 +36,5 @@ export default Model.extend({ return this.get('httpAddr') == null; }), - allocations: hasMany('allocations'), + allocations: hasMany('allocations', { inverse: 'node' }), }); diff --git a/ui/app/models/reschedule-event.js b/ui/app/models/reschedule-event.js new file mode 100644 index 00000000000..ae32ca3e6e2 --- /dev/null +++ b/ui/app/models/reschedule-event.js @@ -0,0 +1,12 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner } from 'ember-data-model-fragments/attributes'; + +export default Fragment.extend({ + allocation: fragmentOwner(), + + previousAllocationID: attr('string'), + previousNodeID: attr('string'), + time: attr('date'), + delay: attr('string'), +}); diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 8a7a5d43baf..49f71e34eb3 100644 --- a/ui/app/serializers/allocation.js +++ b/ui/app/serializers/allocation.js @@ -37,6 +37,13 @@ export default ApplicationSerializer.extend({ hash.ModifyTimeNanos = hash.ModifyTime % 1000000; hash.ModifyTime = Math.floor(hash.ModifyTime / 1000000); + hash.RescheduleEvents = (hash.RescheduleTracker || {}).Events; + + if (hash.RescheduleEvents && hash.RescheduleEvents.length) { + hash.PreviousAllocationID = hash.RescheduleEvents[0].PrevAllocID; + hash.PreviousNodeID = hash.RescheduleEvents[0].PrevNodeID; + } + return this._super(typeHash, hash); }, }); diff --git a/ui/app/serializers/reschedule-event.js b/ui/app/serializers/reschedule-event.js new file mode 100644 index 00000000000..3a40cc62f09 --- /dev/null +++ b/ui/app/serializers/reschedule-event.js @@ -0,0 +1,19 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + attrs: { + previousAllocationID: 'PrevAllocID', + previousNodeID: 'PrevNodeID', + }, + + normalize(typeHash, hash) { + // Time is in the form of nanoseconds since epoch, but JS dates + // only understand time to the millisecond precision. So store + // the time (precise to ms) as a date, and store the remaining ns + // as a number to deal with when it comes up. + hash.TimeNanos = hash.RescheduleTime % 1000000; + hash.Time = Math.floor(hash.RescheduleTime / 1000000); + + return this._super(typeHash, hash); + }, +}); From edb4e8229d7d45c1201dd9d6967a4f5fe353f741 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 2 May 2018 11:14:13 -0700 Subject: [PATCH 03/16] New is-faded modifier for icons For when the icon should be less prominent than the content around it --- ui/app/styles/core/icon.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/app/styles/core/icon.scss b/ui/app/styles/core/icon.scss index fef6cafe83b..c9618655837 100644 --- a/ui/app/styles/core/icon.scss +++ b/ui/app/styles/core/icon.scss @@ -28,6 +28,10 @@ $icon-dimensions-large: 2rem; width: $icon-dimensions-large; } + &.is-faded { + fill: $grey-light; + } + @each $name, $pair in $colors { $color: nth($pair, 1); From 5394ecfbe57d32edbe22baeafd0842cea6c89364 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 2 May 2018 11:16:11 -0700 Subject: [PATCH 04/16] New is-hollow modifer for boxed-sections An open layout that makes its contents feel less cramped. Useful for large visualizations. --- ui/app/styles/components/boxed-section.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ui/app/styles/components/boxed-section.scss b/ui/app/styles/components/boxed-section.scss index cc22c03a61c..31e205f8d69 100644 --- a/ui/app/styles/components/boxed-section.scss +++ b/ui/app/styles/components/boxed-section.scss @@ -28,6 +28,15 @@ background: $white; } + &.is-hollow { + border-bottom: none; + background: transparent; + + & + .boxed-section-body { + border-top: none; + } + } + & + .boxed-section-body { padding: 1.5em; border-top-left-radius: 0; From 940210875dbdf75924911084d2d521093514c780 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 2 May 2018 11:18:02 -0700 Subject: [PATCH 05/16] New is-narrow modifier for slim table cells Useful for actions, icons, and checkboxes --- ui/app/styles/core/table.scss | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ui/app/styles/core/table.scss b/ui/app/styles/core/table.scss index abc1aad8fff..97ff418455e 100644 --- a/ui/app/styles/core/table.scss +++ b/ui/app/styles/core/table.scss @@ -78,6 +78,16 @@ width: 25%; } + &.is-truncatable { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &.is-narrow { + padding: 1.25em 0 1.25em 0.5em; + } + // Only use px modifiers when text needs to be truncated. // In this and only this scenario are percentages not effective. &.is-200px { @@ -85,12 +95,6 @@ max-width: 200px; } - &.is-truncatable { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - @for $i from 1 through 11 { &.is-#{$i} { width: 100% / 12 * $i; From 901ba35ac550a5bbf507b380fe2407f915c81ab2 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 2 May 2018 11:21:01 -0700 Subject: [PATCH 06/16] Add shortId properties to reschedule events model --- ui/app/models/reschedule-event.js | 8 ++++++-- ui/app/serializers/reschedule-event.js | 8 +++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ui/app/models/reschedule-event.js b/ui/app/models/reschedule-event.js index ae32ca3e6e2..911fe789882 100644 --- a/ui/app/models/reschedule-event.js +++ b/ui/app/models/reschedule-event.js @@ -1,12 +1,16 @@ import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; +import shortUUIDProperty from '../utils/properties/short-uuid'; export default Fragment.extend({ allocation: fragmentOwner(), - previousAllocationID: attr('string'), - previousNodeID: attr('string'), + previousAllocationId: attr('string'), + previousNodeId: attr('string'), time: attr('date'), delay: attr('string'), + + previousAllocationShortId: shortUUIDProperty('previousAllocationId'), + previousNodeShortId: shortUUIDProperty('previousNodeShortId'), }); diff --git a/ui/app/serializers/reschedule-event.js b/ui/app/serializers/reschedule-event.js index 3a40cc62f09..775ddac65a1 100644 --- a/ui/app/serializers/reschedule-event.js +++ b/ui/app/serializers/reschedule-event.js @@ -1,11 +1,6 @@ import ApplicationSerializer from './application'; export default ApplicationSerializer.extend({ - attrs: { - previousAllocationID: 'PrevAllocID', - previousNodeID: 'PrevNodeID', - }, - normalize(typeHash, hash) { // Time is in the form of nanoseconds since epoch, but JS dates // only understand time to the millisecond precision. So store @@ -14,6 +9,9 @@ export default ApplicationSerializer.extend({ hash.TimeNanos = hash.RescheduleTime % 1000000; hash.Time = Math.floor(hash.RescheduleTime / 1000000); + hash.PreviousAllocationId = hash.PrevAllocID ? hash.PrevAllocID : null; + hash.PreviousNodeId = hash.PrevNodeID ? hash.PrevNodeID : null; + return this._super(typeHash, hash); }, }); From 8d18997b1c5ac26df1e86f49846e45521610750d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 2 May 2018 11:21:50 -0700 Subject: [PATCH 07/16] Add follow up eval relationship to allocation --- ui/app/models/allocation.js | 7 ++++++- ui/app/serializers/allocation.js | 10 ++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 6348ee5ca5b..6e8952808b5 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -41,7 +41,8 @@ export default Model.extend({ // is left linking all reschedule attempts. previousAllocation: belongsTo('allocation', { inverse: 'nextAllocation' }), nextAllocation: belongsTo('allocation', { inverse: 'previousAllocation' }), - previousNode: belongsTo('node', { inverse: null }), + + followUpEvaluation: belongsTo('evaluation'), statusClass: computed('clientStatus', function() { const classMap = { @@ -74,4 +75,8 @@ export default Model.extend({ states: fragmentArray('task-state'), rescheduleEvents: fragmentArray('reschedule-event'), + + hasRescheduleEvents: computed('rescheduleEvents.length', 'nextAllocation', function() { + return this.get('rescheduleEvents.length') > 0 || this.get('nextAllocation'); + }), }); diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 49f71e34eb3..4bfef2c3c04 100644 --- a/ui/app/serializers/allocation.js +++ b/ui/app/serializers/allocation.js @@ -39,10 +39,12 @@ export default ApplicationSerializer.extend({ hash.RescheduleEvents = (hash.RescheduleTracker || {}).Events; - if (hash.RescheduleEvents && hash.RescheduleEvents.length) { - hash.PreviousAllocationID = hash.RescheduleEvents[0].PrevAllocID; - hash.PreviousNodeID = hash.RescheduleEvents[0].PrevNodeID; - } + // API returns empty strings instead of null + hash.PreviousAllocationID = hash.PreviousAllocation ? hash.PreviousAllocation : null; + hash.NextAllocationID = hash.NextAllocation ? hash.NextAllocation : null; + hash.FollowUpEvaluationID = hash.FollowupEvalID ? hash.FollowupEvalID : null; + + console.log(hash.FollowUpEvaluationID); return this._super(typeHash, hash); }, From 5db8e6d13589552397fc1dd3cfba198eaa2f3d21 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 2 May 2018 13:59:28 -0700 Subject: [PATCH 08/16] Refactor allocation-row --- ui/app/components/allocation-row.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ui/app/components/allocation-row.js b/ui/app/components/allocation-row.js index d6236d6a283..dab83046813 100644 --- a/ui/app/components/allocation-row.js +++ b/ui/app/components/allocation-row.js @@ -24,7 +24,8 @@ export default Component.extend({ stats: null, statsError: false, - enablePolling: computed(() => !Ember.testing), + // enablePolling: computed(() => !Ember.testing), + enablePolling: false, onClick() {}, @@ -59,12 +60,11 @@ export default Component.extend({ const allocation = this.get('allocation'); if (allocation) { - this.get('fetchStats').perform(allocation); + run.scheduleOnce('afterRender', this, qualifyAllocation); } else { this.get('fetchStats').cancelAll(); this.set('stats', null); } - run.scheduleOnce('afterRender', this, qualifyJob); }, fetchStats: task(function*(allocation) { @@ -84,6 +84,14 @@ export default Component.extend({ }).drop(), }); +function qualifyAllocation() { + const allocation = this.get('allocation'); + return allocation.reload().then(() => { + this.get('fetchStats').perform(allocation); + run.scheduleOnce('afterRender', this, qualifyJob); + }); +} + function qualifyJob() { const allocation = this.get('allocation'); if (allocation.get('originalJobId')) { From 47a81e2da7db37c4df6498e7748b31272ead36ba Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 2 May 2018 14:01:34 -0700 Subject: [PATCH 09/16] Add rescheduled icon to allocation row --- ui/app/templates/clients/client.hbs | 1 + ui/app/templates/components/allocation-row.hbs | 7 +++++++ .../components/job-deployment/deployment-allocations.hbs | 1 + ui/app/templates/jobs/job/task-group.hbs | 1 + 4 files changed, 10 insertions(+) diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 1327627100b..b9ec625aa61 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -43,6 +43,7 @@ sortDescending=sortDescending class="with-foot" as |t|}} {{#t.head}} + {{#t.sort-by prop="shortId"}}ID{{/t.sort-by}} {{#t.sort-by prop="modifyIndex" title="Modify Index"}}Modified{{/t.sort-by}} {{#t.sort-by prop="name"}}Name{{/t.sort-by}} diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index ce781d1aa2d..b2f7e1103b8 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -1,3 +1,10 @@ + + {{#if allocation.nextAllocation}} + + {{x-icon "history" class="is-faded"}} + + {{/if}} + {{#link-to "allocations.allocation" allocation class="is-primary"}} {{allocation.shortId}} diff --git a/ui/app/templates/components/job-deployment/deployment-allocations.hbs b/ui/app/templates/components/job-deployment/deployment-allocations.hbs index f42afdb839a..24242723879 100644 --- a/ui/app/templates/components/job-deployment/deployment-allocations.hbs +++ b/ui/app/templates/components/job-deployment/deployment-allocations.hbs @@ -7,6 +7,7 @@ source=deployment.allocations class="allocations" as |t|}} {{#t.head}} + ID Modified Name diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index f1ab0acb2d7..afebdd79677 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -73,6 +73,7 @@ sortDescending=sortDescending class="with-foot" as |t|}} {{#t.head}} + {{#t.sort-by prop="shortId"}}ID{{/t.sort-by}} {{#t.sort-by prop="modifyIndex" title="Modify Index"}}Modified{{/t.sort-by}} {{#t.sort-by prop="name"}}Name{{/t.sort-by}} From 86b725c27797ea8df0b5346c9fb12a309809550c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 2 May 2018 14:02:20 -0700 Subject: [PATCH 10/16] New reschedule-event-row component Intended to be used in a timeline view. --- ui/app/components/reschedule-event-row.js | 20 +++++++++++ .../components/reschedule-event-row.hbs | 33 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 ui/app/components/reschedule-event-row.js create mode 100644 ui/app/templates/components/reschedule-event-row.hbs diff --git a/ui/app/components/reschedule-event-row.js b/ui/app/components/reschedule-event-row.js new file mode 100644 index 00000000000..02fefc3b1da --- /dev/null +++ b/ui/app/components/reschedule-event-row.js @@ -0,0 +1,20 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + store: service(), + tagName: '', + + // When given a string, the component will fetch the allocation + allocationId: null, + + // An allocation can also be provided directly + allocation: computed('allocationId', function() { + return this.get('store').findRecord('allocation', this.get('allocationId')); + }), + + time: null, + linkToAllocation: true, + label: '', +}); diff --git a/ui/app/templates/components/reschedule-event-row.hbs b/ui/app/templates/components/reschedule-event-row.hbs new file mode 100644 index 00000000000..a0793b598e9 --- /dev/null +++ b/ui/app/templates/components/reschedule-event-row.hbs @@ -0,0 +1,33 @@ +
  • + {{#if label}} + {{label}} + {{/if}} + {{moment-format time "MMMM D, YYYY HH:mm:ss"}} +
  • +
  • +
    +
    + + {{allocation.clientStatus}} + + + Allocation + {{#if linkToAllocation}} + {{#link-to "allocations.allocation" allocation.id}} + {{allocation.shortId}} + {{/link-to}} + {{else}} + {{allocation.shortId}} + {{/if}} + + + Client + + {{#link-to "clients.client" allocation.node.id}} + {{allocation.node.id}} + {{/link-to}} + + +
    +
    +
  • From 52c58068193027aef01a862ee1cb730773d8a612 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 3 May 2018 12:17:06 -0700 Subject: [PATCH 11/16] Add WaitUntil and followUpEvaluation --- ui/app/models/allocation.js | 13 +++++++++++++ ui/app/models/evaluation.js | 2 ++ ui/app/serializers/allocation.js | 2 -- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 6e8952808b5..ffe55cff25f 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -79,4 +79,17 @@ export default Model.extend({ hasRescheduleEvents: computed('rescheduleEvents.length', 'nextAllocation', function() { return this.get('rescheduleEvents.length') > 0 || this.get('nextAllocation'); }), + + hasStoppedRescheduling: computed( + 'nextAllocation', + 'clientStatus', + 'followUpEvaluation', + function() { + return ( + !this.get('nextAllocation') && + !this.get('followUpEvaluation') && + this.get('clientStatus') === 'failed' + ); + } + ), }); diff --git a/ui/app/models/evaluation.js b/ui/app/models/evaluation.js index f7755315cfb..3cd0f606424 100644 --- a/ui/app/models/evaluation.js +++ b/ui/app/models/evaluation.js @@ -23,4 +23,6 @@ export default Model.extend({ job: belongsTo('job'), modifyIndex: attr('number'), + + waitUntil: attr('date'), }); diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 4bfef2c3c04..84825bfdd18 100644 --- a/ui/app/serializers/allocation.js +++ b/ui/app/serializers/allocation.js @@ -44,8 +44,6 @@ export default ApplicationSerializer.extend({ hash.NextAllocationID = hash.NextAllocation ? hash.NextAllocation : null; hash.FollowUpEvaluationID = hash.FollowupEvalID ? hash.FollowupEvalID : null; - console.log(hash.FollowUpEvaluationID); - return this._super(typeHash, hash); }, }); From 5952cafccbb1320d96090cc48924e5f55a6c2b85 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 3 May 2018 12:31:25 -0700 Subject: [PATCH 12/16] Reschedule events timeline for the alloc detail page --- ui/app/models/allocation.js | 4 +- .../allocations/allocation/index.hbs | 40 +++++++++++++ .../components/reschedule-event-row.hbs | 60 +++++++++++-------- ui/public/images/icons/clock.svg | 3 + 4 files changed, 80 insertions(+), 27 deletions(-) create mode 100644 ui/public/images/icons/clock.svg diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index ffe55cff25f..048e31e5511 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -86,8 +86,8 @@ export default Model.extend({ 'followUpEvaluation', function() { return ( - !this.get('nextAllocation') && - !this.get('followUpEvaluation') && + !this.get('nextAllocation.content') && + !this.get('followUpEvaluation.content') && this.get('clientStatus') === 'failed' ); } diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 8356eefa74d..c26fb9a2318 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -91,5 +91,45 @@ {{/list-table}} + + {{#if model.hasRescheduleEvents}} +
    +
    + Reschedule Events +
    +
    +
      + {{#if model.nextAllocation}} + {{reschedule-event-row + label="Next Allocation" + allocation=model.nextAllocation + time=model.nextAllocation.modifyTime}} + {{/if}} + {{#if model.hasStoppedRescheduling}} +
    1. + {{x-icon "warning" class="is-warning is-text"}} Nomad has stopped attempting to reschedule this allocation. +
    2. + {{/if}} + {{#if (and model.followUpEvaluation.waitUntil (not model.nextAllocation))}} +
    3. + {{x-icon "clock" class="is-info is-text"}} Nomad will attempt to reschedule + {{moment-from-now model.followUpEvaluation.waitUntil interval=1000}} +
    4. + {{/if}} + {{reschedule-event-row + label="This Allocation" + allocation=model + linkToAllocation=false + time=model.modifyTime}} + + {{#each (reverse model.rescheduleEvents) as |event|}} + {{reschedule-event-row + allocationId=event.previousAllocationId + time=event.time}} + {{/each}} +
    +
    +
    + {{/if}} {{/gutter-menu}} diff --git a/ui/app/templates/components/reschedule-event-row.hbs b/ui/app/templates/components/reschedule-event-row.hbs index a0793b598e9..a494ee3cdf4 100644 --- a/ui/app/templates/components/reschedule-event-row.hbs +++ b/ui/app/templates/components/reschedule-event-row.hbs @@ -1,33 +1,43 @@
  • - {{#if label}} - {{label}} - {{/if}} {{moment-format time "MMMM D, YYYY HH:mm:ss"}}
  • -
    - - {{allocation.clientStatus}} - - - Allocation - {{#if linkToAllocation}} - {{#link-to "allocations.allocation" allocation.id}} - {{allocation.shortId}} - {{/link-to}} - {{else}} - {{allocation.shortId}} - {{/if}} - - - Client - - {{#link-to "clients.client" allocation.node.id}} - {{allocation.node.id}} - {{/link-to}} - - + {{#unless linkToAllocation}} +
    + This Allocation +
    + {{/unless}} +
    +
    +
    + + {{allocation.clientStatus}} + +
    +
    +
    + + Allocation + {{#if linkToAllocation}} + {{#link-to "allocations.allocation" allocation.id}} + {{allocation.shortId}} + {{/link-to}} + {{else}} + {{allocation.shortId}} + {{/if}} + + + Client + + {{#link-to "clients.client" allocation.node.id}} + {{allocation.node.id}} + {{/link-to}} + + +
    +
    +
  • diff --git a/ui/public/images/icons/clock.svg b/ui/public/images/icons/clock.svg new file mode 100644 index 00000000000..d7f8a6dd490 --- /dev/null +++ b/ui/public/images/icons/clock.svg @@ -0,0 +1,3 @@ + + + From 05d095f12982a3f4e698fffda977f314e560b793 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 May 2018 13:08:30 -0700 Subject: [PATCH 13/16] Mirage modeling for rescheduling --- ui/mirage/factories/allocation.js | 60 +++++++++++++++++++++++++++++++ ui/mirage/factories/evaluation.js | 2 ++ ui/mirage/factories/job.js | 4 +++ ui/mirage/factories/task-group.js | 18 ++++++++-- 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index 0a74eb8bebd..ca12f9b5bcc 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import moment from 'moment'; import { Factory, faker, trait } from 'ember-cli-mirage'; import { provide, pickOne } from '../utils'; @@ -56,7 +57,66 @@ export default Factory.extend({ }, }), + rescheduleAttempts: 0, + rescheduleSuccess: false, + + rescheduled: trait({ + // Create another allocation carrying the events of this as well as the reschduleSuccess state. + // Pass along rescheduleAttempts after decrementing. + // After rescheduleAttempts hits zero, a final allocation is made with no nextAllocation and + // a clientStatus of failed or running, depending on rescheduleSuccess + afterCreate(allocation, server) { + console.log('After Create --> rescheduled'); + const attempts = allocation.rescheduleAttempts; + const previousEvents = + (allocation.rescheduleTracker && allocation.rescheduleTracker.Events) || []; + + let rescheduleTime; + if (previousEvents.length) { + const lastEvent = previousEvents[previousEvents.length - 1]; + rescheduleTime = moment(lastEvent.RescheduleTime / 1000000).add(5, 'minutes'); + } else { + rescheduleTime = faker.date.past(2 / 365, REF_TIME); + } + + rescheduleTime *= 1000000; + + const rescheduleTracker = { + Events: previousEvents.concat([ + { + PrevAllocID: allocation.id, + PrevNodeID: null, //allocation.node.id, + RescheduleTime: rescheduleTime, + }, + ]), + }; + + let nextAllocation; + if (attempts) { + nextAllocation = server.create('allocation', 'rescheduled', { + rescheduleAttempts: Math.max(attempts - 1, 0), + rescheduleSuccess: allocation.rescheduleSuccess, + previousAllocation: allocation.id, + clientStatus: 'failed', + rescheduleTracker, + followupEvalId: server.create('evaluation', { + waitUntil: rescheduleTime, + }).id, + }); + } else { + nextAllocation = server.create('allocation', { + previousAllocation: allocation.id, + clientStatus: allocation.rescheduleSuccess ? 'running' : 'failed', + rescheduleTracker, + }); + } + + allocation.update({ nextAllocation: nextAllocation.id, clientStatus: 'failed' }); + }, + }), + afterCreate(allocation, server) { + console.log('After Create'); Ember.assert( '[Mirage] No jobs! make sure jobs are created before allocations', server.db.jobs.length diff --git a/ui/mirage/factories/evaluation.js b/ui/mirage/factories/evaluation.js index d31fcb391fd..ebf01e21755 100644 --- a/ui/mirage/factories/evaluation.js +++ b/ui/mirage/factories/evaluation.js @@ -54,6 +54,8 @@ export default Factory.extend({ modifyIndex: () => faker.random.number({ min: 10, max: 2000 }), + waitUntil: null, + withPlacementFailures: trait({ status: 'blocked', afterCreate(evaluation, server) { diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index e8dc4b4b25e..3323aa19a17 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -86,6 +86,9 @@ export default Factory.extend({ // When true, no evaluations have failed placements noFailedPlacements: false, + // When true, allocations for this job will fail and reschedule, randomly succeeding or not + withRescheduling: false, + afterCreate(job, server) { if (!job.namespaceId) { const namespace = server.db.namespaces.length ? pickOne(server.db.namespaces).id : null; @@ -102,6 +105,7 @@ export default Factory.extend({ const groups = server.createList('task-group', job.groupsCount, { job, createAllocations: job.createAllocations, + withRescheduling: job.withRescheduling, }); job.update({ diff --git a/ui/mirage/factories/task-group.js b/ui/mirage/factories/task-group.js index 682da6ee4a8..e3ef58e0abb 100644 --- a/ui/mirage/factories/task-group.js +++ b/ui/mirage/factories/task-group.js @@ -16,6 +16,10 @@ export default Factory.extend({ // created. createAllocations: true, + // Directived used to control whether or not the allocation should fail + // and reschedule, creating reschedule events. + withRescheduling: false, + afterCreate(group, server) { const tasks = server.createList('task', group.count, { taskGroup: group, @@ -30,12 +34,22 @@ export default Factory.extend({ Array(group.count) .fill(null) .forEach((_, i) => { - server.create('allocation', { + const props = { jobId: group.job.id, namespace: group.job.namespace, taskGroup: group.name, name: `${group.name}.[${i}]`, - }); + rescheduleSuccess: group.withRescheduling ? faker.random.boolean() : null, + rescheduleAttempts: group.withRescheduling + ? faker.random.number({ min: 1, max: 5 }) + : 0, + }; + + if (group.withRescheduling) { + server.create('allocation', 'rescheduled', props); + } else { + server.create('allocation', props); + } }); } }, From 80c5c16f791a29028c752fb3303b34203b39f98d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 May 2018 14:31:04 -0700 Subject: [PATCH 14/16] Acceptance tests for allocation rescheduling --- .../allocations/allocation/index.hbs | 2 +- ui/app/templates/jobs/job/task-group.hbs | 2 +- ui/mirage/factories/allocation.js | 2 -- ui/tests/acceptance/allocation-detail-test.js | 20 +++++++++++++++++++ ui/tests/acceptance/task-group-detail-test.js | 19 ++++++++++++++++++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index c26fb9a2318..a4e885fd0ad 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -93,7 +93,7 @@ {{#if model.hasRescheduleEvents}} -
    +
    Reschedule Events
    diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index afebdd79677..702168f2275 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -84,7 +84,7 @@ Memory {{/t.head}} {{#t.body as |row|}} - {{allocation-row data-test-allocation allocation=row.model context="job" onClick=(action "gotoAllocation" row.model)}} + {{allocation-row data-test-allocation=row.model.id allocation=row.model context="job" onClick=(action "gotoAllocation" row.model)}} {{/t.body}} {{/list-table}}
    diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index ca12f9b5bcc..11bdff4d7bb 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -66,7 +66,6 @@ export default Factory.extend({ // After rescheduleAttempts hits zero, a final allocation is made with no nextAllocation and // a clientStatus of failed or running, depending on rescheduleSuccess afterCreate(allocation, server) { - console.log('After Create --> rescheduled'); const attempts = allocation.rescheduleAttempts; const previousEvents = (allocation.rescheduleTracker && allocation.rescheduleTracker.Events) || []; @@ -116,7 +115,6 @@ export default Factory.extend({ }), afterCreate(allocation, server) { - console.log('After Create'); Ember.assert( '[Mirage] No jobs! make sure jobs are created before allocations', server.db.jobs.length diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index dfbdc747eb3..6218946b4d2 100644 --- a/ui/tests/acceptance/allocation-detail-test.js +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -121,6 +121,10 @@ test('each task row should list high-level information for the task', function(a }); }); +test('when the allocation has not been rescheduled, the reschedule events section is not rendered', function(assert) { + assert.notOk(find('[data-test-reschedule-events]'), 'Reschedule Events section exists'); +}); + test('when the allocation is not found, an error message is shown, but the URL persists', function(assert) { visit('/allocations/not-a-real-allocation'); @@ -185,3 +189,19 @@ test('when the node the allocation is on has yet to load, address links are in a }); }); }); + +moduleForAcceptance('Acceptance | allocation detail (rescheduled)', { + beforeEach() { + server.create('agent'); + + node = server.create('node'); + job = server.create('job', { createAllocations: false }); + allocation = server.create('allocation', 'rescheduled'); + + visit(`/allocations/${allocation.id}`); + }, +}); + +test('when the allocation has been rescheduled, the reschedule events section is rendered', function(assert) { + assert.ok(find('[data-test-reschedule-events]'), 'Reschedule Events section exists'); +}); diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js index d6b2935730c..81bcf464ae0 100644 --- a/ui/tests/acceptance/task-group-detail-test.js +++ b/ui/tests/acceptance/task-group-detail-test.js @@ -45,6 +45,14 @@ moduleForAcceptance('Acceptance | task group detail', { alloc.name = 'aaaaa'; }); + // Mark the first alloc as rescheduled + allocations[0].update({ + nextAllocation: allocations[1].id, + }); + allocations[1].update({ + previousAllocation: allocations[0].id, + }); + visit(`/jobs/${job.id}/${taskGroup.name}`); }, }); @@ -223,3 +231,14 @@ test('when the allocation search has no matches, there is an empty message', fun assert.equal(find('[data-test-empty-allocations-list-headline]').textContent, 'No Matches'); }); }); + +test('when the allocation has reschedule events, the allocation row is denoted with an icon', function(assert) { + const rescheduleRow = find(`[data-test-allocation="${allocations[0].id}"]`); + const normalRow = find(`[data-test-allocation="${allocations[1].id}"]`); + + assert.ok( + rescheduleRow.querySelector('[data-test-indicators] .icon'), + 'Reschedule row has an icon' + ); + assert.notOk(normalRow.querySelector('[data-test-indicators] .icon'), 'Normal row has no icon'); +}); From 658c8426ca873c96b95be67550c001b29a20fd59 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 May 2018 15:03:00 -0700 Subject: [PATCH 15/16] Turn reschedule events timeline into a component for isolated testing --- .../allocations/allocation/index.hbs | 31 +------------------ .../components/reschedule-event-timeline.hbs | 30 ++++++++++++++++++ 2 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 ui/app/templates/components/reschedule-event-timeline.hbs diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index a4e885fd0ad..1aa22ab0a69 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -98,36 +98,7 @@ Reschedule Events
    -
      - {{#if model.nextAllocation}} - {{reschedule-event-row - label="Next Allocation" - allocation=model.nextAllocation - time=model.nextAllocation.modifyTime}} - {{/if}} - {{#if model.hasStoppedRescheduling}} -
    1. - {{x-icon "warning" class="is-warning is-text"}} Nomad has stopped attempting to reschedule this allocation. -
    2. - {{/if}} - {{#if (and model.followUpEvaluation.waitUntil (not model.nextAllocation))}} -
    3. - {{x-icon "clock" class="is-info is-text"}} Nomad will attempt to reschedule - {{moment-from-now model.followUpEvaluation.waitUntil interval=1000}} -
    4. - {{/if}} - {{reschedule-event-row - label="This Allocation" - allocation=model - linkToAllocation=false - time=model.modifyTime}} - - {{#each (reverse model.rescheduleEvents) as |event|}} - {{reschedule-event-row - allocationId=event.previousAllocationId - time=event.time}} - {{/each}} -
    + {{reschedule-event-timeline allocation=model}}
    {{/if}} diff --git a/ui/app/templates/components/reschedule-event-timeline.hbs b/ui/app/templates/components/reschedule-event-timeline.hbs new file mode 100644 index 00000000000..8a75433c214 --- /dev/null +++ b/ui/app/templates/components/reschedule-event-timeline.hbs @@ -0,0 +1,30 @@ +
      + {{#if allocation.nextAllocation}} + {{reschedule-event-row + label="Next Allocation" + allocation=allocation.nextAllocation + time=allocation.nextAllocation.modifyTime}} + {{/if}} + {{#if allocation.hasStoppedRescheduling}} +
    1. + {{x-icon "warning" class="is-warning is-text"}} Nomad has stopped attempting to reschedule this allocation. +
    2. + {{/if}} + {{#if (and allocation.followUpEvaluation.waitUntil (not allocation.nextAllocation))}} +
    3. + {{x-icon "clock" class="is-info is-text"}} Nomad will attempt to reschedule + {{moment-from-now allocation.followUpEvaluation.waitUntil interval=1000}} +
    4. + {{/if}} + {{reschedule-event-row + label="This Allocation" + allocation=allocation + linkToAllocation=false + time=allocation.modifyTime}} + + {{#each (reverse allocation.rescheduleEvents) as |event|}} + {{reschedule-event-row + allocationId=event.previousAllocationId + time=event.time}} + {{/each}} +
    From 94df7bc46aaa354519aeb8bc6535087ec2ed2105 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 4 May 2018 19:04:36 -0700 Subject: [PATCH 16/16] Integration tests for the various reschedule events timeline permutations --- ui/app/components/allocation-row.js | 3 +- .../components/reschedule-event-row.hbs | 15 +- .../components/reschedule-event-timeline.hbs | 5 +- ui/mirage/factories/allocation.js | 6 +- .../reschedule-event-timeline-test.js | 198 ++++++++++++++++++ 5 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 ui/tests/integration/reschedule-event-timeline-test.js diff --git a/ui/app/components/allocation-row.js b/ui/app/components/allocation-row.js index dab83046813..5a0e92f39ad 100644 --- a/ui/app/components/allocation-row.js +++ b/ui/app/components/allocation-row.js @@ -24,8 +24,7 @@ export default Component.extend({ stats: null, statsError: false, - // enablePolling: computed(() => !Ember.testing), - enablePolling: false, + enablePolling: computed(() => !Ember.testing), onClick() {}, diff --git a/ui/app/templates/components/reschedule-event-row.hbs b/ui/app/templates/components/reschedule-event-row.hbs index a494ee3cdf4..733358b3db4 100644 --- a/ui/app/templates/components/reschedule-event-row.hbs +++ b/ui/app/templates/components/reschedule-event-row.hbs @@ -1,17 +1,20 @@
  • + {{#if label}} + {{label}} + {{/if}} {{moment-format time "MMMM D, YYYY HH:mm:ss"}}
  • -
  • +
  • {{#unless linkToAllocation}} -
    +
    This Allocation
    {{/unless}}
    - + {{allocation.clientStatus}}
    @@ -21,16 +24,16 @@ Allocation {{#if linkToAllocation}} {{#link-to "allocations.allocation" allocation.id}} - {{allocation.shortId}} + {{allocation.shortId}} {{/link-to}} {{else}} - {{allocation.shortId}} + {{allocation.shortId}} {{/if}} Client - {{#link-to "clients.client" allocation.node.id}} + {{#link-to "clients.client" data-test-node-link allocation.node.id}} {{allocation.node.id}} {{/link-to}} diff --git a/ui/app/templates/components/reschedule-event-timeline.hbs b/ui/app/templates/components/reschedule-event-timeline.hbs index 8a75433c214..1f2f5385c66 100644 --- a/ui/app/templates/components/reschedule-event-timeline.hbs +++ b/ui/app/templates/components/reschedule-event-timeline.hbs @@ -6,18 +6,17 @@ time=allocation.nextAllocation.modifyTime}} {{/if}} {{#if allocation.hasStoppedRescheduling}} -
  • +
  • {{x-icon "warning" class="is-warning is-text"}} Nomad has stopped attempting to reschedule this allocation.
  • {{/if}} {{#if (and allocation.followUpEvaluation.waitUntil (not allocation.nextAllocation))}} -
  • +
  • {{x-icon "clock" class="is-info is-text"}} Nomad will attempt to reschedule {{moment-from-now allocation.followUpEvaluation.waitUntil interval=1000}}
  • {{/if}} {{reschedule-event-row - label="This Allocation" allocation=allocation linkToAllocation=false time=allocation.modifyTime}} diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index 11bdff4d7bb..6c1c2039310 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -66,7 +66,7 @@ export default Factory.extend({ // After rescheduleAttempts hits zero, a final allocation is made with no nextAllocation and // a clientStatus of failed or running, depending on rescheduleSuccess afterCreate(allocation, server) { - const attempts = allocation.rescheduleAttempts; + const attempts = allocation.rescheduleAttempts - 1; const previousEvents = (allocation.rescheduleTracker && allocation.rescheduleTracker.Events) || []; @@ -91,9 +91,9 @@ export default Factory.extend({ }; let nextAllocation; - if (attempts) { + if (attempts > 0) { nextAllocation = server.create('allocation', 'rescheduled', { - rescheduleAttempts: Math.max(attempts - 1, 0), + rescheduleAttempts: Math.max(attempts, 0), rescheduleSuccess: allocation.rescheduleSuccess, previousAllocation: allocation.id, clientStatus: 'failed', diff --git a/ui/tests/integration/reschedule-event-timeline-test.js b/ui/tests/integration/reschedule-event-timeline-test.js new file mode 100644 index 00000000000..4bed776a052 --- /dev/null +++ b/ui/tests/integration/reschedule-event-timeline-test.js @@ -0,0 +1,198 @@ +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import { find, findAll } from 'ember-native-dom-helpers'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import moment from 'moment'; + +moduleForComponent( + 'reschedule-event-timeline', + 'Integration | Component | reschedule event timeline', + { + integration: true, + beforeEach() { + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + this.server.create('node'); + this.server.create('job', { createAllocations: false }); + }, + afterEach() { + this.server.shutdown(); + }, + } +); + +const commonTemplate = hbs` + {{reschedule-event-timeline allocation=allocation}} +`; + +test('when the allocation is running, the timeline shows past allocations', function(assert) { + const attempts = 2; + + this.server.create('allocation', 'rescheduled', { + rescheduleAttempts: attempts, + rescheduleSuccess: true, + }); + + this.store.findAll('allocation'); + let allocation; + + return wait() + .then(() => { + allocation = this.store + .peekAll('allocation') + .find(alloc => !alloc.get('nextAllocation.content')); + + this.set('allocation', allocation); + this.render(commonTemplate); + + return wait(); + }) + .then(() => { + assert.equal( + findAll('[data-test-allocation]').length, + attempts + 1, + 'Total allocations equals current allocation plus all past allocations' + ); + assert.equal( + find('[data-test-allocation]'), + find(`[data-test-allocation="${allocation.id}"]`), + 'First allocation is the current allocation' + ); + + assert.notOk(find('[data-test-stop-warning]'), 'No stop warning'); + assert.notOk(find('[data-test-attempt-notice]'), 'No attempt notice'); + + assert.equal( + find( + `[data-test-allocation="${allocation.id}"] [data-test-allocation-link]` + ).textContent.trim(), + allocation.get('shortId'), + 'The "this" allocation is correct' + ); + assert.equal( + find( + `[data-test-allocation="${allocation.id}"] [data-test-allocation-status]` + ).textContent.trim(), + allocation.get('clientStatus'), + 'Allocation shows the status' + ); + }); +}); + +test('when the allocation has failed and there is a follow up evaluation, a note with a time is shown', function(assert) { + const attempts = 2; + + this.server.create('allocation', 'rescheduled', { + rescheduleAttempts: attempts, + rescheduleSuccess: false, + }); + + this.store.findAll('allocation'); + let allocation; + + return wait() + .then(() => { + allocation = this.store + .peekAll('allocation') + .find(alloc => !alloc.get('nextAllocation.content')); + + this.set('allocation', allocation); + this.render(commonTemplate); + + return wait(); + }) + .then(() => { + assert.ok( + find('[data-test-stop-warning]'), + 'Stop warning is shown since the last allocation failed' + ); + assert.notOk(find('[data-test-attempt-notice]'), 'Reschdule attempt notice is not shown'); + }); +}); + +test('when the allocation has failed and there is no follow up evaluation, a warning is shown', function(assert) { + const attempts = 2; + + this.server.create('allocation', 'rescheduled', { + rescheduleAttempts: attempts, + rescheduleSuccess: false, + }); + + const lastAllocation = server.schema.allocations.findBy({ nextAllocation: undefined }); + lastAllocation.update({ + followupEvalId: server.create('evaluation', { + waitUntil: moment() + .add(2, 'hours') + .toDate(), + }).id, + }); + + this.store.findAll('allocation'); + let allocation; + + return wait() + .then(() => { + allocation = this.store + .peekAll('allocation') + .find(alloc => !alloc.get('nextAllocation.content')); + + this.set('allocation', allocation); + this.render(commonTemplate); + + return wait(); + }) + .then(() => { + assert.ok( + find('[data-test-attempt-notice]'), + 'Reschedule notice is shown since the follow up eval says so' + ); + assert.notOk(find('[data-test-stop-warning]'), 'Stop warning is not shown'); + }); +}); + +test('when the allocation has a next allocation already, it is shown in the timeline', function(assert) { + const attempts = 2; + + const originalAllocation = this.server.create('allocation', 'rescheduled', { + rescheduleAttempts: attempts, + rescheduleSuccess: true, + }); + + this.store.findAll('allocation'); + let allocation; + + return wait() + .then(() => { + allocation = this.store.peekAll('allocation').findBy('id', originalAllocation.id); + + this.set('allocation', allocation); + this.render(commonTemplate); + + return wait(); + }) + .then(() => { + assert.ok( + find('[data-test-reschedule-label]').textContent.trim(), + 'Next Allocation', + 'The first allocation is the next allocation and labeled as such' + ); + + assert.equal( + find('[data-test-allocation] [data-test-allocation-link]').textContent.trim(), + allocation.get('nextAllocation.shortId'), + 'The next allocation item is for the correct allocation' + ); + + assert.equal( + findAll('[data-test-allocation]')[1], + find(`[data-test-allocation="${allocation.id}"]`), + 'Second allocation is the current allocation' + ); + + assert.notOk(find('[data-test-stop-warning]'), 'No stop warning'); + assert.notOk(find('[data-test-attempt-notice]'), 'No attempt notice'); + }); +});