diff --git a/ui/app/components/lifecycle-chart-row.js b/ui/app/components/lifecycle-chart-row.js
index 44985d747..5a2422cb9 100644
--- a/ui/app/components/lifecycle-chart-row.js
+++ b/ui/app/components/lifecycle-chart-row.js
@@ -23,4 +23,17 @@ export default class LifecycleChartRow extends Component {
return undefined;
}
+
+ @computed('task.lifecycleName')
+ get lifecycleLabel() {
+ const name = this.task.lifecycleName;
+
+ if (name.includes('sidecar')) {
+ return 'sidecar';
+ } else if (name.includes('ephemeral')) {
+ return name.substr(0, name.indexOf('-'));
+ } else {
+ return name;
+ }
+ }
}
diff --git a/ui/app/components/lifecycle-chart.js b/ui/app/components/lifecycle-chart.js
index d2e1735ea..a58e6b839 100644
--- a/ui/app/components/lifecycle-chart.js
+++ b/ui/app/components/lifecycle-chart.js
@@ -14,8 +14,11 @@ export default class LifecycleChart extends Component {
get lifecyclePhases() {
const tasksOrStates = this.taskStates || this.tasks;
const lifecycles = {
- prestarts: [],
- sidecars: [],
+ 'prestart-ephemerals': [],
+ 'prestart-sidecars': [],
+ 'poststart-ephemerals': [],
+ 'poststart-sidecars': [],
+ poststops: [],
mains: [],
};
@@ -25,18 +28,29 @@ export default class LifecycleChart extends Component {
});
const phases = [];
+ const stateActiveIterator = state => state.state === 'running';
- if (lifecycles.prestarts.length || lifecycles.sidecars.length) {
+ if (lifecycles.mains.length < tasksOrStates.length) {
phases.push({
name: 'Prestart',
- isActive: lifecycles.prestarts.some(state => state.state === 'running'),
+ isActive: lifecycles['prestart-ephemerals'].some(stateActiveIterator),
});
- }
- if (lifecycles.sidecars.length || lifecycles.mains.length) {
phases.push({
name: 'Main',
- isActive: lifecycles.mains.some(state => state.state === 'running'),
+ isActive:
+ lifecycles.mains.some(stateActiveIterator) ||
+ lifecycles['poststart-ephemerals'].some(stateActiveIterator),
+ });
+
+ // Poststart is rendered as a subphase of main and therefore has no independent active state
+ phases.push({
+ name: 'Poststart',
+ });
+
+ phases.push({
+ name: 'Poststop',
+ isActive: lifecycles.poststops.some(stateActiveIterator),
});
}
@@ -55,12 +69,14 @@ export default class LifecycleChart extends Component {
}
const lifecycleNameSortPrefix = {
- prestart: 0,
- sidecar: 1,
+ 'prestart-ephemeral': 0,
+ 'prestart-sidecar': 1,
main: 2,
+ 'poststart-sidecar': 3,
+ 'poststart-ephemeral': 4,
+ poststop: 5,
};
function getTaskSortPrefix(task) {
- // Prestarts first, then sidecars, then mains
return `${lifecycleNameSortPrefix[task.lifecycleName]}-${task.name}`;
}
diff --git a/ui/app/controllers/allocations/allocation/task/index.js b/ui/app/controllers/allocations/allocation/task/index.js
index 6af893dff..57e661d1f 100644
--- a/ui/app/controllers/allocations/allocation/task/index.js
+++ b/ui/app/controllers/allocations/allocation/task/index.js
@@ -1,22 +1,10 @@
import Controller from '@ember/controller';
-import { computed } from '@ember/object';
import { computed as overridable } from 'ember-overridable-computed';
import { task } from 'ember-concurrency';
import classic from 'ember-classic-decorator';
@classic
export default class IndexController extends Controller {
- @computed('model.task.taskGroup.tasks.@each.name')
- get otherTaskStates() {
- const taskName = this.model.task.name;
- return this.model.allocation.states.rejectBy('name', taskName);
- }
-
- @computed('otherTaskStates.@each.lifecycle')
- get prestartTaskStates() {
- return this.otherTaskStates.filterBy('task.lifecycle');
- }
-
@overridable(() => {
// { title, description }
return null;
diff --git a/ui/app/models/task.js b/ui/app/models/task.js
index 7c2b95a6f..41722c217 100644
--- a/ui/app/models/task.js
+++ b/ui/app/models/task.js
@@ -14,8 +14,18 @@ export default class Task extends Fragment {
@computed('lifecycle', 'lifecycle.sidecar')
get lifecycleName() {
- if (this.lifecycle && this.lifecycle.sidecar) return 'sidecar';
- if (this.lifecycle && this.lifecycle.hook === 'prestart') return 'prestart';
+ if (this.lifecycle) {
+ const { hook, sidecar } = this.lifecycle;
+
+ if (hook === 'prestart') {
+ return sidecar ? 'prestart-sidecar' : 'prestart-ephemeral';
+ } else if (hook === 'poststart') {
+ return sidecar ? 'poststart-sidecar' : 'poststart-ephemeral';
+ } else if (hook === 'poststop') {
+ return 'poststop';
+ }
+ }
+
return 'main';
}
diff --git a/ui/app/styles/components/lifecycle-chart.scss b/ui/app/styles/components/lifecycle-chart.scss
index 3c009058a..aec38bd91 100644
--- a/ui/app/styles/components/lifecycle-chart.scss
+++ b/ui/app/styles/components/lifecycle-chart.scss
@@ -11,7 +11,6 @@
.divider {
position: absolute;
- left: 25%;
height: 100%;
stroke: $ui-gray-200;
@@ -19,6 +18,14 @@
stroke-dasharray: 1, 7;
stroke-dashoffset: 1;
stroke-linecap: square;
+
+ &.prestart {
+ left: 25%;
+ }
+
+ &.poststop {
+ left: 75%;
+ }
}
}
@@ -52,6 +59,16 @@
&.main {
left: 25%;
+ right: 25%;
+ }
+
+ &.poststart {
+ left: 35%;
+ right: 25%;
+ }
+
+ &.poststop {
+ left: 75%;
right: 0;
}
}
@@ -110,12 +127,34 @@
&.main {
margin-left: 25%;
+ margin-right: 25%;
}
- &.prestart {
+ &.prestart-ephemeral {
margin-right: 75%;
}
+ &.prestart-sidecar {
+ margin-right: 25%;
+ }
+
+ &.poststart-ephemeral,
+ &.poststart-sidecar {
+ margin-left: 35%;
+ }
+
+ &.poststart-sidecar {
+ margin-right: 25%;
+ }
+
+ &.poststart-ephemeral {
+ margin-right: 35%;
+ }
+
+ &.poststop {
+ margin-left: 75%;
+ }
+
&:last-child .task {
margin-bottom: 0.9em;
}
diff --git a/ui/app/templates/allocations/allocation/task/index.hbs b/ui/app/templates/allocations/allocation/task/index.hbs
index 4b36e9147..72dbd3edf 100644
--- a/ui/app/templates/allocations/allocation/task/index.hbs
+++ b/ui/app/templates/allocations/allocation/task/index.hbs
@@ -93,38 +93,6 @@
- {{#if (and (not this.model.task.lifecycle) this.prestartTaskStates)}}
-
{{#each this.lifecyclePhases as |phase|}}
-
diff --git a/ui/mirage/factories/task.js b/ui/mirage/factories/task.js
index 03ac9270b..6f898aaae 100644
--- a/ui/mirage/factories/task.js
+++ b/ui/mirage/factories/task.js
@@ -19,14 +19,20 @@ export default Factory.extend({
Resources: generateResources,
Lifecycle: i => {
- const cycle = i % 3;
+ const cycle = i % 6;
if (cycle === 0) {
return null;
} else if (cycle === 1) {
return { Hook: 'prestart', Sidecar: false };
- } else {
+ } else if (cycle === 2) {
return { Hook: 'prestart', Sidecar: true };
+ } else if (cycle === 3) {
+ return { Hook: 'poststart', Sidecar: false };
+ } else if (cycle === 4) {
+ return { Hook: 'poststart', Sidecar: true };
+ } else if (cycle === 5) {
+ return { Hook: 'poststop' };
}
},
});
diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js
index 3223868e5..67943f525 100644
--- a/ui/tests/acceptance/allocation-detail-test.js
+++ b/ui/tests/acceptance/allocation-detail-test.js
@@ -82,7 +82,7 @@ module('Acceptance | allocation detail', function(hooks) {
test('/allocation/:id should present task lifecycles', async function(assert) {
const job = server.create('job', {
groupsCount: 1,
- groupTaskCount: 3,
+ groupTaskCount: 6,
withGroupServices: true,
createAllocations: false,
});
@@ -92,96 +92,23 @@ module('Acceptance | allocation detail', function(hooks) {
jobId: job.id,
});
- const taskStatePhases = server.db.taskStates.where({ allocationId: allocation.id }).reduce(
- (phases, state) => {
- const lifecycle = server.db.tasks.findBy({ name: state.name }).Lifecycle;
-
- if (lifecycle) {
- if (lifecycle.Sidecar) {
- phases.sidecars.push(state);
- state.lifecycleString = 'Sidecar';
- } else {
- phases.prestarts.push(state);
- state.lifecycleString = 'Prestart';
- }
- } else {
- phases.mains.push(state);
- state.lifecycleString = 'Main';
- }
-
- return phases;
- },
- {
- prestarts: [],
- sidecars: [],
- mains: [],
- }
- );
-
- taskStatePhases.prestarts = taskStatePhases.prestarts.sortBy('name');
- taskStatePhases.sidecars = taskStatePhases.sidecars.sortBy('name');
- taskStatePhases.mains = taskStatePhases.mains.sortBy('name');
-
- const sortedServerStates = taskStatePhases.prestarts.concat(
- taskStatePhases.sidecars,
- taskStatePhases.mains
- );
-
await Allocation.visit({ id: allocation.id });
assert.ok(Allocation.lifecycleChart.isPresent);
assert.equal(Allocation.lifecycleChart.title, 'Task Lifecycle Status');
- assert.equal(Allocation.lifecycleChart.phases.length, 2);
- assert.equal(Allocation.lifecycleChart.tasks.length, sortedServerStates.length);
-
- const stateActiveIterator = state => state.state === 'running';
- const anyPrestartsActive = taskStatePhases.prestarts.some(stateActiveIterator);
-
- if (anyPrestartsActive) {
- assert.ok(Allocation.lifecycleChart.phases[0].isActive);
- } else {
- assert.notOk(Allocation.lifecycleChart.phases[0].isActive);
- }
-
- const anyMainsActive = taskStatePhases.mains.some(stateActiveIterator);
-
- if (anyMainsActive) {
- assert.ok(Allocation.lifecycleChart.phases[1].isActive);
- } else {
- assert.notOk(Allocation.lifecycleChart.phases[1].isActive);
- }
-
- Allocation.lifecycleChart.tasks.forEach((Task, index) => {
- const serverState = sortedServerStates[index];
-
- assert.equal(Task.name, serverState.name);
-
- if (serverState.lifecycleString === 'Sidecar') {
- assert.ok(Task.isSidecar);
- } else if (serverState.lifecycleString === 'Prestart') {
- assert.ok(Task.isPrestart);
- } else {
- assert.ok(Task.isMain);
- }
-
- assert.equal(Task.lifecycle, `${serverState.lifecycleString} Task`);
-
- if (serverState.state === 'running') {
- assert.ok(Task.isActive);
- } else {
- assert.notOk(Task.isActive);
- }
-
- // Task state factory uses invalid dates for tasks that aren’t finished
- if (isNaN(serverState.finishedAt)) {
- assert.notOk(Task.isFinished);
- } else {
- assert.ok(Task.isFinished);
- }
- });
+ assert.equal(Allocation.lifecycleChart.phases.length, 4);
+ assert.equal(Allocation.lifecycleChart.tasks.length, 6);
await Allocation.lifecycleChart.tasks[0].visit();
- assert.equal(currentURL(), `/allocations/${allocation.id}/${sortedServerStates[0].name}`);
+
+ const prestartEphemeralTask = server.db.taskStates
+ .where({ allocationId: allocation.id })
+ .find(taskState => {
+ const task = server.db.tasks.findBy({ name: taskState.name });
+ return task.Lifecycle && task.Lifecycle.Hook === 'prestart' && !task.Lifecycle.Sidecar;
+ });
+
+ assert.equal(currentURL(), `/allocations/${allocation.id}/${prestartEphemeralTask.name}`);
});
test('/allocation/:id should list all tasks for the allocation', async function(assert) {
diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js
index 85d49c5fa..32b9c11cd 100644
--- a/ui/tests/acceptance/task-detail-test.js
+++ b/ui/tests/acceptance/task-detail-test.js
@@ -101,86 +101,6 @@ module('Acceptance | task detail', function(hooks) {
assert.equal(Task.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory');
});
- test('/allocation/:id/:task_name lists related prestart tasks for a main task when they exist', async function(assert) {
- const job = server.create('job', {
- groupsCount: 2,
- groupTaskCount: 3,
- createAllocations: false,
- status: 'running',
- });
-
- job.taskGroups.models.forEach(taskGroup => {
- server.create('allocation', {
- jobId: job.id,
- taskGroup: taskGroup.name,
- forceRunningClientStatus: true,
- });
- });
-
- const taskGroup = job.taskGroups.models[0];
- const [mainTask, sidecarTask, prestartTask] = taskGroup.tasks.models;
-
- mainTask.attrs.Lifecycle = null;
- mainTask.save();
-
- sidecarTask.attrs.Lifecycle = { Sidecar: true, Hook: 'prestart' };
- sidecarTask.save();
-
- prestartTask.attrs.Lifecycle = { Sidecar: false, Hook: 'prestart' };
- prestartTask.save();
-
- taskGroup.save();
-
- const noPrestartTasksTaskGroup = job.taskGroups.models[1];
- noPrestartTasksTaskGroup.tasks.models.forEach(task => {
- task.attrs.Lifecycle = null;
- task.save();
- });
-
- const mainTaskState = server.schema.taskStates.findBy({ name: mainTask.name });
- const sidecarTaskState = server.schema.taskStates.findBy({ name: sidecarTask.name });
- const prestartTaskState = server.schema.taskStates.findBy({ name: prestartTask.name });
-
- prestartTaskState.attrs.state = 'running';
- prestartTaskState.attrs.finishedAt = null;
- prestartTaskState.save();
-
- await Task.visit({ id: mainTaskState.allocationId, name: mainTask.name });
-
- assert.ok(Task.hasPrestartTasks);
- assert.equal(Task.prestartTasks.length, 2);
-
- Task.prestartTasks[0].as(SidecarTask => {
- assert.equal(SidecarTask.name, sidecarTask.name);
- assert.equal(SidecarTask.state, sidecarTaskState.state);
- assert.equal(SidecarTask.lifecycle, 'sidecar');
- assert.notOk(SidecarTask.isBlocking);
- });
-
- Task.prestartTasks[1].as(PrestartTask => {
- assert.equal(PrestartTask.name, prestartTask.name);
- assert.equal(PrestartTask.state, prestartTaskState.state);
- assert.equal(PrestartTask.lifecycle, 'prestart');
- assert.ok(PrestartTask.isBlocking);
- });
-
- await Task.visit({ id: sidecarTaskState.allocationId, name: sidecarTask.name });
-
- assert.notOk(Task.hasPrestartTasks);
-
- const noPrestartTasksTask = noPrestartTasksTaskGroup.tasks.models[0];
- const noPrestartTasksTaskState = server.db.taskStates.findBy({
- name: noPrestartTasksTask.name,
- });
-
- await Task.visit({
- id: noPrestartTasksTaskState.allocationId,
- name: noPrestartTasksTaskState.name,
- });
-
- assert.notOk(Task.hasPrestartTasks);
- });
-
test('the events table lists all recent events', async function(assert) {
const events = server.db.taskEvents.where({ taskStateId: task.id });
diff --git a/ui/tests/integration/components/lifecycle-chart-test.js b/ui/tests/integration/components/lifecycle-chart-test.js
index 3c47d0b96..864f3fcfb 100644
--- a/ui/tests/integration/components/lifecycle-chart-test.js
+++ b/ui/tests/integration/components/lifecycle-chart-test.js
@@ -2,6 +2,7 @@ import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, settled } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
+import { set } from '@ember/object';
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
import { create } from 'ember-cli-page-object';
import LifecycleChart from 'nomad-ui/tests/pages/components/lifecycle-chart';
@@ -11,19 +12,31 @@ const Chart = create(LifecycleChart);
const tasks = [
{
lifecycleName: 'main',
- name: 'main two',
+ name: 'main two: 3',
},
{
lifecycleName: 'main',
- name: 'main one',
+ name: 'main one: 2',
},
{
- lifecycleName: 'prestart',
- name: 'prestart',
+ lifecycleName: 'prestart-ephemeral',
+ name: 'prestart ephemeral: 0',
},
{
- lifecycleName: 'sidecar',
- name: 'sidecar',
+ lifecycleName: 'prestart-sidecar',
+ name: 'prestart sidecar: 1',
+ },
+ {
+ lifecycleName: 'poststart-ephemeral',
+ name: 'poststart ephemeral: 5',
+ },
+ {
+ lifecycleName: 'poststart-sidecar',
+ name: 'poststart sidecar: 4',
+ },
+ {
+ lifecycleName: 'poststop',
+ name: 'poststop: 6',
},
];
@@ -38,20 +51,36 @@ module('Integration | Component | lifecycle-chart', function(hooks) {
assert.equal(Chart.phases[0].name, 'Prestart');
assert.equal(Chart.phases[1].name, 'Main');
+ assert.equal(Chart.phases[2].name, 'Poststart');
+ assert.equal(Chart.phases[3].name, 'Poststop');
Chart.phases.forEach(phase => assert.notOk(phase.isActive));
- assert.deepEqual(Chart.tasks.mapBy('name'), ['prestart', 'sidecar', 'main one', 'main two']);
+ assert.deepEqual(Chart.tasks.mapBy('name'), [
+ 'prestart ephemeral: 0',
+ 'prestart sidecar: 1',
+ 'main one: 2',
+ 'main two: 3',
+ 'poststart sidecar: 4',
+ 'poststart ephemeral: 5',
+ 'poststop: 6',
+ ]);
assert.deepEqual(Chart.tasks.mapBy('lifecycle'), [
'Prestart Task',
'Sidecar Task',
'Main Task',
'Main Task',
+ 'Sidecar Task',
+ 'Poststart Task',
+ 'Poststop Task',
]);
- assert.ok(Chart.tasks[0].isPrestart);
- assert.ok(Chart.tasks[1].isSidecar);
+ assert.ok(Chart.tasks[0].isPrestartEphemeral);
+ assert.ok(Chart.tasks[1].isPrestartSidecar);
assert.ok(Chart.tasks[2].isMain);
+ assert.ok(Chart.tasks[4].isPoststartSidecar);
+ assert.ok(Chart.tasks[5].isPoststartEphemeral);
+ assert.ok(Chart.tasks[6].isPoststop);
Chart.tasks.forEach(task => {
assert.notOk(task.isActive);
@@ -72,6 +101,13 @@ module('Integration | Component | lifecycle-chart', function(hooks) {
assert.notOk(Chart.isPresent);
});
+ test('it renders all phases when there are any non-main tasks', async function(assert) {
+ this.set('tasks', [tasks[0], tasks[6]]);
+
+ await render(hbs`
`);
+ assert.ok(Chart.phases.length, 4);
+ });
+
test('it reflects phase and task states when states are passed in', async function(assert) {
this.set(
'taskStates',
@@ -90,16 +126,64 @@ module('Integration | Component | lifecycle-chart', function(hooks) {
assert.notOk(task.isFinished);
});
- this.set('taskStates.firstObject.state', 'running');
+ // Change poststart-ephemeral to be running
+ this.set('taskStates.4.state', 'running');
await settled();
- assert.ok(Chart.phases[1].isActive);
- assert.ok(Chart.tasks[3].isActive);
await componentA11yAudit(this.element, assert);
- this.set('taskStates.firstObject.finishedAt', new Date());
+ assert.ok(Chart.tasks[5].isActive);
+
+ assert.ok(Chart.phases[1].isActive);
+ assert.notOk(
+ Chart.phases[2].isActive,
+ 'the poststart phase is nested within main and should never have the active class'
+ );
+
+ this.set('taskStates.4.finishedAt', new Date());
await settled();
- assert.ok(Chart.tasks[3].isFinished);
+ assert.ok(Chart.tasks[5].isFinished);
+ });
+
+ [
+ {
+ testName: 'expected active phases',
+ runningTaskNames: ['prestart ephemeral', 'main one', 'poststop'],
+ activePhaseNames: ['Prestart', 'Main', 'Poststop'],
+ },
+ {
+ testName: 'sidecar task states don’t affect phase active states',
+ runningTaskNames: ['prestart sidecar', 'poststart sidecar'],
+ activePhaseNames: [],
+ },
+ {
+ testName: 'poststart ephemeral task states affect main phase active state',
+ runningTaskNames: ['poststart ephemeral'],
+ activePhaseNames: ['Main'],
+ },
+ ].forEach(async ({ testName, runningTaskNames, activePhaseNames }) => {
+ test(testName, async function(assert) {
+ this.set('taskStates', tasks.map(task => ({ task })));
+
+ await render(hbs`
`);
+
+ runningTaskNames.forEach(taskName => {
+ const taskState = this.get('taskStates').find(taskState =>
+ taskState.task.name.includes(taskName)
+ );
+ set(taskState, 'state', 'running');
+ });
+
+ await settled();
+
+ Chart.phases.forEach(Phase => {
+ if (activePhaseNames.includes(Phase.name)) {
+ assert.ok(Phase.isActive, `expected ${Phase.name} not to be active`);
+ } else {
+ assert.notOk(Phase.isActive, `expected ${Phase.name} phase not to be active`);
+ }
+ });
+ });
});
});
diff --git a/ui/tests/pages/components/lifecycle-chart.js b/ui/tests/pages/components/lifecycle-chart.js
index 1733849b7..47f1493e4 100644
--- a/ui/tests/pages/components/lifecycle-chart.js
+++ b/ui/tests/pages/components/lifecycle-chart.js
@@ -19,8 +19,11 @@ export default {
isFinished: hasClass('is-finished'),
isMain: hasClass('main'),
- isPrestart: hasClass('prestart'),
- isSidecar: hasClass('sidecar'),
+ isPrestartEphemeral: hasClass('prestart-ephemeral'),
+ isPrestartSidecar: hasClass('prestart-sidecar'),
+ isPoststartEphemeral: hasClass('poststart-ephemeral'),
+ isPoststartSidecar: hasClass('poststart-sidecar'),
+ isPoststop: hasClass('poststop'),
visit: clickable('a'),
}),