Merge pull request #4529 from hashicorp/f-ui-job-overview-allocs
UI: Recent allocs + job allocs view
This commit is contained in:
commit
4f60b2dc6c
|
@ -10,6 +10,9 @@ module.exports = {
|
|||
parserOptions: {
|
||||
ecmaVersion: 2017,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
experimentalObjectRestSpread: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
indent: ['error', 2, { SwitchCase: 1 }],
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -25,9 +25,13 @@ export default Model.extend({
|
|||
name: attr('string'),
|
||||
taskGroupName: attr('string'),
|
||||
resources: fragment('resources'),
|
||||
jobVersion: attr('number'),
|
||||
|
||||
modifyIndex: attr('number'),
|
||||
modifyTime: attr('date'),
|
||||
jobVersion: attr('number'),
|
||||
|
||||
createIndex: attr('number'),
|
||||
createTime: attr('date'),
|
||||
|
||||
clientStatus: attr('string'),
|
||||
desiredStatus: attr('string'),
|
||||
|
|
|
@ -14,6 +14,7 @@ Router.map(function() {
|
|||
this.route('versions');
|
||||
this.route('deployments');
|
||||
this.route('evaluations');
|
||||
this.route('allocations');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'),
|
||||
});
|
|
@ -11,6 +11,7 @@ export default Route.extend(WithWatchers, {
|
|||
controller.set('watchers', {
|
||||
model: this.get('watch').perform(model),
|
||||
summary: this.get('watchSummary').perform(model.get('summary')),
|
||||
allocations: this.get('watchAllocations').perform(model),
|
||||
evaluations: this.get('watchEvaluations').perform(model),
|
||||
latestDeployment:
|
||||
model.get('supportsDeployments') && this.get('watchLatestDeployment').perform(model),
|
||||
|
@ -21,6 +22,7 @@ export default Route.extend(WithWatchers, {
|
|||
watch: watchRecord('job'),
|
||||
watchAll: watchAll('job'),
|
||||
watchSummary: watchRecord('job-summary'),
|
||||
watchAllocations: watchRelationship('allocations'),
|
||||
watchEvaluations: watchRelationship('evaluations'),
|
||||
watchLatestDeployment: watchRelationship('latestDeployment'),
|
||||
|
||||
|
@ -28,6 +30,7 @@ export default Route.extend(WithWatchers, {
|
|||
'watch',
|
||||
'watchAll',
|
||||
'watchSummary',
|
||||
'watchAllocations',
|
||||
'watchEvaluations',
|
||||
'watchLatestDeployment'
|
||||
),
|
||||
|
|
|
@ -34,6 +34,9 @@ export default ApplicationSerializer.extend({
|
|||
hash.ModifyTimeNanos = 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;
|
||||
|
||||
// API returns empty strings instead of null
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
@import "./charts/distribution-bar";
|
||||
@import "./charts/tooltip";
|
||||
@import "./charts/colors";
|
||||
@import './charts/distribution-bar';
|
||||
@import './charts/tooltip';
|
||||
@import './charts/colors';
|
||||
|
||||
.inline-chart {
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.is-small {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
// Patterns are templates referenced by other SVG fill properties.
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
<th class="is-narrow"></th>
|
||||
{{#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="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="job.name"}}Job{{/t.sort-by}}
|
||||
{{#t.sort-by prop="jobVersion"}}Version{{/t.sort-by}}
|
||||
|
|
|
@ -15,12 +15,19 @@
|
|||
{{allocation.shortId}}
|
||||
{{/link-to}}
|
||||
</td>
|
||||
<td data-test-modify-time>{{moment-format allocation.modifyTime "MM/DD HH:mm:ss"}}</td>
|
||||
<td data-test-name>{{allocation.name}}</td>
|
||||
{{#if (eq context "job")}}
|
||||
<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">
|
||||
<span class="color-swatch {{allocation.clientStatus}}" /> {{allocation.clientStatus}}
|
||||
</td>
|
||||
{{#if (eq context "job")}}
|
||||
{{#if (or (eq context "taskGroup") (eq context "job"))}}
|
||||
<td data-test-job-version>{{allocation.jobVersion}}</td>
|
||||
<td data-test-client>{{#link-to "clients.client" allocation.node}}{{allocation.node.shortId}}{{/link-to}}</td>
|
||||
{{else if (eq context "node")}}
|
||||
|
@ -32,9 +39,9 @@
|
|||
<span class="is-faded" data-test-task-group>/ {{allocation.taskGroup.name}}</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-test-job-version>{{allocation.jobVersion}}</td>
|
||||
<td data-test-job-version class="is-1">{{allocation.jobVersion}}</td>
|
||||
{{/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)}}
|
||||
...
|
||||
{{else if (not allocation)}}
|
||||
|
@ -44,7 +51,7 @@
|
|||
{{x-icon "warning" class="is-warning"}}
|
||||
</span>
|
||||
{{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
|
||||
class="progress is-info is-small"
|
||||
value="{{stats.percentCPU}}"
|
||||
|
@ -54,13 +61,13 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
</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)}}
|
||||
...
|
||||
{{else if (not allocation)}}
|
||||
{{! nothing when there's no allocation}}
|
||||
{{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"}}
|
||||
</span>
|
||||
{{else}}
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
{{#t.head}}
|
||||
<th class="is-narrow"></th>
|
||||
<th>ID</th>
|
||||
<th>Task Group</th>
|
||||
<th>Created</th>
|
||||
<th>Modified</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Version</th>
|
||||
<th>Node</th>
|
||||
|
|
|
@ -22,4 +22,6 @@
|
|||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/recent-allocations job=job}}
|
||||
{{/job-page/parts/body}}
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/recent-allocations job=job}}
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">Payload</div>
|
||||
<div class="boxed-section-body is-dark">
|
||||
|
|
|
@ -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>
|
|
@ -28,4 +28,6 @@
|
|||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/recent-allocations job=job}}
|
||||
{{/job-page/parts/body}}
|
||||
|
|
|
@ -24,4 +24,6 @@
|
|||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/recent-allocations job=job}}
|
||||
{{/job-page/parts/body}}
|
||||
|
|
|
@ -22,4 +22,6 @@
|
|||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/recent-allocations job=job}}
|
||||
{{/job-page/parts/body}}
|
||||
|
|
|
@ -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}}–{{p.endsAt}} of {{sortedAllocations.length}}
|
||||
</div>
|
||||
{{#p.prev class="pagination-previous"}} < {{/p.prev}}
|
||||
{{#p.next class="pagination-next"}} > {{/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}}
|
||||
|
|
@ -1,10 +1,5 @@
|
|||
{{partial "jobs/job/subnav"}}
|
||||
<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}}
|
||||
{{#list-table
|
||||
source=sortedEvaluations
|
||||
|
@ -41,6 +36,4 @@
|
|||
<p class="empty-message-body">This is most likely due to garbage collection.</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
{{#if job.supportsDeployments}}
|
||||
<li data-test-tab="deployments">{{#link-to "jobs.job.deployments" job activeClass="is-active"}}Deployments{{/link-to}}</li>
|
||||
{{/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>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -63,8 +63,8 @@
|
|||
{{#t.head}}
|
||||
<th class="is-narrow"></th>
|
||||
{{#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="name"}}Name{{/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}}
|
||||
|
@ -72,7 +72,7 @@
|
|||
<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)}}
|
||||
{{allocation-row data-test-allocation=row.model.id allocation=row.model context="taskGroup" onClick=(action "gotoAllocation" row.model)}}
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
<div class="table-foot">
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import ArrayProxy from '@ember/array/proxy';
|
||||
import PromiseProxyMixin from '@ember/object/promise-proxy-mixin';
|
||||
|
||||
export default ArrayProxy.extend(PromiseProxyMixin);
|
|
@ -19,6 +19,9 @@ module.exports = function(defaults) {
|
|||
`${defaults.project.pkg.name}/templates/components/freestyle/**/*`,
|
||||
],
|
||||
},
|
||||
babel: {
|
||||
plugins: ['transform-object-rest-spread'],
|
||||
},
|
||||
});
|
||||
|
||||
// Use `app.import` to add additional libraries to the generated
|
||||
|
|
|
@ -11,11 +11,16 @@ const REF_TIME = new Date();
|
|||
export default Factory.extend({
|
||||
id: i => (i >= 100 ? `${UUIDS[i % 100]}-${i}` : UUIDS[i]),
|
||||
|
||||
modifyIndex: () => faker.random.number({ min: 10, max: 2000 }),
|
||||
jobVersion: () => faker.random.number(10),
|
||||
|
||||
modifyIndex: () => faker.random.number({ min: 10, max: 2000 }),
|
||||
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,
|
||||
|
||||
clientStatus: faker.list.random(...CLIENT_STATUSES),
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"broccoli-asset-rev": "^2.4.5",
|
||||
"bulma": "0.6.1",
|
||||
"core-js": "^2.4.1",
|
||||
|
|
|
@ -128,13 +128,17 @@ test('each allocation should have high-level details for the allocation', functi
|
|||
andThen(() => {
|
||||
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(
|
||||
allocationRow.modifyTime,
|
||||
moment(allocation.modifyTime / 1000000).format('MM/DD HH:mm:ss'),
|
||||
moment(allocation.modifyTime / 1000000).fromNow(),
|
||||
'Allocation modify time'
|
||||
);
|
||||
assert.equal(allocationRow.name, allocation.name, 'Allocation name');
|
||||
assert.equal(allocationRow.status, allocation.clientStatus, 'Client status');
|
||||
assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name');
|
||||
assert.ok(allocationRow.taskGroup, 'Task group name');
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -232,7 +232,7 @@ test('when open, a deployment shows a list of all allocations for the deployment
|
|||
const allocation = allocations[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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -143,12 +143,16 @@ test('each allocation should show basic information about the allocation', funct
|
|||
|
||||
andThen(() => {
|
||||
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(
|
||||
allocationRow.modifyTime,
|
||||
moment(allocation.modifyTime / 1000000).format('MM/DD HH:mm:ss'),
|
||||
moment(allocation.modifyTime / 1000000).fromNow(),
|
||||
'Allocation modify time'
|
||||
);
|
||||
assert.equal(allocationRow.name, allocation.name, 'Allocation name');
|
||||
assert.equal(allocationRow.status, allocation.clientStatus, 'Client status');
|
||||
assert.equal(allocationRow.jobVersion, allocation.jobVersion, 'Job Version');
|
||||
assert.equal(
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import { getOwner } from '@ember/application';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
import { stopJob, expectStopError, expectDeleteRequest } from './helpers';
|
||||
import Job from 'nomad-ui/tests/pages/jobs/detail';
|
||||
|
||||
moduleForComponent('job-page/service', 'Integration | Component | job-page/service', {
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
Job.setContext(this);
|
||||
window.localStorage.clear();
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
},
|
||||
afterEach() {
|
||||
Job.removeContext();
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
},
|
||||
|
@ -36,12 +40,18 @@ const commonProperties = job => ({
|
|||
gotoJob() {},
|
||||
});
|
||||
|
||||
const makeMirageJob = server =>
|
||||
server.create('job', {
|
||||
const makeMirageJob = (server, props = {}) =>
|
||||
server.create(
|
||||
'job',
|
||||
assign(
|
||||
{
|
||||
type: 'service',
|
||||
createAllocations: false,
|
||||
status: 'running',
|
||||
});
|
||||
},
|
||||
props
|
||||
)
|
||||
);
|
||||
|
||||
test('Stopping a job sends a delete request for the job', function(assert) {
|
||||
let job;
|
||||
|
@ -80,3 +90,78 @@ test('Stopping a job without proper permissions shows an error message', functio
|
|||
.then(stopJob)
|
||||
.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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
visitable,
|
||||
} from 'ember-cli-page-object';
|
||||
|
||||
import allocations from 'nomad-ui/tests/pages/components/allocations';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/clients/:id'),
|
||||
|
||||
|
@ -36,22 +38,7 @@ export default create({
|
|||
eligibilityDefinition: text('[data-test-eligibility]'),
|
||||
datacenterDefinition: text('[data-test-datacenter-definition]'),
|
||||
|
||||
allocations: collection('[data-test-allocation]', {
|
||||
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]'),
|
||||
}),
|
||||
...allocations(),
|
||||
|
||||
attributesTable: isPresent('[data-test-attributes]'),
|
||||
metaTable: isPresent('[data-test-meta]'),
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -8,6 +8,8 @@ import {
|
|||
visitable,
|
||||
} from 'ember-cli-page-object';
|
||||
|
||||
import allocations from 'nomad-ui/tests/pages/components/allocations';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/jobs/:id'),
|
||||
|
||||
|
@ -29,10 +31,18 @@ export default create({
|
|||
return this.stats.toArray().findBy('id', id);
|
||||
},
|
||||
|
||||
...allocations(),
|
||||
|
||||
viewAllAllocations: text('[data-test-view-all-allocations]'),
|
||||
|
||||
error: {
|
||||
isPresent: isPresent('[data-test-error]'),
|
||||
title: text('[data-test-error-title]'),
|
||||
message: text('[data-test-error-message]'),
|
||||
seekHelp: clickable('[data-test-error-message] a'),
|
||||
},
|
||||
|
||||
recentAllocationsEmptyState: {
|
||||
headline: text('[data-test-empty-recent-allocations-headline]'),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
});
|
|
@ -8,6 +8,8 @@ import {
|
|||
visitable,
|
||||
} from 'ember-cli-page-object';
|
||||
|
||||
import allocations from 'nomad-ui/tests/pages/components/allocations';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/jobs/:id/deployments'),
|
||||
|
||||
|
@ -46,9 +48,7 @@ export default create({
|
|||
progress: text('[data-test-deployment-task-group-progress-deadline]'),
|
||||
}),
|
||||
|
||||
...allocations('[data-test-deployment-allocation]'),
|
||||
hasAllocations: isPresent('[data-test-deployment-allocations]'),
|
||||
allocations: collection('[data-test-deployment-allocation]', {
|
||||
id: text('[data-test-short-id]'),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
visitable,
|
||||
} from 'ember-cli-page-object';
|
||||
|
||||
import allocations from 'nomad-ui/tests/pages/components/allocations';
|
||||
|
||||
export default create({
|
||||
pageSize: 10,
|
||||
|
||||
|
@ -31,27 +33,7 @@ export default create({
|
|||
return this.breadcrumbs.toArray().find(crumb => crumb.id === id);
|
||||
},
|
||||
|
||||
allocations: collection('[data-test-allocation]', {
|
||||
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);
|
||||
},
|
||||
...allocations(),
|
||||
|
||||
isEmpty: isPresent('[data-test-empty-allocations-list]'),
|
||||
|
||||
|
|
11
ui/yarn.lock
11
ui/yarn.lock
|
@ -854,6 +854,10 @@ babel-plugin-syntax-exponentiation-operator@^6.8.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"
|
||||
|
||||
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:
|
||||
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"
|
||||
|
@ -1042,6 +1046,13 @@ babel-plugin-transform-exponentiation-operator@^6.22.0:
|
|||
babel-plugin-syntax-exponentiation-operator "^6.8.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:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
|
||||
|
|
Loading…
Reference in New Issue