Merge pull request #3472 from hashicorp/f-ui-task-hierarchy
Task route hierarchy in the UI
This commit is contained in:
commit
e8bb3318ff
|
@ -1,17 +1,5 @@
|
|||
import Ember from 'ember';
|
||||
import Sortable from 'nomad-ui/mixins/sortable';
|
||||
|
||||
const { Controller, computed } = Ember;
|
||||
const { Controller } = Ember;
|
||||
|
||||
export default Controller.extend(Sortable, {
|
||||
queryParams: {
|
||||
sortProperty: 'sort',
|
||||
sortDescending: 'desc',
|
||||
},
|
||||
|
||||
sortProperty: 'name',
|
||||
sortDescending: false,
|
||||
|
||||
listToSort: computed.alias('model.states'),
|
||||
sortedStates: computed.alias('listSorted'),
|
||||
});
|
||||
export default Controller.extend({});
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import Ember from 'ember';
|
||||
import Sortable from 'nomad-ui/mixins/sortable';
|
||||
|
||||
const { Controller, computed } = Ember;
|
||||
|
||||
export default Controller.extend(Sortable, {
|
||||
queryParams: {
|
||||
sortProperty: 'sort',
|
||||
sortDescending: 'desc',
|
||||
},
|
||||
|
||||
sortProperty: 'name',
|
||||
sortDescending: false,
|
||||
|
||||
listToSort: computed.alias('model.states'),
|
||||
sortedStates: computed.alias('listSorted'),
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { Controller, computed } = Ember;
|
||||
|
||||
export default Controller.extend({
|
||||
network: computed.alias('model.resources.networks.firstObject'),
|
||||
ports: computed('network.reservedPorts.[]', 'network.dynamicPorts.[]', function() {
|
||||
return this.get('network.reservedPorts')
|
||||
.map(port => ({
|
||||
name: port.Label,
|
||||
port: port.Value,
|
||||
isDynamic: false,
|
||||
}))
|
||||
.concat(
|
||||
this.get('network.dynamicPorts').map(port => ({
|
||||
name: port.Label,
|
||||
port: port.Value,
|
||||
isDynamic: true,
|
||||
}))
|
||||
)
|
||||
.sortBy('name');
|
||||
}),
|
||||
});
|
|
@ -38,6 +38,18 @@ export default Model.extend({
|
|||
return STATUS_ORDER[this.get('clientStatus')] || 100;
|
||||
}),
|
||||
|
||||
statusClass: computed('clientStatus', function() {
|
||||
const classMap = {
|
||||
pending: 'is-pending',
|
||||
running: 'is-primary',
|
||||
complete: 'is-complete',
|
||||
failed: 'is-error',
|
||||
lost: 'is-light',
|
||||
};
|
||||
|
||||
return classMap[this.get('clientStatus')] || 'is-dark';
|
||||
}),
|
||||
|
||||
taskGroup: computed('taskGroupName', 'job.taskGroups.[]', function() {
|
||||
const taskGroups = this.get('job.taskGroups');
|
||||
return taskGroups && taskGroups.findBy('name', this.get('taskGroupName'));
|
||||
|
|
|
@ -22,4 +22,15 @@ export default Fragment.extend({
|
|||
|
||||
resources: fragment('resources'),
|
||||
events: fragmentArray('task-event'),
|
||||
|
||||
stateClass: computed('state', function() {
|
||||
const classMap = {
|
||||
pending: 'is-pending',
|
||||
running: 'is-primary',
|
||||
finished: 'is-complete',
|
||||
failed: 'is-error',
|
||||
};
|
||||
|
||||
return classMap[this.get('state')] || 'is-dark';
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -25,7 +25,11 @@ Router.map(function() {
|
|||
});
|
||||
|
||||
this.route('allocations', function() {
|
||||
this.route('allocation', { path: '/:allocation_id' });
|
||||
this.route('allocation', { path: '/:allocation_id' }, function() {
|
||||
this.route('task', { path: '/:name' }, function() {
|
||||
this.route('logs');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.route('settings', function() {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { Route, inject, Error: EmberError } = Ember;
|
||||
|
||||
export default Route.extend({
|
||||
store: inject.service(),
|
||||
|
||||
model({ name }) {
|
||||
const allocation = this.modelFor('allocations.allocation');
|
||||
if (allocation) {
|
||||
const task = allocation.get('states').findBy('name', name);
|
||||
|
||||
if (task) {
|
||||
return task;
|
||||
}
|
||||
|
||||
const err = new EmberError(`Task ${name} not found for allocation ${allocation.get('id')}`);
|
||||
err.code = '404';
|
||||
this.controllerFor('application').set('error', err);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -4,11 +4,6 @@
|
|||
opacity: 0.7;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $primary-invert;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
+ .breadcrumb {
|
||||
margin-left: 15px;
|
||||
&::before {
|
||||
|
@ -23,4 +18,9 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
a.breadcrumb:hover {
|
||||
color: $primary-invert;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,16 @@
|
|||
color: $blue-invert;
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
background: $primary;
|
||||
color: $primary-invert;
|
||||
}
|
||||
|
||||
&.is-complete {
|
||||
background: $nomad-green-dark;
|
||||
color: findColorInvert($nomad-green-dark);
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
background: $danger;
|
||||
color: $danger-invert;
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
<div class="page-layout">
|
||||
{{#global-header class="page-header"}}
|
||||
Allocations
|
||||
{{/global-header}}
|
||||
{{#gutter-menu class="page-body"}}
|
||||
{{outlet}}
|
||||
{{/gutter-menu}}
|
||||
{{outlet}}
|
||||
</div>
|
||||
|
|
|
@ -1,91 +1 @@
|
|||
<section class="section">
|
||||
<h1 class="title">Allocation {{model.name}}</h1>
|
||||
<h3 class="subtitle">
|
||||
For job {{#link-to "jobs.job" model.job (query-params jobNamespace=model.job.namespace.id)}}{{model.job.name}}{{/link-to}}
|
||||
on client {{#link-to "clients.client" model.node}}{{model.node.shortId}}{{/link-to}}
|
||||
</h3>
|
||||
|
||||
<div class="message">
|
||||
<div class="message-header">
|
||||
Tasks
|
||||
</div>
|
||||
{{#list-table
|
||||
source=sortedStates
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
class="is-striped tasks" as |t|}}
|
||||
{{#t.head}}
|
||||
{{#t.sort-by prop="name"}}Name{{/t.sort-by}}
|
||||
{{#t.sort-by prop="state"}}State{{/t.sort-by}}
|
||||
<th>Last Event</th>
|
||||
{{#t.sort-by prop="events.lastObject.time"}}Time{{/t.sort-by}}
|
||||
<th>Addresses</th>
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
<tr>
|
||||
<td>{{row.model.task.name}}</td>
|
||||
<td>{{row.model.state}}</td>
|
||||
<td>
|
||||
{{#if row.model.events.lastObject.displayMessage}}
|
||||
{{row.model.events.lastObject.displayMessage}}
|
||||
{{else}}
|
||||
<em>No message</em>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>{{moment-format row.model.events.lastObject.time "MM/DD/YY HH:mm:ss [UTC]"}}</td>
|
||||
<td>
|
||||
<ul>
|
||||
{{#each row.model.resources.networks.firstObject.reservedPorts as |port|}}
|
||||
<li>
|
||||
<strong>{{port.Label}}:</strong>
|
||||
<a href="http://{{row.model.allocation.node.address}}:{{port.Value}}" target="_blank">{{row.model.allocation.node.address}}:{{port.Value}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{#each row.model.resources.networks.firstObject.dynamicPorts as |port|}}
|
||||
<li>
|
||||
<strong>{{port.Label}}:</strong>
|
||||
<a href="http://{{row.model.allocation.node.address}}:{{port.Value}}" target="_blank">{{row.model.allocation.node.address}}:{{port.Value}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
</div>
|
||||
|
||||
{{#each model.states as |state|}}
|
||||
<div class="message task-state-events">
|
||||
<div class="message-header">
|
||||
{{state.task.name}} ({{state.state}}) Started: {{moment-format state.startedAt "MM/DD/YY HH:mm:ss [UTC]"}}
|
||||
{{#unless state.isActive}}
|
||||
Ended: {{moment-format state.finishedAt "MM/DD/YY HH:mm:ss [UTC]"}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
<table class="table is-striped task-events">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="is-3">Time</td>
|
||||
<td class="is-1">Type</td>
|
||||
<td>Description</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each (reverse state.events) as |event|}}
|
||||
<tr>
|
||||
<td>{{moment-format event.time "MM/DD/YY HH:mm:ss [UTC]"}}</td>
|
||||
<td>{{event.type}}</td>
|
||||
<td>
|
||||
{{#if event.displayMessage}}
|
||||
{{event.displayMessage}}
|
||||
{{else}}
|
||||
<em>No message</em>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/each}}
|
||||
</section>
|
||||
{{outlet}}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
{{#global-header class="page-header"}}
|
||||
<span class="breadcrumb">Allocations</span>
|
||||
{{#link-to "allocations.allocation" model class="breadcrumb"}}
|
||||
{{model.shortId}}
|
||||
{{/link-to}}
|
||||
{{/global-header}}
|
||||
{{#gutter-menu class="page-body"}}
|
||||
<section class="section">
|
||||
<h1 class="title">
|
||||
Allocation {{model.name}}
|
||||
<span class="bumper-left tag {{model.statusClass}}">{{model.clientStatus}}</span>
|
||||
<span class="tag is-hollow is-small no-text-transform">{{model.id}}</span>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section is-small">
|
||||
<div class="boxed-section-body inline-definitions">
|
||||
<span class="label">Allocation Details</span>
|
||||
<span class="pair job-link"><span class="term">Job</span>
|
||||
{{#link-to "jobs.job" model.job (query-params jobNamespace=model.job.namespace.id)}}{{model.job.name}}{{/link-to}}
|
||||
</span>
|
||||
<span class="pair node-link"><span class="term">Client</span>
|
||||
{{#link-to "clients.client" model.node}}{{model.node.shortId}}{{/link-to}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Tasks
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{#list-table
|
||||
source=sortedStates
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
class="is-striped tasks" as |t|}}
|
||||
{{#t.head}}
|
||||
{{#t.sort-by prop="name"}}Name{{/t.sort-by}}
|
||||
{{#t.sort-by prop="state"}}State{{/t.sort-by}}
|
||||
<th>Last Event</th>
|
||||
{{#t.sort-by prop="events.lastObject.time"}}Time{{/t.sort-by}}
|
||||
<th>Addresses</th>
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
<tr>
|
||||
<td>
|
||||
{{#link-to "allocations.allocation.task" row.model.allocation row.model}}
|
||||
{{row.model.task.name}}
|
||||
{{/link-to}}
|
||||
</td>
|
||||
<td>{{row.model.state}}</td>
|
||||
<td>
|
||||
{{#if row.model.events.lastObject.displayMessage}}
|
||||
{{row.model.events.lastObject.displayMessage}}
|
||||
{{else}}
|
||||
<em>No message</em>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>{{moment-format row.model.events.lastObject.time "MM/DD/YY HH:mm:ss [UTC]"}}</td>
|
||||
<td>
|
||||
<ul>
|
||||
{{#each row.model.resources.networks.firstObject.reservedPorts as |port|}}
|
||||
<li>
|
||||
<strong>{{port.Label}}:</strong>
|
||||
<a href="http://{{row.model.allocation.node.address}}:{{port.Value}}" target="_blank">{{row.model.allocation.node.address}}:{{port.Value}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{#each row.model.resources.networks.firstObject.dynamicPorts as |port|}}
|
||||
<li>
|
||||
<strong>{{port.Label}}:</strong>
|
||||
<a href="http://{{row.model.allocation.node.address}}:{{port.Value}}" target="_blank">{{row.model.allocation.node.address}}:{{port.Value}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/gutter-menu}}
|
|
@ -0,0 +1 @@
|
|||
{{outlet}}
|
|
@ -0,0 +1,94 @@
|
|||
{{#global-header class="page-header"}}
|
||||
<span class="breadcrumb">Allocations</span>
|
||||
{{#link-to "allocations.allocation" model.allocation class="breadcrumb"}}
|
||||
{{model.allocation.shortId}}
|
||||
{{/link-to}}
|
||||
{{#link-to "allocations.allocation.task" model.allocation model class="breadcrumb"}}
|
||||
{{model.name}}
|
||||
{{/link-to}}
|
||||
{{/global-header}}
|
||||
{{#gutter-menu class="page-body"}}
|
||||
{{partial "allocations/allocation/task/subnav"}}
|
||||
<section class="section">
|
||||
<h1 class="title">
|
||||
{{model.name}}
|
||||
<span class="bumper-left tag {{model.stateClass}}">{{model.state}}</span>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section is-small">
|
||||
<div class="boxed-section-body inline-definitions">
|
||||
<span class="label">Task Details</span>
|
||||
<span class="pair">
|
||||
<span class="term">Started At</span>
|
||||
{{moment-format model.startedAt "MM/DD/YY HH:mm:ss"}}
|
||||
</span>
|
||||
{{#if model.finishedAt}}
|
||||
<span class="pair">
|
||||
<span class="term">Finished At</span>
|
||||
{{moment-format model.finishedAt "MM/DD/YY HH:mm:ss"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
<span class="pair">
|
||||
<span class="term">Driver</span>
|
||||
{{model.task.driver}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if ports.length}}
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Addresses
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{#list-table source=ports class="addresses-list" as |t|}}
|
||||
{{#t.head}}
|
||||
<th class="is-1">Dynamic?</th>
|
||||
<th class="is-2">Name</th>
|
||||
<th>Address</th>
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
<tr>
|
||||
<td>{{if row.model.isDynamic "Yes" "No"}}</td>
|
||||
<td>{{row.model.name}}</td>
|
||||
<td>
|
||||
<a href="http://{{model.allocation.node.address}}:{{row.model.port}}" target="_blank">
|
||||
{{model.allocation.node.address}}:{{row.model.port}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Recent Events
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{#list-table source=(reverse model.events) class="is-striped task-events" as |t|}}
|
||||
{{#t.head}}
|
||||
<th class="is-3">Time</th>
|
||||
<th class="is-1">Type</th>
|
||||
<th>Description</th>
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
<tr>
|
||||
<td>{{moment-format row.model.time "MM/DD/YY HH:mm:ss"}}</td>
|
||||
<td>{{row.model.type}}</td>
|
||||
<td>
|
||||
{{#if row.model.displayMessage}}
|
||||
{{row.model.displayMessage}}
|
||||
{{else}}
|
||||
<em>No message</em>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/gutter-menu}}
|
|
@ -0,0 +1,5 @@
|
|||
<div class="tabs is-subnav">
|
||||
<ul>
|
||||
<li>{{#link-to "allocations.allocation.task.index" model.allocation model activeClass="is-active"}}Overview{{/link-to}}</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -41,8 +41,19 @@ export function generateNetworks(options = {}) {
|
|||
MBits: 10,
|
||||
ReservedPorts: Array(
|
||||
faker.random.number({
|
||||
min: options.minPorts || 0,
|
||||
max: options.maxPorts || 3,
|
||||
min: options.minPorts != null ? options.minPorts : 0,
|
||||
max: options.maxPorts != null ? options.maxPorts : 2,
|
||||
})
|
||||
)
|
||||
.fill(null)
|
||||
.map(() => ({
|
||||
Label: faker.hacker.noun(),
|
||||
Value: faker.random.number({ min: 5000, max: 60000 }),
|
||||
})),
|
||||
DynamicPorts: Array(
|
||||
faker.random.number({
|
||||
min: options.minPorts != null ? options.minPorts : 0,
|
||||
max: options.maxPorts != null ? options.maxPorts : 2,
|
||||
})
|
||||
)
|
||||
.fill(null)
|
||||
|
|
|
@ -36,6 +36,24 @@ export default Factory.extend({
|
|||
},
|
||||
}),
|
||||
|
||||
withoutTaskWithPorts: trait({
|
||||
afterCreate(allocation, server) {
|
||||
const taskGroup = server.db.taskGroups.findBy({ name: allocation.taskGroup });
|
||||
const resources = taskGroup.taskIds.map(id =>
|
||||
server.create(
|
||||
'task-resources',
|
||||
{
|
||||
allocation,
|
||||
name: server.db.tasks.find(id).name,
|
||||
},
|
||||
'withoutReservedPorts'
|
||||
)
|
||||
);
|
||||
|
||||
allocation.update({ taskResourcesIds: resources.mapBy('id') });
|
||||
},
|
||||
}),
|
||||
|
||||
afterCreate(allocation, server) {
|
||||
Ember.assert(
|
||||
'[Mirage] No jobs! make sure jobs are created before allocations',
|
||||
|
|
|
@ -9,4 +9,8 @@ export default Factory.extend({
|
|||
withReservedPorts: trait({
|
||||
resources: () => generateResources({ networks: { minPorts: 1 } }),
|
||||
}),
|
||||
|
||||
withoutReservedPorts: trait({
|
||||
resources: () => generateResources({ networks: { minPorts: 0, maxPorts: 0 } }),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -28,14 +28,19 @@ test('/allocation/:id should name the allocation and link to the corresponding j
|
|||
assert
|
||||
) {
|
||||
assert.ok(find('h1').textContent.includes(allocation.name), 'Allocation name is in the heading');
|
||||
assert.ok(find('h3').textContent.includes(job.name), 'Job name is in the subheading');
|
||||
assert.ok(
|
||||
find('h3').textContent.includes(node.id.split('-')[0]),
|
||||
assert.equal(
|
||||
find('.inline-definitions .job-link a').textContent.trim(),
|
||||
job.name,
|
||||
'Job name is in the subheading'
|
||||
);
|
||||
assert.equal(
|
||||
find('.inline-definitions .node-link a').textContent.trim(),
|
||||
node.id.split('-')[0],
|
||||
'Node short id is in the subheading'
|
||||
);
|
||||
|
||||
andThen(() => {
|
||||
click(findAll('h3 a')[0]);
|
||||
click('.inline-definitions .job-link a');
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
|
@ -45,7 +50,7 @@ test('/allocation/:id should name the allocation and link to the corresponding j
|
|||
visit(`/allocations/${allocation.id}`);
|
||||
|
||||
andThen(() => {
|
||||
click(findAll('h3 a')[1]);
|
||||
click('.inline-definitions .node-link a');
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
|
@ -67,6 +72,7 @@ test('each task row should list high-level information for the task', function(a
|
|||
.map(id => server.db.taskResources.find(id))
|
||||
.sortBy('name')[0];
|
||||
const reservedPorts = taskResources.resources.Networks[0].ReservedPorts;
|
||||
const dynamicPorts = taskResources.resources.Networks[0].DynamicPorts;
|
||||
const taskRow = $(findAll('.tasks tbody tr')[0]);
|
||||
const events = server.db.taskEvents.where({ taskStateId: task.id });
|
||||
const event = events[events.length - 1];
|
||||
|
@ -105,83 +111,17 @@ test('each task row should list high-level information for the task', function(a
|
|||
);
|
||||
|
||||
assert.ok(reservedPorts.length, 'The task has reserved ports');
|
||||
assert.ok(dynamicPorts.length, 'The task has dynamic ports');
|
||||
|
||||
const addressesText = taskRow.find('td:eq(4)').text();
|
||||
reservedPorts.forEach(port => {
|
||||
assert.ok(addressesText.includes(port.Label), `Found label ${port.Label}`);
|
||||
assert.ok(addressesText.includes(port.Value), `Found value ${port.Value}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('/allocation/:id should list recent events for each task', function(assert) {
|
||||
const tasks = server.db.taskStates.where({ allocationId: allocation.id });
|
||||
assert.equal(
|
||||
findAll('.task-state-events').length,
|
||||
tasks.length,
|
||||
'A task state event block per task'
|
||||
);
|
||||
});
|
||||
|
||||
test('each recent events list should include the name, state, and time info for the task', function(
|
||||
assert
|
||||
) {
|
||||
const task = server.db.taskStates.where({ allocationId: allocation.id })[0];
|
||||
const recentEventsSection = $(findAll('.task-state-events')[0]);
|
||||
const heading = recentEventsSection
|
||||
.find('.message-header')
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
assert.ok(heading.includes(task.name), 'Task name');
|
||||
assert.ok(heading.includes(task.state), 'Task state');
|
||||
assert.ok(
|
||||
heading.includes(moment(task.startedAt).format('MM/DD/YY HH:mm:ss [UTC]')),
|
||||
'Task started at'
|
||||
);
|
||||
});
|
||||
|
||||
test('each recent events list should list all recent events for the task', function(assert) {
|
||||
const task = server.db.taskStates.where({ allocationId: allocation.id })[0];
|
||||
const events = server.db.taskEvents.where({ taskStateId: task.id });
|
||||
|
||||
assert.equal(
|
||||
findAll('.task-state-events')[0].querySelectorAll('.task-events tbody tr').length,
|
||||
events.length,
|
||||
`Lists ${events.length} events`
|
||||
);
|
||||
});
|
||||
|
||||
test('each recent event should list the time, type, and description of the event', function(
|
||||
assert
|
||||
) {
|
||||
const task = server.db.taskStates.where({ allocationId: allocation.id })[0];
|
||||
const event = server.db.taskEvents.where({ taskStateId: task.id })[0];
|
||||
const recentEvent = $('.task-state-events:eq(0) .task-events tbody tr:last');
|
||||
|
||||
assert.equal(
|
||||
recentEvent
|
||||
.find('td:eq(0)')
|
||||
.text()
|
||||
.trim(),
|
||||
moment(event.time / 1000000).format('MM/DD/YY HH:mm:ss [UTC]'),
|
||||
'Event timestamp'
|
||||
);
|
||||
assert.equal(
|
||||
recentEvent
|
||||
.find('td:eq(1)')
|
||||
.text()
|
||||
.trim(),
|
||||
event.type,
|
||||
'Event type'
|
||||
);
|
||||
assert.equal(
|
||||
recentEvent
|
||||
.find('td:eq(2)')
|
||||
.text()
|
||||
.trim(),
|
||||
event.message,
|
||||
'Event message'
|
||||
);
|
||||
dynamicPorts.forEach(port => {
|
||||
assert.ok(addressesText.includes(port.Label), `Found label ${port.Label}`);
|
||||
assert.ok(addressesText.includes(port.Value), `Found value ${port.Value}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the allocation is not found, an error message is shown, but the URL persists', function(
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
import Ember from 'ember';
|
||||
import { click, findAll, currentURL, find, visit } from 'ember-native-dom-helpers';
|
||||
import { test } from 'qunit';
|
||||
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
|
||||
import moment from 'moment';
|
||||
import ipParts from 'nomad-ui/utils/ip-parts';
|
||||
|
||||
const { $ } = Ember;
|
||||
|
||||
let allocation;
|
||||
let task;
|
||||
|
||||
moduleForAcceptance('Acceptance | task detail', {
|
||||
beforeEach() {
|
||||
server.create('agent');
|
||||
server.create('node');
|
||||
server.create('job', { createAllocations: false });
|
||||
allocation = server.create('allocation', 'withTaskWithPorts', {
|
||||
useMessagePassthru: true,
|
||||
});
|
||||
task = server.db.taskStates.where({ allocationId: allocation.id })[0];
|
||||
|
||||
visit(`/allocations/${allocation.id}/${task.name}`);
|
||||
},
|
||||
});
|
||||
|
||||
test('/allocation/:id/:task_name should name the task and list high-level task information', function(
|
||||
assert
|
||||
) {
|
||||
assert.ok(find('.title').textContent.includes(task.name), 'Task name');
|
||||
assert.ok(find('.title').textContent.includes(task.state), 'Task state');
|
||||
|
||||
const inlineDefinitions = findAll('.inline-definitions .pair');
|
||||
assert.ok(
|
||||
inlineDefinitions[0].textContent.includes(moment(task.startedAt).format('MM/DD/YY HH:mm:ss')),
|
||||
'Task started at'
|
||||
);
|
||||
});
|
||||
|
||||
test('breadcrumbs includes allocations and link to the allocation detail page', function(assert) {
|
||||
const breadcrumbs = findAll('.breadcrumb');
|
||||
assert.equal(
|
||||
breadcrumbs[0].textContent.trim(),
|
||||
'Allocations',
|
||||
'Allocations is the first breadcrumb'
|
||||
);
|
||||
assert.notEqual(
|
||||
breadcrumbs[0].tagName.toLowerCase(),
|
||||
'a',
|
||||
'Allocations breadcrumb is not a link'
|
||||
);
|
||||
assert.equal(
|
||||
breadcrumbs[1].textContent.trim(),
|
||||
allocation.id.split('-')[0],
|
||||
'Allocation short id is the second breadcrumb'
|
||||
);
|
||||
assert.equal(breadcrumbs[2].textContent.trim(), task.name, 'Task name is the third breadcrumb');
|
||||
|
||||
click(breadcrumbs[1]);
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/allocations/${allocation.id}`,
|
||||
'Second breadcrumb links back to the allocation detail'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('the addresses table lists all reserved and dynamic ports', function(assert) {
|
||||
const taskResources = allocation.taskResourcesIds
|
||||
.map(id => server.db.taskResources.find(id))
|
||||
.sortBy('name')[0];
|
||||
const reservedPorts = taskResources.resources.Networks[0].ReservedPorts;
|
||||
const dynamicPorts = taskResources.resources.Networks[0].DynamicPorts;
|
||||
const addresses = reservedPorts.concat(dynamicPorts);
|
||||
|
||||
assert.equal(
|
||||
findAll('.addresses-list tbody tr').length,
|
||||
addresses.length,
|
||||
'All addresses are listed'
|
||||
);
|
||||
});
|
||||
|
||||
test('each address row shows the label and value of the address', function(assert) {
|
||||
const node = server.db.nodes.find(allocation.nodeId);
|
||||
const taskResources = allocation.taskResourcesIds
|
||||
.map(id => server.db.taskResources.find(id))
|
||||
.findBy('name', task.name);
|
||||
const reservedPorts = taskResources.resources.Networks[0].ReservedPorts;
|
||||
const dynamicPorts = taskResources.resources.Networks[0].DynamicPorts;
|
||||
const address = reservedPorts.concat(dynamicPorts).sortBy('Label')[0];
|
||||
|
||||
const addressRow = $(find('.addresses-list tbody tr'));
|
||||
assert.equal(
|
||||
addressRow
|
||||
.find('td:eq(0)')
|
||||
.text()
|
||||
.trim(),
|
||||
reservedPorts.includes(address) ? 'No' : 'Yes',
|
||||
'Dynamic port is denoted as such'
|
||||
);
|
||||
assert.equal(
|
||||
addressRow
|
||||
.find('td:eq(1)')
|
||||
.text()
|
||||
.trim(),
|
||||
address.Label,
|
||||
'Label'
|
||||
);
|
||||
assert.equal(
|
||||
addressRow
|
||||
.find('td:eq(2)')
|
||||
.text()
|
||||
.trim(),
|
||||
`${ipParts(node.httpAddr).address}:${address.Value}`,
|
||||
'Value'
|
||||
);
|
||||
});
|
||||
|
||||
test('the events table lists all recent events', function(assert) {
|
||||
const events = server.db.taskEvents.where({ taskStateId: task.id });
|
||||
|
||||
assert.equal(
|
||||
findAll('.task-events tbody tr').length,
|
||||
events.length,
|
||||
`Lists ${events.length} events`
|
||||
);
|
||||
});
|
||||
|
||||
test('each recent event should list the time, type, and description of the event', function(
|
||||
assert
|
||||
) {
|
||||
const event = server.db.taskEvents.where({ taskStateId: task.id })[0];
|
||||
const recentEvent = $('.task-events tbody tr:last');
|
||||
|
||||
assert.equal(
|
||||
recentEvent
|
||||
.find('td:eq(0)')
|
||||
.text()
|
||||
.trim(),
|
||||
moment(event.time / 1000000).format('MM/DD/YY HH:mm:ss'),
|
||||
'Event timestamp'
|
||||
);
|
||||
assert.equal(
|
||||
recentEvent
|
||||
.find('td:eq(1)')
|
||||
.text()
|
||||
.trim(),
|
||||
event.type,
|
||||
'Event type'
|
||||
);
|
||||
assert.equal(
|
||||
recentEvent
|
||||
.find('td:eq(2)')
|
||||
.text()
|
||||
.trim(),
|
||||
event.message,
|
||||
'Event message'
|
||||
);
|
||||
});
|
||||
|
||||
test('when the allocation is not found, the application errors', function(assert) {
|
||||
visit(`/allocations/not-a-real-allocation/${task.name}`);
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
server.pretender.handledRequests.findBy('status', 404).url,
|
||||
'/v1/allocation/not-a-real-allocation',
|
||||
'A request to the non-existent allocation is made'
|
||||
);
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/allocations/not-a-real-allocation/${task.name}`,
|
||||
'The URL persists'
|
||||
);
|
||||
assert.ok(find('.error-message'), 'Error message is shown');
|
||||
assert.equal(
|
||||
find('.error-message .title').textContent,
|
||||
'Not Found',
|
||||
'Error message is for 404'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the allocation is found but the task is not, the application errors', function(assert) {
|
||||
visit(`/allocations/${allocation.id}/not-a-real-task-name`);
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
server.pretender.handledRequests.findBy('status', 200).url,
|
||||
`/v1/allocation/${allocation.id}`,
|
||||
'A request to the allocation is made successfully'
|
||||
);
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/allocations/${allocation.id}/not-a-real-task-name`,
|
||||
'The URL persists'
|
||||
);
|
||||
assert.ok(find('.error-message'), 'Error message is shown');
|
||||
assert.equal(
|
||||
find('.error-message .title').textContent,
|
||||
'Not Found',
|
||||
'Error message is for 404'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
moduleForAcceptance('Acceptance | task detail (no addresses)', {
|
||||
beforeEach() {
|
||||
server.create('agent');
|
||||
server.create('node');
|
||||
server.create('job');
|
||||
allocation = server.create('allocation', 'withoutTaskWithPorts');
|
||||
task = server.db.taskStates.where({ allocationId: allocation.id })[0];
|
||||
|
||||
visit(`/allocations/${allocation.id}/${task.name}`);
|
||||
},
|
||||
});
|
||||
|
||||
test('when the task has no addresses, the addresses table is not shown', function(assert) {
|
||||
assert.notOk(find('.addresses-list'), 'No addresses table');
|
||||
});
|
Loading…
Reference in New Issue