Merge pull request #4529 from hashicorp/f-ui-job-overview-allocs

UI: Recent allocs + job allocs view
This commit is contained in:
Michael Lange 2018-08-06 11:21:09 -07:00 committed by GitHub
commit 4f60b2dc6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 616 additions and 111 deletions

View file

@ -10,6 +10,9 @@ module.exports = {
parserOptions: { parserOptions: {
ecmaVersion: 2017, ecmaVersion: 2017,
sourceType: 'module', sourceType: 'module',
ecmaFeatures: {
experimentalObjectRestSpread: true,
},
}, },
rules: { rules: {
indent: ['error', 2, { SwitchCase: 1 }], indent: ['error', 2, { SwitchCase: 1 }],

View file

@ -0,0 +1,24 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import PromiseArray from 'nomad-ui/utils/classes/promise-array';
export default Component.extend({
sortProperty: 'modifyIndex',
sortDescending: true,
sortedAllocations: computed('job.allocations.@each.modifyIndex', function() {
return new PromiseArray({
promise: this.get('job.allocations').then(allocations =>
allocations
.sortBy('modifyIndex')
.reverse()
.slice(0, 5)
),
});
}),
actions: {
gotoAllocation(allocation) {
this.transitionToRoute('allocations.allocation', allocation);
},
},
});

View file

@ -0,0 +1,39 @@
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import Sortable from 'nomad-ui/mixins/sortable';
import Searchable from 'nomad-ui/mixins/searchable';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
export default Controller.extend(Sortable, Searchable, WithNamespaceResetting, {
queryParams: {
currentPage: 'page',
searchTerm: 'search',
sortProperty: 'sort',
sortDescending: 'desc',
},
currentPage: 1,
pageSize: 25,
sortProperty: 'modifyIndex',
sortDescending: true,
job: alias('model'),
searchProps: computed(() => ['shortId', 'name', 'taskGroupName']),
allocations: computed('model.allocations.[]', function() {
return this.get('model.allocations') || [];
}),
listToSort: alias('allocations'),
listToSearch: alias('listSorted'),
sortedAllocations: alias('listSearched'),
actions: {
gotoAllocation(allocation) {
this.transitionToRoute('allocations.allocation', allocation);
},
},
});

View file

@ -25,9 +25,13 @@ export default Model.extend({
name: attr('string'), name: attr('string'),
taskGroupName: attr('string'), taskGroupName: attr('string'),
resources: fragment('resources'), resources: fragment('resources'),
jobVersion: attr('number'),
modifyIndex: attr('number'), modifyIndex: attr('number'),
modifyTime: attr('date'), modifyTime: attr('date'),
jobVersion: attr('number'),
createIndex: attr('number'),
createTime: attr('date'),
clientStatus: attr('string'), clientStatus: attr('string'),
desiredStatus: attr('string'), desiredStatus: attr('string'),

View file

@ -14,6 +14,7 @@ Router.map(function() {
this.route('versions'); this.route('versions');
this.route('deployments'); this.route('deployments');
this.route('evaluations'); this.route('evaluations');
this.route('allocations');
}); });
}); });

View file

@ -0,0 +1,19 @@
import Route from '@ember/routing/route';
import { collect } from '@ember/object/computed';
import { watchRelationship } from 'nomad-ui/utils/properties/watch';
import WithWatchers from 'nomad-ui/mixins/with-watchers';
export default Route.extend(WithWatchers, {
model() {
const job = this.modelFor('jobs.job');
return job.get('allocations').then(() => job);
},
startWatchers(controller, model) {
controller.set('watchAllocations', this.get('watchAllocations').perform(model));
},
watchAllocations: watchRelationship('allocations'),
watchers: collect('watchAllocations'),
});

View file

