diff --git a/ui/app/controllers/allocations/allocation.js b/ui/app/controllers/allocations/allocation.js index 5a764f2a64b8..1ee88420b6ea 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 000000000000..5a764f2a64b8 --- /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 000000000000..669884d9d777 --- /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 810213bfee41..7d3a680a23ae 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 e67b0110b704..a5b3bff22ddb 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 367e311e2b8a..bddf8813da34 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 000000000000..4a57f1ad25cd --- /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 e7d5942e68ba..c3ec634a6b4e 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 dd7f31e0fe62..de4f25c74f94 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 ce0bdbbe5f44..8fab3547f4c7 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 5d9a537900a6..c24cd68950a9 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}} -

- -
-
- 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|}} - - {{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|}} -
-
- {{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}} -
- - - - - - - - - - {{#each (reverse state.events) as |event|}} - - - - - - {{/each}} - -
TimeTypeDescription
{{moment-format event.time "MM/DD/YY HH:mm:ss [UTC]"}}{{event.type}} - {{#if event.displayMessage}} - {{event.displayMessage}} - {{else}} - No message - {{/if}} -
-
- {{/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 000000000000..d114f3334001 --- /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 000000000000..c24cd68950a9 --- /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 000000000000..20dcf01ac4c5 --- /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 000000000000..fec8ff349fe9 --- /dev/null +++ b/ui/app/templates/allocations/allocation/task/subnav.hbs @@ -0,0 +1,5 @@ +
+ +
diff --git a/ui/mirage/common.js b/ui/mirage/common.js index 5bc38ac244b9..c9c5de517526 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 e4bcad24c88b..1a19e4637ab2 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 e6fe87de85e7..782988bcda61 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 213ed47d2a4c..1f0260b6952f 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 000000000000..1d031cbec551 --- /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'); +});