diff --git a/ui/app/controllers/allocations/allocation.js b/ui/app/controllers/allocations/allocation.js
index 5a764f2a64b..1ee88420b6e 100644
--- a/ui/app/controllers/allocations/allocation.js
+++ b/ui/app/controllers/allocations/allocation.js
@@ -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({});
diff --git a/ui/app/controllers/allocations/allocation/index.js b/ui/app/controllers/allocations/allocation/index.js
new file mode 100644
index 00000000000..5a764f2a64b
--- /dev/null
+++ b/ui/app/controllers/allocations/allocation/index.js
@@ -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'),
+});
diff --git a/ui/app/controllers/allocations/allocation/task/index.js b/ui/app/controllers/allocations/allocation/task/index.js
new file mode 100644
index 00000000000..669884d9d77
--- /dev/null
+++ b/ui/app/controllers/allocations/allocation/task/index.js
@@ -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');
+ }),
+});
diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js
index 810213bfee4..7d3a680a23a 100644
--- a/ui/app/models/allocation.js
+++ b/ui/app/models/allocation.js
@@ -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'));
diff --git a/ui/app/models/task-state.js b/ui/app/models/task-state.js
index e67b0110b70..a5b3bff22dd 100644
--- a/ui/app/models/task-state.js
+++ b/ui/app/models/task-state.js
@@ -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';
+ }),
});
diff --git a/ui/app/router.js b/ui/app/router.js
index 367e311e2b8..bddf8813da3 100644
--- a/ui/app/router.js
+++ b/ui/app/router.js
@@ -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() {
diff --git a/ui/app/routes/allocations/allocation/task.js b/ui/app/routes/allocations/allocation/task.js
new file mode 100644
index 00000000000..4a57f1ad25c
--- /dev/null
+++ b/ui/app/routes/allocations/allocation/task.js
@@ -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);
+ }
+ },
+});
diff --git a/ui/app/styles/components/breadcrumbs.scss b/ui/app/styles/components/breadcrumbs.scss
index e7d5942e68b..c3ec634a6b4 100644
--- a/ui/app/styles/components/breadcrumbs.scss
+++ b/ui/app/styles/components/breadcrumbs.scss
@@ -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;
+ }
}
diff --git a/ui/app/styles/core/tag.scss b/ui/app/styles/core/tag.scss
index dd7f31e0fe6..de4f25c74f9 100644
--- a/ui/app/styles/core/tag.scss
+++ b/ui/app/styles/core/tag.scss
@@ -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;
diff --git a/ui/app/templates/allocations.hbs b/ui/app/templates/allocations.hbs
index ce0bdbbe5f4..8fab3547f4c 100644
--- a/ui/app/templates/allocations.hbs
+++ b/ui/app/templates/allocations.hbs
@@ -1,8 +1,3 @@
- {{#global-header class="page-header"}}
- Allocations
- {{/global-header}}
- {{#gutter-menu class="page-body"}}
- {{outlet}}
- {{/gutter-menu}}
+ {{outlet}}
diff --git a/ui/app/templates/allocations/allocation.hbs b/ui/app/templates/allocations/allocation.hbs
index 5d9a537900a..c24cd68950a 100644
--- a/ui/app/templates/allocations/allocation.hbs
+++ b/ui/app/templates/allocations/allocation.hbs
@@ -1,91 +1 @@
-
- Allocation {{model.name}}
-
- 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}}
-
-
-
-
- {{#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}}
-
Last Event |
- {{#t.sort-by prop="events.lastObject.time"}}Time{{/t.sort-by}}
-
Addresses |
- {{/t.head}}
- {{#t.body as |row|}}
-
- {{row.model.task.name}} |
- {{row.model.state}} |
-
- {{#if row.model.events.lastObject.displayMessage}}
- {{row.model.events.lastObject.displayMessage}}
- {{else}}
- No message
- {{/if}}
- |
- {{moment-format row.model.events.lastObject.time "MM/DD/YY HH:mm:ss [UTC]"}} |
-
-
- |
-
- {{/t.body}}
- {{/list-table}}
-
-
- {{#each model.states as |state|}}
-
-
-
-
-
- Time |
- Type |
- Description |
-
-
-
- {{#each (reverse state.events) as |event|}}
-
- {{moment-format event.time "MM/DD/YY HH:mm:ss [UTC]"}} |
- {{event.type}} |
-
- {{#if event.displayMessage}}
- {{event.displayMessage}}
- {{else}}
- No message
- {{/if}}
- |
-
- {{/each}}
-
-
-
- {{/each}}
-
+{{outlet}}
diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs
new file mode 100644
index 00000000000..d114f333400
--- /dev/null
+++ b/ui/app/templates/allocations/allocation/index.hbs
@@ -0,0 +1,82 @@
+{{#global-header class="page-header"}}
+ Allocations
+ {{#link-to "allocations.allocation" model class="breadcrumb"}}
+ {{model.shortId}}
+ {{/link-to}}
+{{/global-header}}
+{{#gutter-menu class="page-body"}}
+
+
+ Allocation {{model.name}}
+ {{model.clientStatus}}
+ {{model.id}}
+
+
+
+
+ Allocation Details
+ Job
+ {{#link-to "jobs.job" model.job (query-params jobNamespace=model.job.namespace.id)}}{{model.job.name}}{{/link-to}}
+
+ Client
+ {{#link-to "clients.client" model.node}}{{model.node.shortId}}{{/link-to}}
+
+
+
+
+
+
+ Tasks
+
+
+ {{#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}}
+
Last Event |
+ {{#t.sort-by prop="events.lastObject.time"}}Time{{/t.sort-by}}
+
Addresses |
+ {{/t.head}}
+ {{#t.body as |row|}}
+
+
+ {{#link-to "allocations.allocation.task" row.model.allocation row.model}}
+ {{row.model.task.name}}
+ {{/link-to}}
+ |
+ {{row.model.state}} |
+
+ {{#if row.model.events.lastObject.displayMessage}}
+ {{row.model.events.lastObject.displayMessage}}
+ {{else}}
+ No message
+ {{/if}}
+ |
+ {{moment-format row.model.events.lastObject.time "MM/DD/YY HH:mm:ss [UTC]"}} |
+
+
+ |
+
+ {{/t.body}}
+ {{/list-table}}
+
+
+
+{{/gutter-menu}}
diff --git a/ui/app/templates/allocations/allocation/task.hbs b/ui/app/templates/allocations/allocation/task.hbs
new file mode 100644
index 00000000000..c24cd68950a
--- /dev/null
+++ b/ui/app/templates/allocations/allocation/task.hbs
@@ -0,0 +1 @@
+{{outlet}}
diff --git a/ui/app/templates/allocations/allocation/task/index.hbs b/ui/app/templates/allocations/allocation/task/index.hbs
new file mode 100644
index 00000000000..20dcf01ac4c
--- /dev/null
+++ b/ui/app/templates/allocations/allocation/task/index.hbs
@@ -0,0 +1,94 @@
+{{#global-header class="page-header"}}
+ Allocations
+ {{#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"}}
+
+
+ {{model.name}}
+ {{model.state}}
+
+
+
+
+ Task Details
+
+ Started At
+ {{moment-format model.startedAt "MM/DD/YY HH:mm:ss"}}
+
+ {{#if model.finishedAt}}
+
+ Finished At
+ {{moment-format model.finishedAt "MM/DD/YY HH:mm:ss"}}
+
+ {{/if}}
+
+ Driver
+ {{model.task.driver}}
+
+
+
+
+ {{#if ports.length}}
+
+
+ Addresses
+
+
+ {{#list-table source=ports class="addresses-list" as |t|}}
+ {{#t.head}}
+
Dynamic? |
+
Name |
+
Address |
+ {{/t.head}}
+ {{#t.body as |row|}}
+
+ {{if row.model.isDynamic "Yes" "No"}} |
+ {{row.model.name}} |
+
+
+ {{model.allocation.node.address}}:{{row.model.port}}
+
+ |
+
+ {{/t.body}}
+ {{/list-table}}
+
+
+ {{/if}}
+
+
+
+ Recent Events
+
+
+ {{#list-table source=(reverse model.events) class="is-striped task-events" as |t|}}
+ {{#t.head}}
+
Time |
+ Type |
+ Description |
+ {{/t.head}}
+ {{#t.body as |row|}}
+
+ {{moment-format row.model.time "MM/DD/YY HH:mm:ss"}} |
+ {{row.model.type}} |
+
+ {{#if row.model.displayMessage}}
+ {{row.model.displayMessage}}
+ {{else}}
+ No message
+ {{/if}}
+ |
+
+ {{/t.body}}
+ {{/list-table}}
+
+
+
+{{/gutter-menu}}
diff --git a/ui/app/templates/allocations/allocation/task/subnav.hbs b/ui/app/templates/allocations/allocation/task/subnav.hbs
new file mode 100644
index 00000000000..fec8ff349fe
--- /dev/null
+++ b/ui/app/templates/allocations/allocation/task/subnav.hbs
@@ -0,0 +1,5 @@
+
+
+ - {{#link-to "allocations.allocation.task.index" model.allocation model activeClass="is-active"}}Overview{{/link-to}}
+
+
diff --git a/ui/mirage/common.js b/ui/mirage/common.js
index 5bc38ac244b..c9c5de51752 100644
--- a/ui/mirage/common.js
+++ b/ui/mirage/common.js
@@ -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)
diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js
index e4bcad24c88..1a19e4637ab 100644
--- a/ui/mirage/factories/allocation.js
+++ b/ui/mirage/factories/allocation.js
@@ -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',
diff --git a/ui/mirage/factories/task-resources.js b/ui/mirage/factories/task-resources.js
index e6fe87de85e..782988bcda6 100644
--- a/ui/mirage/factories/task-resources.js
+++ b/ui/mirage/factories/task-resources.js
@@ -9,4 +9,8 @@ export default Factory.extend({
withReservedPorts: trait({
resources: () => generateResources({ networks: { minPorts: 1 } }),
}),
+
+ withoutReservedPorts: trait({
+ resources: () => generateResources({ networks: { minPorts: 0, maxPorts: 0 } }),
+ }),
});
diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js
index 213ed47d2a4..1f0260b6952 100644
--- a/ui/tests/acceptance/allocation-detail-test.js
+++ b/ui/tests/acceptance/allocation-detail-test.js
@@ -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(
diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js
new file mode 100644
index 00000000000..1d031cbec55
--- /dev/null
+++ b/ui/tests/acceptance/task-detail-test.js
@@ -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');
+});