@ -11,6 +11,7 @@ export default Route.extend(WithWatchers, {
controller.set('watchers', { controller.set('watchers', {
model: this.get('watch').perform(model), model: this.get('watch').perform(model),
summary: this.get('watchSummary').perform(model.get('summary')), summary: this.get('watchSummary').perform(model.get('summary')),
allocations: this.get('watchAllocations').perform(model),
evaluations: this.get('watchEvaluations').perform(model), evaluations: this.get('watchEvaluations').perform(model),
latestDeployment: latestDeployment:
model.get('supportsDeployments') && this.get('watchLatestDeployment').perform(model), model.get('supportsDeployments') && this.get('watchLatestDeployment').perform(model),
@ -21,6 +22,7 @@ export default Route.extend(WithWatchers, {
watch: watchRecord('job'), watch: watchRecord('job'),
watchAll: watchAll('job'), watchAll: watchAll('job'),
watchSummary: watchRecord('job-summary'), watchSummary: watchRecord('job-summary'),
watchAllocations: watchRelationship('allocations'),
watchEvaluations: watchRelationship('evaluations'), watchEvaluations: watchRelationship('evaluations'),
watchLatestDeployment: watchRelationship('latestDeployment'), watchLatestDeployment: watchRelationship('latestDeployment'),
@ -28,6 +30,7 @@ export default Route.extend(WithWatchers, {
'watch', 'watch',
'watchAll', 'watchAll',
'watchSummary', 'watchSummary',
'watchAllocations',
'watchEvaluations', 'watchEvaluations',
'watchLatestDeployment' 'watchLatestDeployment'
), ),

View file

@ -34,6 +34,9 @@ export default ApplicationSerializer.extend({
hash.ModifyTimeNanos = hash.ModifyTime % 1000000; hash.ModifyTimeNanos = hash.ModifyTime % 1000000;
hash.ModifyTime = Math.floor(hash.ModifyTime / 1000000); hash.ModifyTime = Math.floor(hash.ModifyTime / 1000000);
hash.CreateTimeNanos = hash.CreateTime % 1000000;
hash.CreateTime = Math.floor(hash.CreateTime / 1000000);
hash.RescheduleEvents = (hash.RescheduleTracker || {}).Events; hash.RescheduleEvents = (hash.RescheduleTracker || {}).Events;
// API returns empty strings instead of null // API returns empty strings instead of null

View file

@ -1,11 +1,15 @@
@import "./charts/distribution-bar"; @import './charts/distribution-bar';
@import "./charts/tooltip"; @import './charts/tooltip';
@import "./charts/colors"; @import './charts/colors';
.inline-chart { .inline-chart {
height: 1.5rem; height: 1.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
&.is-small {
width: 50px;
}
} }
// Patterns are templates referenced by other SVG fill properties. // Patterns are templates referenced by other SVG fill properties.

View file

@ -100,7 +100,7 @@
<th class="is-narrow"></th> <th class="is-narrow"></th>
{{#t.sort-by prop="shortId"}}ID{{/t.sort-by}} {{#t.sort-by prop="shortId"}}ID{{/t.sort-by}}
{{#t.sort-by prop="modifyIndex" title="Modify Index"}}Modified{{/t.sort-by}} {{#t.sort-by prop="modifyIndex" title="Modify Index"}}Modified{{/t.sort-by}}
{{#t.sort-by prop="name"}}Name{{/t.sort-by}} {{#t.sort-by prop="createIndex" title="Create Index"}}Created{{/t.sort-by}}
{{#t.sort-by prop="statusIndex"}}Status{{/t.sort-by}} {{#t.sort-by prop="statusIndex"}}Status{{/t.sort-by}}
{{#t.sort-by prop="job.name"}}Job{{/t.sort-by}} {{#t.sort-by prop="job.name"}}Job{{/t.sort-by}}
{{#t.sort-by prop="jobVersion"}}Version{{/t.sort-by}} {{#t.sort-by prop="jobVersion"}}Version{{/t.sort-by}}

View file

@ -15,12 +15,19 @@
{{allocation.shortId}} {{allocation.shortId}}
{{/link-to}} {{/link-to}}
</td> </td>
<td data-test-modify-time>{{moment-format allocation.modifyTime "MM/DD HH:mm:ss"}}</td> {{#if (eq context "job")}}
<td data-test-name>{{allocation.name}}</td> <td data-test-task-group>
{{#link-to "jobs.job.task-group" allocation.job allocation.taskGroupName (query-params jobNamespace=allocation.job.namespace.id)}}
{{allocation.taskGroupName}}
{{/link-to}}
</td>
{{/if}}
<td data-test-create-time>{{moment-format allocation.createTime "MM/DD HH:mm:ss"}}</td>
<td data-test-modify-time>{{moment-from-now allocation.modifyTime}}</td>
<td data-test-client-status class="is-one-line"> <td data-test-client-status class="is-one-line">
<span class="color-swatch {{allocation.clientStatus}}" /> {{allocation.clientStatus}} <span class="color-swatch {{allocation.clientStatus}}" /> {{allocation.clientStatus}}
</td> </td>
{{#if (eq context "job")}} {{#if (or (eq context "taskGroup") (eq context "job"))}}
<td data-test-job-version>{{allocation.jobVersion}}</td> <td data-test-job-version>{{allocation.jobVersion}}</td>
<td data-test-client>{{#link-to "clients.client" allocation.node}}{{allocation.node.shortId}}{{/link-to}}</td> <td data-test-client>{{#link-to "clients.client" allocation.node}}{{allocation.node.shortId}}{{/link-to}}</td>
{{else if (eq context "node")}} {{else if (eq context "node")}}
@ -32,9 +39,9 @@
<span class="is-faded" data-test-task-group>/ {{allocation.taskGroup.name}}</span> <span class="is-faded" data-test-task-group>/ {{allocation.taskGroup.name}}</span>
{{/if}} {{/if}}
</td> </td>
<td data-test-job-version>{{allocation.jobVersion}}</td> <td data-test-job-version class="is-1">{{allocation.jobVersion}}</td>
{{/if}} {{/if}}
<td data-test-cpu class="has-text-centered"> <td data-test-cpu class="is-1 has-text-centered">
{{#if (and (not stats) fetchStats.isRunning)}} {{#if (and (not stats) fetchStats.isRunning)}}
... ...
{{else if (not allocation)}} {{else if (not allocation)}}
@ -44,7 +51,7 @@
{{x-icon "warning" class="is-warning"}} {{x-icon "warning" class="is-warning"}}
</span> </span>
{{else}} {{else}}
<div class="inline-chart tooltip" aria-label="{{stats.cpuUsed}} / {{stats.reservedCPU}} MHz"> <div class="inline-chart is-small tooltip" aria-label="{{stats.cpuUsed}} / {{stats.reservedCPU}} MHz">
<progress <progress
class="progress is-info is-small" class="progress is-info is-small"
value="{{stats.percentCPU}}" value="{{stats.percentCPU}}"
@ -54,13 +61,13 @@
</div> </div>
{{/if}} {{/if}}
</td> </td>
<td data-test-mem class="has-text-centered"> <td data-test-mem class="is-1 has-text-centered">
{{#if (and (not stats) fetchStats.isRunning)}} {{#if (and (not stats) fetchStats.isRunning)}}
... ...
{{else if (not allocation)}} {{else if (not allocation)}}
{{! nothing when there's no allocation}} {{! nothing when there's no allocation}}
{{else if statsError}} {{else if statsError}}
<span class="tooltip text-center" aria-label="Couldn't collect stats"> <span class="tooltip is-small text-center" aria-label="Couldn't collect stats">
{{x-icon "warning" class="is-warning"}} {{x-icon "warning" class="is-warning"}}
</span> </span>
{{else}} {{else}}

View file

@ -9,8 +9,9 @@
{{#t.head}} {{#t.head}}
<th class="is-narrow"></th> <th class="is-narrow"></th>
<th>ID</th> <th>ID</th>
<th>Task Group</th>
<th>Created</th>
<th>Modified</th> <th>Modified</th>
<th>Name</th>
<th>Status</th> <th>Status</th>
<th>Version</th> <th>Version</th>
<th>Node</th> <th>Node</th>

View file

@ -22,4 +22,6 @@
sortProperty=sortProperty sortProperty=sortProperty
sortDescending=sortDescending sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}} gotoTaskGroup=gotoTaskGroup}}
{{job-page/parts/recent-allocations job=job}}
{{/job-page/parts/body}} {{/job-page/parts/body}}

View file

@ -29,6 +29,8 @@
sortDescending=sortDescending sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}} gotoTaskGroup=gotoTaskGroup}}
{{job-page/parts/recent-allocations job=job}}
<div class="boxed-section"> <div class="boxed-section">
<div class="boxed-section-head">Payload</div> <div class="boxed-section-head">Payload</div>
<div class="boxed-section-body is-dark"> <div class="boxed-section-body is-dark">

View file

@ -0,0 +1,46 @@
<div class="boxed-section">
<div class="boxed-section-head">
Recent Allocations
</div>
<div class="boxed-section-body is-full-bleed">
{{#if job.allocations.length}}
{{#list-table
source=sortedAllocations
sortProperty=sortProperty
sortDescending=sortDescending
class="with-foot" as |t|}}
{{#t.head}}
<th class="is-narrow"></th>
<th>ID</th>
<th>Task Group</th>
<th>Created</th>
<th>Modified</th>
<th>Status</th>
<th>Version</th>
<th>Client</th>
<th>CPU</th>
<th>Memory</th>
{{/t.head}}
{{#t.body as |row|}}
{{allocation-row
data-test-allocation=row.model.id
allocation=row.model
context="job"
onClick=(action "gotoAllocation" row.model)}}
{{/t.body}}
{{/list-table}}
{{else}}
<div class="empty-message" data-test-empty-recent-allocations>
<h3 class="empty-message-headline" data-test-empty-recent-allocations-headline>No Allocations</h3>
<p class="empty-message-body" data-test-empty-recent-allocations-message>No allocations have been placed.</p>
</div>
{{/if}}
</div>
{{#if job.allocations.length}}
<div class="boxed-section-foot">
<p class="pull-right" data-test-view-all-allocations>{{#link-to "jobs.job.allocations" job}}
View all {{job.allocations.length}} {{pluralize "allocation" job.allocations.length}}
{{/link-to}}</p>
</div>
{{/if}}
</div>

View file

@ -28,4 +28,6 @@
sortProperty=sortProperty sortProperty=sortProperty
sortDescending=sortDescending sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}} gotoTaskGroup=gotoTaskGroup}}
{{job-page/parts/recent-allocations job=job}}
{{/job-page/parts/body}} {{/job-page/parts/body}}

View file

@ -24,4 +24,6 @@
sortProperty=sortProperty sortProperty=sortProperty
sortDescending=sortDescending sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}} gotoTaskGroup=gotoTaskGroup}}
{{job-page/parts/recent-allocations job=job}}
{{/job-page/parts/body}} {{/job-page/parts/body}}

View file

@ -22,4 +22,6 @@
sortProperty=sortProperty sortProperty=sortProperty
sortDescending=sortDescending sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}} gotoTaskGroup=gotoTaskGroup}}
{{job-page/parts/recent-allocations job=job}}
{{/job-page/parts/body}} {{/job-page/parts/body}}

View file

@ -0,0 +1,71 @@
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
{{partial "jobs/job/subnav"}}
<section class="section">
{{#if allocations.length}}
<div class="content">
<div>
{{search-box
data-test-allocations-search
searchTerm=(mut searchTerm)
placeholder="Search allocations..."}}
</div>
</div>
{{#list-pagination
source=sortedAllocations
size=pageSize
page=currentPage
class="allocations" as |p|}}
{{#list-table
source=p.list
sortProperty=sortProperty
sortDescending=sortDescending
class="with-foot" as |t|}}
{{#t.head}}
<th class="is-narrow"></th>
{{#t.sort-by prop="shortId"}}ID{{/t.sort-by}}
{{#t.sort-by prop="taskGroupName"}}Task Group{{/t.sort-by}}
{{#t.sort-by prop="createIndex" title="Create Index"}}Created{{/t.sort-by}}
{{#t.sort-by prop="modifyIndex" title="Modify Index"}}Modified{{/t.sort-by}}
{{#t.sort-by prop="statusIndex"}}Status{{/t.sort-by}}
{{#t.sort-by prop="jobVersion"}}Version{{/t.sort-by}}
{{#t.sort-by prop="node.shortId"}}Client{{/t.sort-by}}
<th>CPU</th>
<th>Memory</th>
{{/t.head}}
{{#t.body as |row|}}
{{allocation-row
data-test-allocation=row.model.id
allocation=row.model
context="job"
onClick=(action "gotoAllocation" row.model)}}
{{/t.body}}
{{/list-table}}
<div class="table-foot">
<nav class="pagination">
<div class="pagination-numbers">
{{p.startsAt}}&ndash;{{p.endsAt}} of {{sortedAllocations.length}}
</div>
{{#p.prev class="pagination-previous"}} &lt; {{/p.prev}}
{{#p.next class="pagination-next"}} &gt; {{/p.next}}
<ul class="pagination-list"></ul>
</nav>
</div>
{{else}}
<div class="boxed-section-body">
<div class="empty-message" data-test-empty-allocations-list>
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>No Matches</h3>
<p class="empty-message-body">No allocations match the term <strong>{{searchTerm}}</strong></p>
</div>
</div>
{{/list-pagination}}
{{else}}
<div class="boxed-section-body">
<div class="empty-message" data-test-empty-allocations-list>
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>No Allocations</h3>
<p class="empty-message-body">No allocations have been placed.</p>
</div>
</div>
{{/if}}
</section>
{{/gutter-menu}}

View file

@ -1,10 +1,5 @@
{{partial "jobs/job/subnav"}} {{partial "jobs/job/subnav"}}
<section class="section"> <section class="section">
<div class="boxed-section">
<div class="boxed-section-head">
Evaluations
</div>
<div class="boxed-section-body {{if sortedEvaluations.length "is-full-bleed"}}">
{{#if sortedEvaluations.length}} {{#if sortedEvaluations.length}}
{{#list-table {{#list-table
source=sortedEvaluations source=sortedEvaluations
@ -41,6 +36,4 @@
<p class="empty-message-body">This is most likely due to garbage collection.</p> <p class="empty-message-body">This is most likely due to garbage collection.</p>
</div> </div>
{{/if}} {{/if}}
</div>
</div>
</section> </section>

View file

@ -6,6 +6,7 @@
{{#if job.supportsDeployments}} {{#if job.supportsDeployments}}
<li data-test-tab="deployments">{{#link-to "jobs.job.deployments" job activeClass="is-active"}}Deployments{{/link-to}}</li> <li data-test-tab="deployments">{{#link-to "jobs.job.deployments" job activeClass="is-active"}}Deployments{{/link-to}}</li>
{{/if}} {{/if}}
<li data-test-tab="allocations">{{#link-to "jobs.job.allocations" job activeClass="is-active"}}Allocations{{/link-to}}</li>
<li data-test-tab="evaluations">{{#link-to "jobs.job.evaluations" job activeClass="is-active"}}Evaluations{{/link-to}}</li> <li data-test-tab="evaluations">{{#link-to "jobs.job.evaluations" job activeClass="is-active"}}Evaluations{{/link-to}}</li>
</ul> </ul>
</div> </div>

View file

@ -63,8 +63,8 @@
{{#t.head}} {{#t.head}}
<th class="is-narrow"></th> <th class="is-narrow"></th>
{{#t.sort-by prop="shortId"}}ID{{/t.sort-by}} {{#t.sort-by prop="shortId"}}ID{{/t.sort-by}}
{{#t.sort-by prop="createIndex" title="Create Index"}}Created{{/t.sort-by}}
{{#t.sort-by prop="modifyIndex" title="Modify Index"}}Modified{{/t.sort-by}} {{#t.sort-by prop="modifyIndex" title="Modify Index"}}Modified{{/t.sort-by}}
{{#t.sort-by prop="name"}}Name{{/t.sort-by}}
{{#t.sort-by prop="statusIndex"}}Status{{/t.sort-by}} {{#t.sort-by prop="statusIndex"}}Status{{/t.sort-by}}
{{#t.sort-by prop="jobVersion"}}Version{{/t.sort-by}} {{#t.sort-by prop="jobVersion"}}Version{{/t.sort-by}}
{{#t.sort-by prop="node.shortId"}}Client{{/t.sort-by}} {{#t.sort-by prop="node.shortId"}}Client{{/t.sort-by}}
@ -72,7 +72,7 @@
<th>Memory</th> <th>Memory</th>
{{/t.head}} {{/t.head}}
{{#t.body as |row|}} {{#t.body as |row|}}
{{allocation-row data-test-allocation=row.model.id allocation=row.model context="job" onClick=(action "gotoAllocation" row.model)}} {{allocation-row data-test-allocation=row.model.id allocation=row.model context="taskGroup" onClick=(action "gotoAllocation" row.model)}}
{{/t.body}} {{/t.body}}
{{/list-table}} {{/list-table}}
<div class="table-foot"> <div class="table-foot">

View file

@ -0,0 +1,4 @@
import ArrayProxy from '@ember/array/proxy';
import PromiseProxyMixin from '@ember/object/promise-proxy-mixin';
export default ArrayProxy.extend(PromiseProxyMixin);

View file

@ -19,6 +19,9 @@ module.exports = function(defaults) {
`${defaults.project.pkg.name}/templates/components/freestyle/**/*`, `${defaults.project.pkg.name}/templates/components/freestyle/**/*`,
], ],
}, },
babel: {
plugins: ['transform-object-rest-spread'],
},
}); });
// Use `app.import` to add additional libraries to the generated // Use `app.import` to add additional libraries to the generated

View file

@ -11,11 +11,16 @@ const REF_TIME = new Date();
export default Factory.extend({ export default Factory.extend({
id: i => (i >= 100 ? `${UUIDS[i % 100]}-${i}` : UUIDS[i]), id: i => (i >= 100 ? `${UUIDS[i % 100]}-${i}` : UUIDS[i]),
modifyIndex: () => faker.random.number({ min: 10, max: 2000 }),
jobVersion: () => faker.random.number(10), jobVersion: () => faker.random.number(10),
modifyIndex: () => faker.random.number({ min: 10, max: 2000 }),
modifyTime: () => faker.date.past(2 / 365, REF_TIME) * 1000000, modifyTime: () => faker.date.past(2 / 365, REF_TIME) * 1000000,
createIndex: () => faker.random.number({ min: 10, max: 2000 }),
createTime() {
return faker.date.past(2 / 365, new Date(this.modifyTime / 1000000)) * 1000000;
},
namespace: null, namespace: null,
clientStatus: faker.list.random(...CLIENT_STATUSES), clientStatus: faker.list.random(...CLIENT_STATUSES),

View file

@ -24,6 +24,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"broccoli-asset-rev": "^2.4.5", "broccoli-asset-rev": "^2.4.5",
"bulma": "0.6.1", "bulma": "0.6.1",
"core-js": "^2.4.1", "core-js": "^2.4.1",

View file

@ -128,13 +128,17 @@ test('each allocation should have high-level details for the allocation', functi
andThen(() => { andThen(() => {
const allocationRow = ClientDetail.allocations.objectAt(0); const allocationRow = ClientDetail.allocations.objectAt(0);
assert.equal(allocationRow.id, allocation.id.split('-')[0], 'Allocation short ID'); assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short ID');
assert.equal(
allocationRow.createTime,
moment(allocation.createTime / 1000000).format('MM/DD HH:mm:ss'),
'Allocation create time'
);
assert.equal( assert.equal(
allocationRow.modifyTime, allocationRow.modifyTime,
moment(allocation.modifyTime / 1000000).format('MM/DD HH:mm:ss'), moment(allocation.modifyTime / 1000000).fromNow(),
'Allocation modify time' 'Allocation modify time'
); );
assert.equal(allocationRow.name, allocation.name, 'Allocation name');
assert.equal(allocationRow.status, allocation.clientStatus, 'Client status'); assert.equal(allocationRow.status, allocation.clientStatus, 'Client status');
assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name'); assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name');
assert.ok(allocationRow.taskGroup, 'Task group name'); assert.ok(allocationRow.taskGroup, 'Task group name');

View file

@ -0,0 +1,110 @@
import { test } from 'qunit';
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
import Allocations from 'nomad-ui/tests/pages/jobs/job/allocations';
let job;
let allocations;
const makeSearchAllocations = server => {
Array(10)
.fill(null)
.map((_, index) => {
server.create('allocation', {
id: index < 5 ? `ffffff-dddddd-${index}` : `111111-222222-${index}`,
});
});
};
moduleForAcceptance('Acceptance | job allocations', {
beforeEach() {
server.create('node');
job = server.create('job', { noFailedPlacements: true, createAllocations: false });
},
});
test('lists all allocations for the job', function(assert) {
server.createList('allocation', Allocations.pageSize - 1);
allocations = server.schema.allocations.where({ jobId: job.id }).models;
Allocations.visit({ id: job.id });
andThen(() => {
assert.equal(
Allocations.allocations.length,
Allocations.pageSize - 1,
'Allocations are shown in a table'
);
const sortedAllocations = allocations.sortBy('modifyIndex').reverse();
Allocations.allocations.forEach((allocation, index) => {
const shortId = sortedAllocations[index].id.split('-')[0];
assert.equal(allocation.shortId, shortId, `Allocation ${index} is ${shortId}`);
});
});
});
test('allocations table is sortable', function(assert) {
server.createList('allocation', Allocations.pageSize - 1);
allocations = server.schema.allocations.where({ jobId: job.id }).models;
Allocations.visit({ id: job.id });
andThen(() => {
Allocations.sortBy('taskGroupName');
andThen(() => {
assert.equal(
currentURL(),
`/jobs/${job.id}/allocations?sort=taskGroupName`,
'the URL persists the sort parameter'
);
const sortedAllocations = allocations.sortBy('taskGroup').reverse();
Allocations.allocations.forEach((allocation, index) => {
const shortId = sortedAllocations[index].id.split('-')[0];
assert.equal(
allocation.shortId,
shortId,
`Allocation ${index} is ${shortId} with task group ${sortedAllocations[index].taskGroup}`
);
});
});
});
});
test('allocations table is searchable', function(assert) {
makeSearchAllocations(server);
allocations = server.schema.allocations.where({ jobId: job.id }).models;
Allocations.visit({ id: job.id });
andThen(() => {
Allocations.search('ffffff');
});
andThen(() => {
assert.equal(Allocations.allocations.length, 5, 'List is filtered by search term');
});
});
test('when a search yields no results, the search box remains', function(assert) {
makeSearchAllocations(server);
allocations = server.schema.allocations.where({ jobId: job.id }).models;
Allocations.visit({ id: job.id });
andThen(() => {
Allocations.search('^nothing will ever match this long regex$');
});
andThen(() => {
assert.equal(
Allocations.emptyState.headline,
'No Matches',
'List is empty and the empty state is about search'
);
assert.ok(Allocations.hasSearchBox, 'Search box is still shown');
});
});

View file

@ -232,7 +232,7 @@ test('when open, a deployment shows a list of all allocations for the deployment
const allocation = allocations[0]; const allocation = allocations[0];
const allocationRow = deploymentRow.allocations.objectAt(0); const allocationRow = deploymentRow.allocations.objectAt(0);
assert.equal(allocationRow.id, allocation.id.split('-')[0], 'Allocation is as expected'); assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation is as expected');
}); });
}); });
}); });

View file

@ -143,12 +143,16 @@ test('each allocation should show basic information about the allocation', funct
andThen(() => { andThen(() => {
assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short id'); assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short id');
assert.equal(
allocationRow.createTime,
moment(allocation.createTime / 1000000).format('MM/DD HH:mm:ss'),
'Allocation create time'
);
assert.equal( assert.equal(
allocationRow.modifyTime, allocationRow.modifyTime,
moment(allocation.modifyTime / 1000000).format('MM/DD HH:mm:ss'), moment(allocation.modifyTime / 1000000).fromNow(),
'Allocation modify time' 'Allocation modify time'
); );
assert.equal(allocationRow.name, allocation.name, 'Allocation name');
assert.equal(allocationRow.status, allocation.clientStatus, 'Client status'); assert.equal(allocationRow.status, allocation.clientStatus, 'Client status');
assert.equal(allocationRow.jobVersion, allocation.jobVersion, 'Job Version'); assert.equal(allocationRow.jobVersion, allocation.jobVersion, 'Job Version');
assert.equal( assert.equal(

View file

@ -1,19 +1,23 @@
import { getOwner } from '@ember/application'; import { getOwner } from '@ember/application';
import { assign } from '@ember/polyfills';
import { test, moduleForComponent } from 'ember-qunit'; import { test, moduleForComponent } from 'ember-qunit';
import wait from 'ember-test-helpers/wait'; import wait from 'ember-test-helpers/wait';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
import { stopJob, expectStopError, expectDeleteRequest } from './helpers'; import { stopJob, expectStopError, expectDeleteRequest } from './helpers';
import Job from 'nomad-ui/tests/pages/jobs/detail';
moduleForComponent('job-page/service', 'Integration | Component | job-page/service', { moduleForComponent('job-page/service', 'Integration | Component | job-page/service', {
integration: true, integration: true,
beforeEach() { beforeEach() {
Job.setContext(this);
window.localStorage.clear(); window.localStorage.clear();
this.store = getOwner(this).lookup('service:store'); this.store = getOwner(this).lookup('service:store');
this.server = startMirage(); this.server = startMirage();
this.server.create('namespace'); this.server.create('namespace');
}, },
afterEach() { afterEach() {
Job.removeContext();
this.server.shutdown(); this.server.shutdown();
window.localStorage.clear(); window.localStorage.clear();
}, },
@ -36,12 +40,18 @@ const commonProperties = job => ({
gotoJob() {}, gotoJob() {},
}); });
const makeMirageJob = server => const makeMirageJob = (server, props = {}) =>
server.create('job', { server.create(
'job',
assign(
{
type: 'service', type: 'service',
createAllocations: false, createAllocations: false,
status: 'running', status: 'running',
}); },
props
)
);
test('Stopping a job sends a delete request for the job', function(assert) { test('Stopping a job sends a delete request for the job', function(assert) {
let job; let job;
@ -80,3 +90,78 @@ test('Stopping a job without proper permissions shows an error message', functio
.then(stopJob) .then(stopJob)
.then(expectStopError(assert)); .then(expectStopError(assert));
}); });
test('Recent allocations shows allocations in the job context', function(assert) {
let job;
this.server.create('node');
const mirageJob = makeMirageJob(this.server, { createAllocations: true });
this.store.findAll('job');
return wait()
.then(() => {
job = this.store.peekAll('job').findBy('plainId', mirageJob.id);
this.setProperties(commonProperties(job));
this.render(commonTemplate);
return wait();
})
.then(() => {
const allocation = this.server.db.allocations.sortBy('modifyIndex').reverse()[0];
const allocationRow = Job.allocations.objectAt(0);
assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'ID');
assert.equal(allocationRow.taskGroup, allocation.taskGroup, 'Task Group name');
});
});
test('Recent allocations caps out at five', function(assert) {
let job;
this.server.create('node');
const mirageJob = makeMirageJob(this.server);
this.server.createList('allocation', 10);
this.store.findAll('job');
return wait().then(() => {
job = this.store.peekAll('job').findBy('plainId', mirageJob.id);
this.setProperties(commonProperties(job));
this.render(commonTemplate);
return wait().then(() => {
assert.equal(Job.allocations.length, 5, 'Capped at 5 allocations');
assert.ok(
Job.viewAllAllocations.includes(job.get('allocations.length') + ''),
`View link mentions ${job.get('allocations.length')} allocations`
);
});
});
});
test('Recent allocations shows an empty message when the job has no allocations', function(assert) {
let job;
this.server.create('node');
const mirageJob = makeMirageJob(this.server);
this.store.findAll('job');
return wait()
.then(() => {
job = this.store.peekAll('job').findBy('plainId', mirageJob.id);
this.setProperties(commonProperties(job));
this.render(commonTemplate);
return wait();
})
.then(() => {
assert.ok(
Job.recentAllocationsEmptyState.headline.includes('No Allocations'),
'No allocations empty message'
);
});
});

View file

@ -9,6 +9,8 @@ import {
visitable, visitable,
} from 'ember-cli-page-object'; } from 'ember-cli-page-object';
import allocations from 'nomad-ui/tests/pages/components/allocations';
export default create({ export default create({
visit: visitable('/clients/:id'), visit: visitable('/clients/:id'),
@ -36,22 +38,7 @@ export default create({
eligibilityDefinition: text('[data-test-eligibility]'), eligibilityDefinition: text('[data-test-eligibility]'),
datacenterDefinition: text('[data-test-datacenter-definition]'), datacenterDefinition: text('[data-test-datacenter-definition]'),
allocations: collection('[data-test-allocation]', { ...allocations(),
id: text('[data-test-short-id]'),
modifyTime: text('[data-test-modify-time]'),
name: text('[data-test-name]'),
status: text('[data-test-client-status]'),
job: text('[data-test-job]'),
taskGroup: text('[data-test-task-group]'),
jobVersion: text('[data-test-job-version]'),
cpu: text('[data-test-cpu]'),
cpuTooltip: attribute('aria-label', '[data-test-cpu] .tooltip'),
mem: text('[data-test-mem]'),
memTooltip: attribute('aria-label', '[data-test-mem] .tooltip'),
visit: clickable('[data-test-short-id] a'),
visitJob: clickable('[data-test-job]'),
}),
attributesTable: isPresent('[data-test-attributes]'), attributesTable: isPresent('[data-test-attributes]'),
metaTable: isPresent('[data-test-meta]'), metaTable: isPresent('[data-test-meta]'),

View file

@ -0,0 +1,30 @@
import { attribute, collection, clickable, isPresent, text } from 'ember-cli-page-object';
export default function(selector = '[data-test-allocation]') {
return {
allocations: collection(selector, {
id: attribute('data-test-allocation'),
shortId: text('[data-test-short-id]'),
createTime: text('[data-test-create-time]'),
modifyTime: text('[data-test-modify-time]'),
status: text('[data-test-client-status]'),
job: text('[data-test-job]'),
taskGroup: text('[data-test-task-group]'),
client: text('[data-test-client]'),
jobVersion: text('[data-test-job-version]'),
cpu: text('[data-test-cpu]'),
cpuTooltip: attribute('aria-label', '[data-test-cpu] .tooltip'),
mem: text('[data-test-mem]'),
memTooltip: attribute('aria-label', '[data-test-mem] .tooltip'),
rescheduled: isPresent('[data-test-indicators] [data-test-icon="reschedule"]'),
visit: clickable('[data-test-short-id] a'),
visitJob: clickable('[data-test-job]'),
visitClient: clickable('[data-test-client] a'),
}),
allocationFor(id) {
return this.allocations.toArray().find(allocation => allocation.id === id);
},
};
}

View file

@ -8,6 +8,8 @@ import {
visitable, visitable,
} from 'ember-cli-page-object'; } from 'ember-cli-page-object';
import allocations from 'nomad-ui/tests/pages/components/allocations';
export default create({ export default create({
visit: visitable('/jobs/:id'), visit: visitable('/jobs/:id'),
@ -29,10 +31,18 @@ export default create({
return this.stats.toArray().findBy('id', id); return this.stats.toArray().findBy('id', id);
}, },
...allocations(),
viewAllAllocations: text('[data-test-view-all-allocations]'),
error: { error: {
isPresent: isPresent('[data-test-error]'), isPresent: isPresent('[data-test-error]'),
title: text('[data-test-error-title]'), title: text('[data-test-error-title]'),
message: text('[data-test-error-message]'), message: text('[data-test-error-message]'),
seekHelp: clickable('[data-test-error-message] a'), seekHelp: clickable('[data-test-error-message] a'),
}, },
recentAllocationsEmptyState: {
headline: text('[data-test-empty-recent-allocations-headline]'),
},
}); });

View file

@ -0,0 +1,40 @@
import {
attribute,
clickable,
create,
collection,
fillable,
isPresent,
text,
visitable,
} from 'ember-cli-page-object';
import allocations from 'nomad-ui/tests/pages/components/allocations';
export default create({
visit: visitable('/jobs/:id/allocations'),
pageSize: 25,
hasSearchBox: isPresent('[data-test-allocations-search]'),
search: fillable('[data-test-allocations-search] input'),
...allocations(),
isEmpty: isPresent('[data-test-empty-allocations-list]'),
emptyState: {
headline: text('[data-test-empty-allocations-list-headline]'),
},
sortOptions: collection('[data-test-sort-by]', {
id: attribute('data-test-sort-by'),
sort: clickable(),
}),
sortBy(id) {
return this.sortOptions
.toArray()
.findBy('id', id)
.sort();
},
});

View file

@ -8,6 +8,8 @@ import {
visitable, visitable,
} from 'ember-cli-page-object'; } from 'ember-cli-page-object';
import allocations from 'nomad-ui/tests/pages/components/allocations';
export default create({ export default create({
visit: visitable('/jobs/:id/deployments'), visit: visitable('/jobs/:id/deployments'),
@ -46,9 +48,7 @@ export default create({
progress: text('[data-test-deployment-task-group-progress-deadline]'), progress: text('[data-test-deployment-task-group-progress-deadline]'),
}), }),
...allocations('[data-test-deployment-allocation]'),
hasAllocations: isPresent('[data-test-deployment-allocations]'), hasAllocations: isPresent('[data-test-deployment-allocations]'),
allocations: collection('[data-test-deployment-allocation]', {
id: text('[data-test-short-id]'),
}),
}), }),
}); });

View file

@ -9,6 +9,8 @@ import {
visitable, visitable,
} from 'ember-cli-page-object'; } from 'ember-cli-page-object';
import allocations from 'nomad-ui/tests/pages/components/allocations';
export default create({ export default create({
pageSize: 10, pageSize: 10,
@ -31,27 +33,7 @@ export default create({
return this.breadcrumbs.toArray().find(crumb => crumb.id === id); return this.breadcrumbs.toArray().find(crumb => crumb.id === id);
}, },
allocations: collection('[data-test-allocation]', { ...allocations(),
id: attribute('data-test-allocation'),
shortId: text('[data-test-short-id]'),
modifyTime: text('[data-test-modify-time]'),
name: text('[data-test-name]'),
status: text('[data-test-client-status]'),
jobVersion: text('[data-test-job-version]'),
client: text('[data-test-client]'),
cpu: text('[data-test-cpu]'),
cpuTooltip: attribute('aria-label', '[data-test-cpu] .tooltip'),
mem: text('[data-test-mem]'),
memTooltip: attribute('aria-label', '[data-test-mem] .tooltip'),
rescheduled: isPresent('[data-test-indicators] [data-test-icon="reschedule"]'),
visit: clickable('[data-test-short-id] a'),
visitClient: clickable('[data-test-client] a'),
}),
allocationFor(id) {
return this.allocations.toArray().find(allocation => allocation.id === id);
},
isEmpty: isPresent('[data-test-empty-allocations-list]'), isEmpty: isPresent('[data-test-empty-allocations-list]'),

View file

@ -854,6 +854,10 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0:
version "6.13.0" version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
babel-plugin-syntax-trailing-function-commas@^6.22.0: babel-plugin-syntax-trailing-function-commas@^6.22.0:
version "6.22.0" version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
@ -1042,6 +1046,13 @@ babel-plugin-transform-exponentiation-operator@^6.22.0:
babel-plugin-syntax-exponentiation-operator "^6.8.0" babel-plugin-syntax-exponentiation-operator "^6.8.0"
babel-runtime "^6.22.0" babel-runtime "^6.22.0"
babel-plugin-transform-object-rest-spread@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
dependencies:
babel-plugin-syntax-object-rest-spread "^6.8.0"
babel-runtime "^6.26.0"
babel-plugin-transform-regenerator@^6.22.0: babel-plugin-transform-regenerator@^6.22.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"