UI: Add representations for task lifecycles (#7659)
This adds details about task lifecycles to allocations, task groups, and tasks. It includes a live-updating timeline-like chart on allocations.
This commit is contained in:
parent
a7a64443e1
commit
a9004faa11
|
@ -1,5 +1,8 @@
|
|||
## 0.11.2 (Unreleased)
|
||||
|
||||
FEATURES:
|
||||
* **Task dependencies UI**: task lifecycle charts and details
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* api: autoscaling policies should not be returned for stopped jobs [[GH-7768](https://github.com/hashicorp/nomad/issues/7768)]
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
activeClass: computed('taskState.state', function() {
|
||||
if (this.taskState && this.taskState.state === 'running') {
|
||||
return 'is-active';
|
||||
}
|
||||
}),
|
||||
|
||||
finishedClass: computed('taskState.finishedAt', function() {
|
||||
if (this.taskState && this.taskState.finishedAt) {
|
||||
return 'is-finished';
|
||||
}
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { sort } from '@ember/object/computed';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
tasks: null,
|
||||
taskStates: null,
|
||||
|
||||
lifecyclePhases: computed('tasks.@each.lifecycle', 'taskStates.@each.state', function() {
|
||||
const tasksOrStates = this.taskStates || this.tasks;
|
||||
const lifecycles = {
|
||||
prestarts: [],
|
||||
sidecars: [],
|
||||
mains: [],
|
||||
};
|
||||
|
||||
tasksOrStates.forEach(taskOrState => {
|
||||
const task = taskOrState.task || taskOrState;
|
||||
lifecycles[`${task.lifecycleName}s`].push(taskOrState);
|
||||
});
|
||||
|
||||
const phases = [];
|
||||
|
||||
if (lifecycles.prestarts.length || lifecycles.sidecars.length) {
|
||||
phases.push({
|
||||
name: 'Prestart',
|
||||
isActive: lifecycles.prestarts.some(state => state.state === 'running'),
|
||||
});
|
||||
}
|
||||
|
||||
if (lifecycles.sidecars.length || lifecycles.mains.length) {
|
||||
phases.push({
|
||||
name: 'Main',
|
||||
isActive: lifecycles.mains.some(state => state.state === 'running'),
|
||||
});
|
||||
}
|
||||
|
||||
return phases;
|
||||
}),
|
||||
|
||||
sortedLifecycleTaskStates: sort('taskStates', function(a, b) {
|
||||
return getTaskSortPrefix(a.task).localeCompare(getTaskSortPrefix(b.task));
|
||||
}),
|
||||
|
||||
sortedLifecycleTasks: sort('tasks', function(a, b) {
|
||||
return getTaskSortPrefix(a).localeCompare(getTaskSortPrefix(b));
|
||||
}),
|
||||
});
|
||||
|
||||
const lifecycleNameSortPrefix = {
|
||||
prestart: 0,
|
||||
sidecar: 1,
|
||||
main: 2,
|
||||
};
|
||||
|
||||
function getTaskSortPrefix(task) {
|
||||
// Prestarts first, then sidecars, then mains
|
||||
return `${lifecycleNameSortPrefix[task.lifecycleName]}-${task.name}`;
|
||||
}
|
|
@ -5,6 +5,15 @@ import { alias } from '@ember/object/computed';
|
|||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
otherTaskStates: computed('model.task.taskGroup.tasks.@each.name', function() {
|
||||
const taskName = this.model.task.name;
|
||||
return this.model.allocation.states.rejectBy('name', taskName);
|
||||
}),
|
||||
|
||||
prestartTaskStates: computed('otherTaskStates.@each.lifecycle', function() {
|
||||
return this.otherTaskStates.filterBy('task.lifecycle');
|
||||
}),
|
||||
|
||||
network: alias('model.resources.networks.firstObject'),
|
||||
ports: computed('network.reservedPorts.[]', 'network.dynamicPorts.[]', function() {
|
||||
return (this.get('network.reservedPorts') || [])
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import attr from 'ember-data/attr';
|
||||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { fragmentOwner } from 'ember-data-model-fragments/attributes';
|
||||
|
||||
export default Fragment.extend({
|
||||
task: fragmentOwner(),
|
||||
|
||||
hook: attr('string'),
|
||||
sidecar: attr('boolean'),
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
import attr from 'ember-data/attr';
|
||||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { fragmentArray, fragmentOwner } from 'ember-data-model-fragments/attributes';
|
||||
import { fragment, fragmentArray, fragmentOwner } from 'ember-data-model-fragments/attributes';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default Fragment.extend({
|
||||
taskGroup: fragmentOwner(),
|
||||
|
@ -9,6 +10,14 @@ export default Fragment.extend({
|
|||
driver: attr('string'),
|
||||
kind: attr('string'),
|
||||
|
||||
lifecycle: fragment('lifecycle'),
|
||||
|
||||
lifecycleName: computed('lifecycle', 'lifecycle.sidecar', function() {
|
||||
if (this.lifecycle && this.lifecycle.sidecar) return 'sidecar';
|
||||
if (this.lifecycle && this.lifecycle.hook === 'prestart') return 'prestart';
|
||||
return 'main';
|
||||
}),
|
||||
|
||||
reservedMemory: attr('number'),
|
||||
reservedCPU: attr('number'),
|
||||
reservedDisk: attr('number'),
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
@import './components/image-file.scss';
|
||||
@import './components/inline-definitions';
|
||||
@import './components/job-diff';
|
||||
@import './components/lifecycle-chart';
|
||||
@import './components/loading-spinner';
|
||||
@import './components/metrics';
|
||||
@import './components/node-status-light';
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
.lifecycle-chart {
|
||||
padding-top: 2rem;
|
||||
position: relative;
|
||||
|
||||
.lifecycle-phases {
|
||||
position: absolute;
|
||||
top: 1.5em;
|
||||
bottom: 1.5em;
|
||||
right: 1.5em;
|
||||
left: 1.5em;
|
||||
|
||||
.divider {
|
||||
position: absolute;
|
||||
left: 25%;
|
||||
height: 100%;
|
||||
|
||||
stroke: $ui-gray-200;
|
||||
stroke-width: 3px;
|
||||
stroke-dasharray: 1, 7;
|
||||
stroke-dashoffset: 1;
|
||||
stroke-linecap: square;
|
||||
}
|
||||
}
|
||||
|
||||
.lifecycle-phase {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
|
||||
border-top: 2px solid transparent;
|
||||
|
||||
.name {
|
||||
padding: 0.5rem 0.9rem;
|
||||
font-size: $size-7;
|
||||
font-weight: $weight-semibold;
|
||||
color: $ui-gray-500;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: $white-bis;
|
||||
border-top: 2px solid $vagrant-blue;
|
||||
|
||||
.name {
|
||||
color: $vagrant-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.prestart {
|
||||
left: 0;
|
||||
right: 75%;
|
||||
}
|
||||
|
||||
&.main {
|
||||
left: 25%;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lifecycle-chart-rows {
|
||||
margin-top: 2.5em;
|
||||
}
|
||||
|
||||
.lifecycle-chart-row {
|
||||
position: relative;
|
||||
|
||||
.task {
|
||||
margin: 0.55em 0.9em;
|
||||
padding: 0.3em 0.55em;
|
||||
border: 1px solid $grey-blue;
|
||||
border-radius: $radius;
|
||||
background: white;
|
||||
|
||||
.name {
|
||||
font-weight: $weight-semibold;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.name a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.lifecycle {
|
||||
font-size: $size-7;
|
||||
color: $ui-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.task {
|
||||
border-color: $nomad-green;
|
||||
background: lighten($nomad-green, 50%);
|
||||
|
||||
.lifecycle {
|
||||
color: $ui-gray-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-finished {
|
||||
.task {
|
||||
color: $ui-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
&.main {
|
||||
margin-left: 25%;
|
||||
}
|
||||
|
||||
&.prestart {
|
||||
margin-right: 75%;
|
||||
}
|
||||
|
||||
&:last-child .task {
|
||||
margin-bottom: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
$ui-gray-200: #dce0e6;
|
||||
$ui-gray-300: #bac1cc;
|
||||
$ui-gray-400: #8e96a3;
|
||||
$ui-gray-500: #6f7682;
|
||||
$ui-gray-700: #525761;
|
||||
$ui-gray-800: #373a42;
|
||||
|
|
|
@ -88,6 +88,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{lifecycle-chart taskStates=model.states}}
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Tasks
|
||||
|
|
|
@ -63,6 +63,10 @@
|
|||
<span class="term">Driver</span>
|
||||
{{model.task.driver}}
|
||||
</span>
|
||||
<span class="pair">
|
||||
<span class="term">Lifecycle</span>
|
||||
<span data-test-lifecycle>{{model.task.lifecycleName}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -89,6 +93,38 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (and (not model.task.lifecycle) prestartTaskStates)}}
|
||||
<div class="boxed-section" data-test-prestart-tasks>
|
||||
<div class="boxed-section-head">
|
||||
Prestart Tasks
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{#list-table source=prestartTaskStates as |t|}}
|
||||
{{#t.head}}
|
||||
<th class="is-narrow"></th>
|
||||
<th>Task</th>
|
||||
<th>State</th>
|
||||
<th>Lifecycle</th>
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
<tr data-test-prestart-task>
|
||||
<td class="is-narrow">
|
||||
{{#if (and row.model.isRunning (eq row.model.task.lifecycleName "prestart"))}}
|
||||
<span class="tooltip text-center" role="tooltip" aria-label="Lifecycle constraints not met">
|
||||
{{x-icon "warning" class="is-warning"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-test-name>{{row.model.task.name}}</td>
|
||||
<td data-test-state>{{row.model.state}}</td>
|
||||
<td data-test-lifecycle>{{row.model.task.lifecycleName}}</td>
|
||||
</tr>
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if network.ports.length}}
|
||||
<div class="boxed-section" data-test-task-addresses>
|
||||
<div class="boxed-section-head">
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<div
|
||||
class="lifecycle-chart-row {{task.lifecycleName}} {{activeClass}} {{finishedClass}}"
|
||||
data-test-lifecycle-task>
|
||||
<div class="task">
|
||||
<div class="name" data-test-name>
|
||||
{{#if taskState}}
|
||||
{{#link-to "allocations.allocation.task" taskState.allocation taskState}}
|
||||
{{task.name}}
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{task.name}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="lifecycle" data-test-lifecycle>{{capitalize task.lifecycleName}} Task</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,33 @@
|
|||
{{#if (gt lifecyclePhases.length 1)}}
|
||||
<div class="boxed-section" data-test-lifecycle-chart>
|
||||
<div class="boxed-section-head">
|
||||
Task Lifecycle {{if taskStates "Status" "Configuration"}}
|
||||
</div>
|
||||
<div class="boxed-section-body lifecycle-chart">
|
||||
|
||||
<div class="lifecycle-phases">
|
||||
{{#each lifecyclePhases as |phase|}}
|
||||
<div class="lifecycle-phase {{if phase.isActive "is-active"}} {{if (eq phase.name "Main") "main" "prestart"}}" data-test-lifecycle-phase>
|
||||
<div class="name" data-test-name>{{phase.name}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
<svg class="divider">
|
||||
<line x1="0" y1="0" x2="0" y2="100%" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="lifecycle-chart-rows">
|
||||
{{#if tasks}}
|
||||
{{#each sortedLifecycleTasks as |task|}}
|
||||
{{lifecycle-chart-row task=task}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#each sortedLifecycleTaskStates as |state|}}
|
||||
{{lifecycle-chart-row taskState=state task=state.task}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -112,6 +112,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{lifecycle-chart tasks=model.tasks}}
|
||||
|
||||
{{#if model.volumes.length}}
|
||||
<div data-test-volumes class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
|
|
|
@ -17,4 +17,16 @@ export default Factory.extend({
|
|||
driver: () => faker.helpers.randomize(DRIVERS),
|
||||
|
||||
Resources: generateResources,
|
||||
|
||||
Lifecycle: i => {
|
||||
const cycle = i % 3;
|
||||
|
||||
if (cycle === 0) {
|
||||
return null;
|
||||
} else if (cycle === 1) {
|
||||
return { Hook: 'prestart', Sidecar: false };
|
||||
} else {
|
||||
return { Hook: 'prestart', Sidecar: true };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -72,6 +72,111 @@ module('Acceptance | allocation detail', function(hooks) {
|
|||
assert.equal(Allocation.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory');
|
||||
});
|
||||
|
||||
test('/allocation/:id should present task lifecycles', async function(assert) {
|
||||
const job = server.create('job', {
|
||||
groupsCount: 1,
|
||||
groupTaskCount: 3,
|
||||
withGroupServices: true,
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
const allocation = server.create('allocation', 'withTaskWithPorts', 'withAllocatedResources', {
|
||||
clientStatus: 'running',
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
await Allocation.lifecycleChart.tasks[0].visit();
|
||||
assert.equal(currentURL(), `/allocations/${allocation.id}/${sortedServerStates[0].name}`);
|
||||
});
|
||||
|
||||
test('/allocation/:id should list all tasks for the allocation', async function(assert) {
|
||||
assert.equal(
|
||||
Allocation.tasks.length,
|
||||
|
|
|
@ -31,6 +31,10 @@ module('Acceptance | task detail', function(hooks) {
|
|||
'Task started at'
|
||||
);
|
||||
|
||||
const lifecycle = server.db.tasks.where({ name: task.name })[0].Lifecycle;
|
||||
const prestartString = lifecycle && lifecycle.Sidecar ? 'sidecar' : 'prestart';
|
||||
assert.equal(Task.lifecycle, lifecycle ? prestartString : 'main');
|
||||
|
||||
assert.equal(document.title, `Task ${task.name} - Nomad`);
|
||||
});
|
||||
|
||||
|
@ -92,6 +96,86 @@ 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.task_group_ids.forEach(taskGroupId => {
|
||||
server.create('allocation', {
|
||||
jobId: job.id,
|
||||
taskGroup: server.db.taskGroups.find(taskGroupId).name,
|
||||
forceRunningClientStatus: true,
|
||||
});
|
||||
});
|
||||
|
||||
const taskGroup = job.task_groups.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.task_groups.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 addresses table lists all reserved and dynamic ports', async function(assert) {
|
||||
const taskResources = allocation.taskResourceIds
|
||||
.map(id => server.db.taskResources.find(id))
|
||||
|
|
|
@ -241,6 +241,34 @@ module('Acceptance | task group detail', function(hooks) {
|
|||
assert.notOk(normalRow.rescheduled, 'Normal row has no reschedule icon');
|
||||
});
|
||||
|
||||
test('/jobs/:id/:task-group should present task lifecycles', async function(assert) {
|
||||
job = server.create('job', {
|
||||
groupsCount: 2,
|
||||
groupTaskCount: 3,
|
||||
});
|
||||
|
||||
const taskGroups = server.db.taskGroups.where({ jobId: job.id });
|
||||
taskGroup = taskGroups[0];
|
||||
|
||||
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
|
||||
|
||||
assert.ok(TaskGroup.lifecycleChart.isPresent);
|
||||
assert.equal(TaskGroup.lifecycleChart.title, 'Task Lifecycle Configuration');
|
||||
|
||||
tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
|
||||
const taskNames = tasks.mapBy('name');
|
||||
|
||||
// This is thoroughly tested in allocation detail tests, so this mostly checks what’s different
|
||||
|
||||
assert.equal(TaskGroup.lifecycleChart.tasks.length, 3);
|
||||
|
||||
TaskGroup.lifecycleChart.tasks.forEach(Task => {
|
||||
assert.ok(taskNames.includes(Task.name));
|
||||
assert.notOk(Task.isActive);
|
||||
assert.notOk(Task.isFinished);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the task group depends on volumes, the volumes table is shown', async function(assert) {
|
||||
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
|
||||
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, settled } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import LifecycleChart from 'nomad-ui/tests/pages/components/lifecycle-chart';
|
||||
|
||||
const Chart = create(LifecycleChart);
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
lifecycleName: 'main',
|
||||
name: 'main two',
|
||||
},
|
||||
{
|
||||
lifecycleName: 'main',
|
||||
name: 'main one',
|
||||
},
|
||||
{
|
||||
lifecycleName: 'prestart',
|
||||
name: 'prestart',
|
||||
},
|
||||
{
|
||||
lifecycleName: 'sidecar',
|
||||
name: 'sidecar',
|
||||
},
|
||||
];
|
||||
|
||||
module('Integration | Component | lifecycle-chart', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders stateless phases and lifecycle- and name-sorted tasks', async function(assert) {
|
||||
this.set('tasks', tasks);
|
||||
|
||||
await render(hbs`{{lifecycle-chart tasks=tasks}}`);
|
||||
assert.ok(Chart.isPresent);
|
||||
|
||||
assert.equal(Chart.phases[0].name, 'Prestart');
|
||||
assert.equal(Chart.phases[1].name, 'Main');
|
||||
|
||||
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('lifecycle'), [
|
||||
'Prestart Task',
|
||||
'Sidecar Task',
|
||||
'Main Task',
|
||||
'Main Task',
|
||||
]);
|
||||
|
||||
assert.ok(Chart.tasks[0].isPrestart);
|
||||
assert.ok(Chart.tasks[1].isSidecar);
|
||||
assert.ok(Chart.tasks[2].isMain);
|
||||
|
||||
Chart.tasks.forEach(task => {
|
||||
assert.notOk(task.isActive);
|
||||
assert.notOk(task.isFinished);
|
||||
});
|
||||
});
|
||||
|
||||
test('it doesn’t render when there’s only one phase', async function(assert) {
|
||||
this.set('tasks', [
|
||||
{
|
||||
lifecycleName: 'main',
|
||||
},
|
||||
]);
|
||||
|
||||
await render(hbs`{{lifecycle-chart tasks=tasks}}`);
|
||||
assert.notOk(Chart.isPresent);
|
||||
});
|
||||
|
||||
test('it reflects phase and task states when states are passed in', async function(assert) {
|
||||
this.set(
|
||||
'taskStates',
|
||||
tasks.map(task => {
|
||||
return { task };
|
||||
})
|
||||
);
|
||||
|
||||
await render(hbs`{{lifecycle-chart taskStates=taskStates}}`);
|
||||
assert.ok(Chart.isPresent);
|
||||
|
||||
Chart.phases.forEach(phase => assert.notOk(phase.isActive));
|
||||
|
||||
Chart.tasks.forEach(task => {
|
||||
assert.notOk(task.isActive);
|
||||
assert.notOk(task.isFinished);
|
||||
});
|
||||
|
||||
this.set('taskStates.firstObject.state', 'running');
|
||||
await settled();
|
||||
|
||||
assert.ok(Chart.phases[1].isActive);
|
||||
assert.ok(Chart.tasks[3].isActive);
|
||||
|
||||
this.set('taskStates.firstObject.finishedAt', new Date());
|
||||
await settled();
|
||||
|
||||
assert.ok(Chart.tasks[3].isFinished);
|
||||
});
|
||||
});
|
|
@ -10,6 +10,7 @@ import {
|
|||
|
||||
import allocations from 'nomad-ui/tests/pages/components/allocations';
|
||||
import twoStepButton from 'nomad-ui/tests/pages/components/two-step-button';
|
||||
import LifecycleChart from 'nomad-ui/tests/pages/components/lifecycle-chart';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/allocations/:id'),
|
||||
|
@ -36,6 +37,8 @@ export default create({
|
|||
|
||||
resourceEmptyMessage: text('[data-test-resource-error-headline]'),
|
||||
|
||||
lifecycleChart: LifecycleChart,
|
||||
|
||||
tasks: collection('[data-test-task-row]', {
|
||||
name: text('[data-test-name]'),
|
||||
state: text('[data-test-state]'),
|
||||
|
|
|
@ -21,9 +21,11 @@ export default create({
|
|||
},
|
||||
},
|
||||
|
||||
state: text('[data-test-state]'),
|
||||
state: text('.title [data-test-state]'),
|
||||
startedAt: text('[data-test-started-at]'),
|
||||
|
||||
lifecycle: text('.pair [data-test-lifecycle]'),
|
||||
|
||||
restart: twoStepButton('[data-test-restart]'),
|
||||
|
||||
execButton: {
|
||||
|
@ -47,6 +49,14 @@ export default create({
|
|||
|
||||
resourceEmptyMessage: text('[data-test-resource-error-headline]'),
|
||||
|
||||
hasPrestartTasks: isPresent('[data-test-prestart-tasks]'),
|
||||
prestartTasks: collection('[data-test-prestart-task]', {
|
||||
name: text('[data-test-name]'),
|
||||
state: text('[data-test-state]'),
|
||||
lifecycle: text('[data-test-lifecycle]'),
|
||||
isBlocking: isPresent('.icon-is-warning'),
|
||||
}),
|
||||
|
||||
hasAddresses: isPresent('[data-test-task-addresses]'),
|
||||
addresses: collection('[data-test-task-address]', {
|
||||
name: text('[data-test-task-address-name]'),
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { clickable, collection, hasClass, text } from 'ember-cli-page-object';
|
||||
|
||||
export default {
|
||||
scope: '[data-test-lifecycle-chart]',
|
||||
|
||||
title: text('.boxed-section-head'),
|
||||
|
||||
phases: collection('[data-test-lifecycle-phase]', {
|
||||
name: text('[data-test-name]'),
|
||||
|
||||
isActive: hasClass('is-active'),
|
||||
}),
|
||||
|
||||
tasks: collection('[data-test-lifecycle-task]', {
|
||||
name: text('[data-test-name]'),
|
||||
lifecycle: text('[data-test-lifecycle]'),
|
||||
|
||||
isActive: hasClass('is-active'),
|
||||
isFinished: hasClass('is-finished'),
|
||||
|
||||
isMain: hasClass('main'),
|
||||
isPrestart: hasClass('prestart'),
|
||||
isSidecar: hasClass('sidecar'),
|
||||
|
||||
visit: clickable('a'),
|
||||
}),
|
||||
};
|
|
@ -12,6 +12,7 @@ import {
|
|||
import allocations from 'nomad-ui/tests/pages/components/allocations';
|
||||
import error from 'nomad-ui/tests/pages/components/error';
|
||||
import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select';
|
||||
import LifecycleChart from 'nomad-ui/tests/pages/components/lifecycle-chart';
|
||||
|
||||
export default create({
|
||||
pageSize: 25,
|
||||
|
@ -39,6 +40,8 @@ export default create({
|
|||
|
||||
isEmpty: isPresent('[data-test-empty-allocations-list]'),
|
||||
|
||||
lifecycleChart: LifecycleChart,
|
||||
|
||||
hasVolumes: isPresent('[data-test-volumes]'),
|
||||
volumes: collection('[data-test-volumes] [data-test-volume]', {
|
||||
name: text('[data-test-volume-name]'),
|
||||
|
|
Loading…
Reference in New Issue