diff --git a/ui/app/components/allocation-row.js b/ui/app/components/allocation-row.js index d6236d6a283..5a0e92f39ad 100644 --- a/ui/app/components/allocation-row.js +++ b/ui/app/components/allocation-row.js @@ -59,12 +59,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 +83,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')) { 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/models/allocation.js b/ui/app/models/allocation.js index 6d85ae85f57..048e31e5511 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -37,6 +37,13 @@ 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' }), + + followUpEvaluation: belongsTo('evaluation'), + statusClass: computed('clientStatus', function() { const classMap = { pending: 'is-pending', @@ -67,4 +74,22 @@ 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'); + }), + + hasStoppedRescheduling: computed( + 'nextAllocation', + 'clientStatus', + 'followUpEvaluation', + function() { + return ( + !this.get('nextAllocation.content') && + !this.get('followUpEvaluation.content') && + 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/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..911fe789882 --- /dev/null +++ b/ui/app/models/reschedule-event.js @@ -0,0 +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'), + time: attr('date'), + delay: attr('string'), + + previousAllocationShortId: shortUUIDProperty('previousAllocationId'), + previousNodeShortId: shortUUIDProperty('previousNodeShortId'), +}); diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 8a7a5d43baf..84825bfdd18 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; + + // 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; + 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..775ddac65a1 --- /dev/null +++ b/ui/app/serializers/reschedule-event.js @@ -0,0 +1,17 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + 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); + + hash.PreviousAllocationId = hash.PrevAllocID ? hash.PrevAllocID : null; + hash.PreviousNodeId = hash.PrevNodeID ? hash.PrevNodeID : null; + + return this._super(typeHash, hash); + }, +}); 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; 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); 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; diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 8356eefa74d..1aa22ab0a69 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -91,5 +91,16 @@ {{/list-table}} + + {{#if model.hasRescheduleEvents}} +
{{allocation.shortId}}
+ {{/link-to}}
+ {{else}}
+ {{allocation.shortId}}
+ {{/if}}
+
+
+ Client
+
+ {{#link-to "clients.client" data-test-node-link allocation.node.id}}
+ {{allocation.node.id}}
+ {{/link-to}}
+
+
+