Merge pull request #3829 from hashicorp/f-ui-specialized-job-pages
UI: Specialized Job Detail Pages
This commit is contained in:
commit
f95f5a1a66
|
@ -65,4 +65,17 @@ export default ApplicationAdapter.extend({
|
|||
const url = this.buildURL('job', name, job, 'findRecord');
|
||||
return this.ajax(url, 'GET', { data: assign(this.buildQuery() || {}, namespaceQuery) });
|
||||
},
|
||||
|
||||
forcePeriodic(job) {
|
||||
if (job.get('periodic')) {
|
||||
const [name, namespace] = JSON.parse(job.get('id'));
|
||||
let url = `${this.buildURL('job', name, job, 'findRecord')}/periodic/force`;
|
||||
|
||||
if (namespace) {
|
||||
url += `?namespace=${namespace}`;
|
||||
}
|
||||
|
||||
return this.ajax(url, 'POST');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@ export default DistributionBar.extend({
|
|||
|
||||
allocationContainer: null,
|
||||
|
||||
'data-test-allocation-status-bar': true,
|
||||
|
||||
data: computed(
|
||||
'allocationContainer.{queuedAllocs,completeAllocs,failedAllocs,runningAllocs,startingAllocs}',
|
||||
function() {
|
||||
|
|
27
ui/app/components/children-status-bar.js
Normal file
27
ui/app/components/children-status-bar.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { computed } from '@ember/object';
|
||||
import DistributionBar from './distribution-bar';
|
||||
|
||||
export default DistributionBar.extend({
|
||||
layoutName: 'components/distribution-bar',
|
||||
|
||||
job: null,
|
||||
|
||||
'data-test-children-status-bar': true,
|
||||
|
||||
data: computed('job.{pendingChildren,runningChildren,deadChildren}', function() {
|
||||
if (!this.get('job')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const children = this.get('job').getProperties(
|
||||
'pendingChildren',
|
||||
'runningChildren',
|
||||
'deadChildren'
|
||||
);
|
||||
return [
|
||||
{ label: 'Pending', value: children.pendingChildren, className: 'queued' },
|
||||
{ label: 'Running', value: children.runningChildren, className: 'running' },
|
||||
{ label: 'Dead', value: children.deadChildren, className: 'complete' },
|
||||
];
|
||||
}),
|
||||
});
|
29
ui/app/components/job-page/abstract.js
Normal file
29
ui/app/components/job-page/abstract.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
system: service(),
|
||||
|
||||
job: null,
|
||||
|
||||
// Provide a value that is bound to a query param
|
||||
sortProperty: null,
|
||||
sortDescending: null,
|
||||
|
||||
// Provide actions that require routing
|
||||
onNamespaceChange() {},
|
||||
gotoTaskGroup() {},
|
||||
gotoJob() {},
|
||||
|
||||
breadcrumbs: computed('job.{name,id}', function() {
|
||||
const job = this.get('job');
|
||||
return [
|
||||
{ label: 'Jobs', args: ['jobs'] },
|
||||
{
|
||||
label: job.get('name'),
|
||||
args: ['jobs.job', job],
|
||||
},
|
||||
];
|
||||
}),
|
||||
});
|
3
ui/app/components/job-page/batch.js
Normal file
3
ui/app/components/job-page/batch.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import AbstractJobPage from './abstract';
|
||||
|
||||
export default AbstractJobPage.extend();
|
16
ui/app/components/job-page/parameterized-child.js
Normal file
16
ui/app/components/job-page/parameterized-child.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { computed } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import PeriodicChildJobPage from './periodic-child';
|
||||
|
||||
export default PeriodicChildJobPage.extend({
|
||||
payload: alias('job.decodedPayload'),
|
||||
payloadJSON: computed('payload', function() {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(this.get('payload'));
|
||||
} catch (e) {
|
||||
// Swallow error and fall back to plain text rendering
|
||||
}
|
||||
return json;
|
||||
}),
|
||||
});
|
3
ui/app/components/job-page/parameterized.js
Normal file
3
ui/app/components/job-page/parameterized.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import AbstractJobPage from './abstract';
|
||||
|
||||
export default AbstractJobPage.extend();
|
31
ui/app/components/job-page/parts/children.js
Normal file
31
ui/app/components/job-page/parts/children.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Sortable from 'nomad-ui/mixins/sortable';
|
||||
|
||||
export default Component.extend(Sortable, {
|
||||
job: null,
|
||||
|
||||
classNames: ['boxed-section'],
|
||||
|
||||
// Provide a value that is bound to a query param
|
||||
sortProperty: null,
|
||||
sortDescending: null,
|
||||
currentPage: null,
|
||||
|
||||
// Provide an action with access to the router
|
||||
gotoJob() {},
|
||||
|
||||
pageSize: 10,
|
||||
|
||||
taskGroups: computed('job.taskGroups.[]', function() {
|
||||
return this.get('job.taskGroups') || [];
|
||||
}),
|
||||
|
||||
children: computed('job.children.[]', function() {
|
||||
return this.get('job.children') || [];
|
||||
}),
|
||||
|
||||
listToSort: alias('children'),
|
||||
sortedChildren: alias('listSorted'),
|
||||
});
|
12
ui/app/components/job-page/parts/evaluations.js
Normal file
12
ui/app/components/job-page/parts/evaluations.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
job: null,
|
||||
|
||||
classNames: ['boxed-section'],
|
||||
|
||||
sortedEvaluations: computed('job.evaluations.@each.modifyIndex', function() {
|
||||
return (this.get('job.evaluations') || []).sortBy('modifyIndex').reverse();
|
||||
}),
|
||||
});
|
6
ui/app/components/job-page/parts/placement-failures.js
Normal file
6
ui/app/components/job-page/parts/placement-failures.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
job: null,
|
||||
tagName: '',
|
||||
});
|
6
ui/app/components/job-page/parts/running-deployment.js
Normal file
6
ui/app/components/job-page/parts/running-deployment.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
job: null,
|
||||
tagName: '',
|
||||
});
|
7
ui/app/components/job-page/parts/summary.js
Normal file
7
ui/app/components/job-page/parts/summary.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
job: null,
|
||||
|
||||
classNames: ['boxed-section'],
|
||||
});
|
24
ui/app/components/job-page/parts/task-groups.js
Normal file
24
ui/app/components/job-page/parts/task-groups.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Sortable from 'nomad-ui/mixins/sortable';
|
||||
|
||||
export default Component.extend(Sortable, {
|
||||
job: null,
|
||||
|
||||
classNames: ['boxed-section'],
|
||||
|
||||
// Provide a value that is bound to a query param
|
||||
sortProperty: null,
|
||||
sortDescending: null,
|
||||
|
||||
// Provide an action with access to the router
|
||||
gotoTaskGroup() {},
|
||||
|
||||
taskGroups: computed('job.taskGroups.[]', function() {
|
||||
return this.get('job.taskGroups') || [];
|
||||
}),
|
||||
|
||||
listToSort: alias('taskGroups'),
|
||||
sortedTaskGroups: alias('listSorted'),
|
||||
});
|
21
ui/app/components/job-page/periodic-child.js
Normal file
21
ui/app/components/job-page/periodic-child.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import AbstractJobPage from './abstract';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default AbstractJobPage.extend({
|
||||
breadcrumbs: computed('job.{name,id}', 'job.parent.{name,id}', function() {
|
||||
const job = this.get('job');
|
||||
const parent = this.get('job.parent');
|
||||
|
||||
return [
|
||||
{ label: 'Jobs', args: ['jobs'] },
|
||||
{
|
||||
label: parent.get('name'),
|
||||
args: ['jobs.job', parent],
|
||||
},
|
||||
{
|
||||
label: job.get('trimmedName'),
|
||||
args: ['jobs.job', job],
|
||||
},
|
||||
];
|
||||
}),
|
||||
});
|
15
ui/app/components/job-page/periodic.js
Normal file
15
ui/app/components/job-page/periodic.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import AbstractJobPage from './abstract';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default AbstractJobPage.extend({
|
||||
store: service(),
|
||||
actions: {
|
||||
forceLaunch() {
|
||||
this.get('job')
|
||||
.forcePeriodic()
|
||||
.then(() => {
|
||||
this.get('store').findAll('job');
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
3
ui/app/components/job-page/service.js
Normal file
3
ui/app/components/job-page/service.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import AbstractJobPage from './abstract';
|
||||
|
||||
export default AbstractJobPage.extend();
|
|
@ -1,5 +1,5 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { alias, filterBy } from '@ember/object/computed';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Controller, { inject as controller } from '@ember/controller';
|
||||
import { computed } from '@ember/object';
|
||||
import Sortable from 'nomad-ui/mixins/sortable';
|
||||
|
@ -11,10 +11,6 @@ export default Controller.extend(Sortable, Searchable, {
|
|||
|
||||
isForbidden: alias('jobsController.isForbidden'),
|
||||
|
||||
pendingJobs: filterBy('model', 'status', 'pending'),
|
||||
runningJobs: filterBy('model', 'status', 'running'),
|
||||
deadJobs: filterBy('model', 'status', 'dead'),
|
||||
|
||||
queryParams: {
|
||||
currentPage: 'page',
|
||||
searchTerm: 'search',
|
||||
|
@ -30,16 +26,22 @@ export default Controller.extend(Sortable, Searchable, {
|
|||
|
||||
searchProps: computed(() => ['id', 'name']),
|
||||
|
||||
/**
|
||||
Filtered jobs are those that match the selected namespace and aren't children
|
||||
of periodic or parameterized jobs.
|
||||
*/
|
||||
filteredJobs: computed(
|
||||
'model.[]',
|
||||
'model.@each.parent',
|
||||
'system.activeNamespace',
|
||||
'system.namespaces.length',
|
||||
function() {
|
||||
if (this.get('system.namespaces.length')) {
|
||||
return this.get('model').filterBy('namespace.id', this.get('system.activeNamespace.id'));
|
||||
} else {
|
||||
return this.get('model');
|
||||
}
|
||||
const hasNamespaces = this.get('system.namespaces.length');
|
||||
const activeNamespace = this.get('system.activeNamespace.id');
|
||||
|
||||
return this.get('model')
|
||||
.filter(job => !hasNamespaces || job.get('namespace.id') === activeNamespace)
|
||||
.filter(job => !job.get('parent.content'));
|
||||
}
|
||||
),
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Controller, { inject as controller } from '@ember/controller';
|
||||
import { computed } from '@ember/object';
|
||||
import Sortable from 'nomad-ui/mixins/sortable';
|
||||
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
|
||||
|
||||
export default Controller.extend(Sortable, WithNamespaceResetting, {
|
||||
export default Controller.extend(WithNamespaceResetting, {
|
||||
system: service(),
|
||||
jobController: controller('jobs.job'),
|
||||
|
||||
|
@ -16,7 +14,6 @@ export default Controller.extend(Sortable, WithNamespaceResetting, {
|
|||
},
|
||||
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
|
||||
sortProperty: 'name',
|
||||
sortDescending: false,
|
||||
|
@ -24,20 +21,15 @@ export default Controller.extend(Sortable, WithNamespaceResetting, {
|
|||
breadcrumbs: alias('jobController.breadcrumbs'),
|
||||
job: alias('model'),
|
||||
|
||||
taskGroups: computed('model.taskGroups.[]', function() {
|
||||
return this.get('model.taskGroups') || [];
|
||||
}),
|
||||
|
||||
listToSort: alias('taskGroups'),
|
||||
sortedTaskGroups: alias('listSorted'),
|
||||
|
||||
sortedEvaluations: computed('model.evaluations.@each.modifyIndex', function() {
|
||||
return (this.get('model.evaluations') || []).sortBy('modifyIndex').reverse();
|
||||
}),
|
||||
|
||||
actions: {
|
||||
gotoTaskGroup(taskGroup) {
|
||||
this.transitionToRoute('jobs.job.task-group', taskGroup.get('job'), taskGroup);
|
||||
},
|
||||
|
||||
gotoJob(job) {
|
||||
this.transitionToRoute('jobs.job', job, {
|
||||
queryParams: { jobNamespace: job.get('namespace.name') },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { collect, sum, bool, equal } from '@ember/object/computed';
|
||||
import { collect, sum, bool, equal, or } from '@ember/object/computed';
|
||||
import { computed } from '@ember/object';
|
||||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
|
@ -6,6 +6,8 @@ import { belongsTo, hasMany } from 'ember-data/relationships';
|
|||
import { fragmentArray } from 'ember-data-model-fragments/attributes';
|
||||
import sumAggregation from '../utils/properties/sum-aggregation';
|
||||
|
||||
const JOB_TYPES = ['service', 'batch', 'system'];
|
||||
|
||||
export default Model.extend({
|
||||
region: attr('string'),
|
||||
name: attr('string'),
|
||||
|
@ -19,9 +21,66 @@ export default Model.extend({
|
|||
createIndex: attr('number'),
|
||||
modifyIndex: attr('number'),
|
||||
|
||||
// True when the job is the parent periodic or parameterized jobs
|
||||
// Instances of periodic or parameterized jobs are false for both properties
|
||||
periodic: attr('boolean'),
|
||||
parameterized: attr('boolean'),
|
||||
|
||||
periodicDetails: attr(),
|
||||
parameterizedDetails: attr(),
|
||||
|
||||
hasChildren: or('periodic', 'parameterized'),
|
||||
|
||||
parent: belongsTo('job', { inverse: 'children' }),
|
||||
children: hasMany('job', { inverse: 'parent' }),
|
||||
|
||||
// The parent job name is prepended to child launch job names
|
||||
trimmedName: computed('name', 'parent', function() {
|
||||
return this.get('parent.content') ? this.get('name').replace(/.+?\//, '') : this.get('name');
|
||||
}),
|
||||
|
||||
// A composite of type and other job attributes to determine
|
||||
// a better type descriptor for human interpretation rather
|
||||
// than for scheduling.
|
||||
displayType: computed('type', 'periodic', 'parameterized', function() {
|
||||
if (this.get('periodic')) {
|
||||
return 'periodic';
|
||||
} else if (this.get('parameterized')) {
|
||||
return 'parameterized';
|
||||
}
|
||||
return this.get('type');
|
||||
}),
|
||||
|
||||
// A composite of type and other job attributes to determine
|
||||
// type for templating rather than scheduling
|
||||
templateType: computed(
|
||||
'type',
|
||||
'periodic',
|
||||
'parameterized',
|
||||
'parent.periodic',
|
||||
'parent.parameterized',
|
||||
function() {
|
||||
const type = this.get('type');
|
||||
|
||||
if (this.get('periodic')) {
|
||||
return 'periodic';
|
||||
} else if (this.get('parameterized')) {
|
||||
return 'parameterized';
|
||||
} else if (this.get('parent.periodic')) {
|
||||
return 'periodic-child';
|
||||
} else if (this.get('parent.parameterized')) {
|
||||
return 'parameterized-child';
|
||||
} else if (JOB_TYPES.includes(type)) {
|
||||
// Guard against the API introducing a new type before the UI
|
||||
// is prepared to handle it.
|
||||
return this.get('type');
|
||||
}
|
||||
|
||||
// A fail-safe in the event the API introduces a new type.
|
||||
return 'service';
|
||||
}
|
||||
),
|
||||
|
||||
datacenters: attr(),
|
||||
taskGroups: fragmentArray('task-group', { defaultValue: () => [] }),
|
||||
taskGroupSummaries: fragmentArray('task-group-summary'),
|
||||
|
@ -49,6 +108,12 @@ export default Model.extend({
|
|||
runningChildren: attr('number'),
|
||||
deadChildren: attr('number'),
|
||||
|
||||
childrenList: collect('pendingChildren', 'runningChildren', 'deadChildren'),
|
||||
|
||||
totalChildren: sum('childrenList'),
|
||||
|
||||
version: attr('number'),
|
||||
|
||||
versions: hasMany('job-versions'),
|
||||
allocations: hasMany('allocations'),
|
||||
deployments: hasMany('deployments'),
|
||||
|
@ -91,6 +156,10 @@ export default Model.extend({
|
|||
return this.store.adapterFor('job').fetchRawDefinition(this);
|
||||
},
|
||||
|
||||
forcePeriodic() {
|
||||
return this.store.adapterFor('job').forcePeriodic(this);
|
||||
},
|
||||
|
||||
statusClass: computed('status', function() {
|
||||
const classMap = {
|
||||
pending: 'is-pending',
|
||||
|
@ -100,4 +169,10 @@ export default Model.extend({
|
|||
|
||||
return classMap[this.get('status')] || 'is-dark';
|
||||
}),
|
||||
|
||||
payload: attr('string'),
|
||||
decodedPayload: computed('payload', function() {
|
||||
// Lazily decode the base64 encoded payload
|
||||
return window.atob(this.get('payload') || '');
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -15,6 +15,25 @@ export default ApplicationSerializer.extend({
|
|||
hash.PlainId = hash.ID;
|
||||
hash.ID = JSON.stringify([hash.ID, hash.NamespaceID || 'default']);
|
||||
|
||||
// ParentID comes in as "" instead of null
|
||||
if (!hash.ParentID) {
|
||||
hash.ParentID = null;
|
||||
} else {
|
||||
hash.ParentID = JSON.stringify([hash.ParentID, hash.NamespaceID || 'default']);
|
||||
}
|
||||
|
||||
// Periodic is a boolean on list and an object on single
|
||||
if (hash.Periodic instanceof Object) {
|
||||
hash.PeriodicDetails = hash.Periodic;
|
||||
hash.Periodic = true;
|
||||
}
|
||||
|
||||
// Parameterized behaves like Periodic
|
||||
if (hash.ParameterizedJob instanceof Object) {
|
||||
hash.ParameterizedDetails = hash.ParameterizedJob;
|
||||
hash.ParameterizedJob = true;
|
||||
}
|
||||
|
||||
// Transform the map-based JobSummary object into an array-based
|
||||
// JobSummary fragment list
|
||||
hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary') || {}).map(key => {
|
||||
|
|
|
@ -12,4 +12,8 @@
|
|||
.is-light {
|
||||
color: $text;
|
||||
}
|
||||
|
||||
&.is-elastic {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,10 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2);
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
&.is-inline {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.is-compact {
|
||||
padding: 0.25em 0.75em;
|
||||
margin: -0.25em -0.25em -0.25em 0;
|
||||
|
|
|
@ -42,6 +42,6 @@
|
|||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-column is-right">
|
||||
<div data-test-page-content class="page-column is-right">
|
||||
{{yield}}
|
||||
</div>
|
||||
|
|
35
ui/app/templates/components/job-page/batch.hbs
Normal file
35
ui/app/templates/components/job-page/batch.hbs
Normal file
|
@ -0,0 +1,35 @@
|
|||
{{#global-header class="page-header"}}
|
||||
{{#each breadcrumbs as |breadcrumb index|}}
|
||||
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
|
||||
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
<span data-test-job-stat="type"><strong>Type:</strong> {{job.type}} | </span>
|
||||
<span data-test-job-stat="priority"><strong>Priority:</strong> {{job.priority}} </span>
|
||||
{{#if (and job.namespace system.shouldShowNamespaces)}}
|
||||
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{job.namespace.name}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{job-page/parts/summary job=job}}
|
||||
|
||||
{{job-page/parts/placement-failures job=job}}
|
||||
|
||||
{{job-page/parts/task-groups
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/evaluations job=job}}
|
||||
{{/job-page/parts/body}}
|
52
ui/app/templates/components/job-page/parameterized-child.hbs
Normal file
52
ui/app/templates/components/job-page/parameterized-child.hbs
Normal file
|
@ -0,0 +1,52 @@
|
|||
{{#global-header class="page-header"}}
|
||||
{{#each breadcrumbs as |breadcrumb index|}}
|
||||
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
|
||||
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.trimmedName}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
<span data-test-job-stat="type"><strong>Type:</strong> {{job.type}} | </span>
|
||||
<span data-test-job-stat="priority"><strong>Priority:</strong> {{job.priority}} </span>
|
||||
<span data-test-job-stat="parent">
|
||||
<strong>Parent:</strong>
|
||||
{{#link-to "jobs.job" job.parent (query-params jobNamespace=job.parent.namespace.name)}}
|
||||
{{job.parent.name}}
|
||||
{{/link-to}}
|
||||
</span>
|
||||
{{#if (and job.namespace system.shouldShowNamespaces)}}
|
||||
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{job.namespace.name}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{job-page/parts/summary job=job}}
|
||||
|
||||
{{job-page/parts/placement-failures job=job}}
|
||||
|
||||
{{job-page/parts/task-groups
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/evaluations job=job}}
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">Payload</div>
|
||||
<div class="boxed-section-body is-dark">
|
||||
{{#if payloadJSON}}
|
||||
{{json-viewer json=payloadJSON}}
|
||||
{{else}}
|
||||
<pre class="cli-window is-elastic"><code>{{payload}}</code></pre>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/job-page/parts/body}}
|
32
ui/app/templates/components/job-page/parameterized.hbs
Normal file
32
ui/app/templates/components/job-page/parameterized.hbs
Normal file
|
@ -0,0 +1,32 @@
|
|||
{{#global-header class="page-header"}}
|
||||
{{#each breadcrumbs as |breadcrumb index|}}
|
||||
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
|
||||
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
<span class="tag is-hollow">Parameterized</span>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
<span data-test-job-stat="version"><strong>Version:</strong> {{job.version}} | </span>
|
||||
<span data-test-job-stat="priority"><strong>Priority:</strong> {{job.priority}} </span>
|
||||
{{#if (and job.namespace system.shouldShowNamespaces)}}
|
||||
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{job.namespace.name}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{job-page/parts/summary job=job}}
|
||||
{{job-page/parts/children
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
{{/job-page/parts/body}}
|
6
ui/app/templates/components/job-page/parts/body.hbs
Normal file
6
ui/app/templates/components/job-page/parts/body.hbs
Normal file
|
@ -0,0 +1,6 @@
|
|||
{{#gutter-menu class="page-body" onNamespaceChange=onNamespaceChange}}
|
||||
{{partial "jobs/job/subnav"}}
|
||||
<section class="section">
|
||||
{{yield}}
|
||||
</section>
|
||||
{{/gutter-menu}}
|
42
ui/app/templates/components/job-page/parts/children.hbs
Normal file
42
ui/app/templates/components/job-page/parts/children.hbs
Normal file
|
@ -0,0 +1,42 @@
|
|||
<div class="boxed-section-head">
|
||||
Job Launches
|
||||
</div>
|
||||
<div class="boxed-section-body {{if sortedChildren.length "is-full-bleed"}}">
|
||||
{{#list-pagination
|
||||
source=sortedChildren
|
||||
size=pageSize
|
||||
page=currentPage as |p|}}
|
||||
{{#list-table
|
||||
source=p.list
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
class="with-foot" as |t|}}
|
||||
{{#t.head}}
|
||||
{{#t.sort-by prop="name"}}Name{{/t.sort-by}}
|
||||
{{#t.sort-by prop="status"}}Status{{/t.sort-by}}
|
||||
{{#t.sort-by prop="type"}}Type{{/t.sort-by}}
|
||||
{{#t.sort-by prop="priority"}}Priority{{/t.sort-by}}
|
||||
<th>Groups</th>
|
||||
<th class="is-3">Summary</th>
|
||||
{{/t.head}}
|
||||
{{#t.body key="model.id" as |row|}}
|
||||
{{job-row data-test-job-row job=row.model onClick=(action gotoJob row.model)}}
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
<div class="table-foot">
|
||||
<nav class="pagination">
|
||||
<div class="pagination-numbers">
|
||||
{{p.startsAt}}–{{p.endsAt}} of {{sortedChildren.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="empty-message">
|
||||
<h3 class="empty-message-headline">No Job Launches</h3>
|
||||
<p class="empty-message-body">No remaining living job launches.</p>
|
||||
</div>
|
||||
{{/list-pagination}}
|
||||
</div>
|
38
ui/app/templates/components/job-page/parts/evaluations.hbs
Normal file
38
ui/app/templates/components/job-page/parts/evaluations.hbs
Normal file
|
@ -0,0 +1,38 @@
|
|||
<div class="boxed-section-head">
|
||||
Evaluations
|
||||
</div>
|
||||
<div class="boxed-section-body {{if sortedEvaluations.length "is-full-bleed"}} evaluations">
|
||||
{{#if sortedEvaluations.length}}
|
||||
{{#list-table source=sortedEvaluations as |t|}}
|
||||
{{#t.head}}
|
||||
<th>ID</th>
|
||||
<th>Priority</th>
|
||||
<th>Triggered By</th>
|
||||
<th>Status</th>
|
||||
<th>Placement Failures</th>
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
<tr data-test-evaluation="{{row.model.shortId}}">
|
||||
<td data-test-id>{{row.model.shortId}}</td>
|
||||
<td data-test-priority>{{row.model.priority}}</td>
|
||||
<td data-test-triggered-by>{{row.model.triggeredBy}}</td>
|
||||
<td data-test-status>{{row.model.status}}</td>
|
||||
<td data-test-blocked>
|
||||
{{#if (eq row.model.status "blocked")}}
|
||||
N/A - In Progress
|
||||
{{else if row.model.hasPlacementFailures}}
|
||||
True
|
||||
{{else}}
|
||||
False
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
{{else}}
|
||||
<div data-test-empty-evaluations-list class="empty-message">
|
||||
<h3 data-test-empty-evaluations-list-headline class="empty-message-headline">No Evaluations</h3>
|
||||
<p class="empty-message-body">This is most likely due to garbage collection.</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
{{#if job.hasPlacementFailures}}
|
||||
<div class="boxed-section is-danger" data-test-placement-failures>
|
||||
<div class="boxed-section-head">
|
||||
Placement Failures
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
{{#each job.taskGroups as |taskGroup|}}
|
||||
{{placement-failure taskGroup=taskGroup}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -0,0 +1,33 @@
|
|||
{{#if job.runningDeployment}}
|
||||
<div class="boxed-section is-info" data-test-active-deployment>
|
||||
<div class="boxed-section-head">
|
||||
<div class="boxed-section-row">
|
||||
Active Deployment
|
||||
<span class="badge is-white is-subtle bumper-left" data-test-active-deployment-stat="id">{{job.runningDeployment.shortId}}</span>
|
||||
{{#if job.runningDeployment.version.submitTime}}
|
||||
<span class="pull-right submit-time" data-test-active-deployment-stat="submit-time">{{moment-from-now job.runningDeployment.version.submitTime}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="boxed-section-row">
|
||||
<span class="tag is-info is-outlined">Running</span>
|
||||
{{#if job.runningDeployment.requiresPromotion}}
|
||||
<span class="tag bumper-left is-warning no-text-transform">Deployment is running but requires promotion</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section-body with-foot">
|
||||
{{#job-deployment-details deployment=job.runningDeployment as |d|}}
|
||||
{{d.metrics}}
|
||||
{{#if isShowingDeploymentDetails}}
|
||||
{{d.taskGroups}}
|
||||
{{d.allocations}}
|
||||
{{/if}}
|
||||
{{/job-deployment-details}}
|
||||
</div>
|
||||
<div class="boxed-section-foot">
|
||||
<a class="pull-right" {{action (toggle "isShowingDeploymentDetails" this)}} data-test-deployment-toggle-details>
|
||||
{{if isShowingDeploymentDetails "Hide" "Show"}} deployment task groups and allocations
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
27
ui/app/templates/components/job-page/parts/summary.hbs
Normal file
27
ui/app/templates/components/job-page/parts/summary.hbs
Normal file
|
@ -0,0 +1,27 @@
|
|||
<div class="boxed-section-head">
|
||||
<div>
|
||||
{{#if job.hasChildren}}
|
||||
Children Status <span class="badge is-white">{{job.totalChildren}}</span>
|
||||
{{else}}
|
||||
Allocation Status <span class="badge is-white">{{job.totalAllocs}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
{{#component (if job.hasChildren "children-status-bar" "allocation-status-bar")
|
||||
allocationContainer=job
|
||||
job=job
|
||||
class="split-view" as |chart|}}
|
||||
<ol data-test-legend class="legend">
|
||||
{{#each chart.data as |datum index|}}
|
||||
<li class="{{datum.className}} {{if (eq datum.index chart.activeDatum.index) "is-active"}} {{if (eq datum.value 0) "is-empty"}}">
|
||||
<span class="color-swatch {{if datum.className datum.className (concat "swatch-" index)}}" />
|
||||
<span class="value" data-test-legend-value="{{datum.className}}">{{datum.value}}</span>
|
||||
<span class="label">
|
||||
{{datum.label}}
|
||||
</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
{{/component}}
|
||||
</div>
|
25
ui/app/templates/components/job-page/parts/task-groups.hbs
Normal file
25
ui/app/templates/components/job-page/parts/task-groups.hbs
Normal file
|
@ -0,0 +1,25 @@
|
|||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Task Groups
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{#list-table
|
||||
source=sortedTaskGroups
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending as |t|}}
|
||||
{{#t.head}}
|
||||
{{#t.sort-by prop="name"}}Name{{/t.sort-by}}
|
||||
{{#t.sort-by prop="count"}}Count{{/t.sort-by}}
|
||||
{{#t.sort-by prop="queuedOrStartingAllocs" class="is-3"}}Allocation Status{{/t.sort-by}}
|
||||
{{#t.sort-by prop="reservedCPU"}}Reserved CPU{{/t.sort-by}}
|
||||
{{#t.sort-by prop="reservedMemory"}}Reserved Memory{{/t.sort-by}}
|
||||
{{#t.sort-by prop="reservedEphemeralDisk"}}Reserved Disk{{/t.sort-by}}
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
{{task-group-row data-test-task-group
|
||||
taskGroup=row.model
|
||||
onClick=(action gotoTaskGroup row.model)}}
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
</div>
|
||||
</div>
|
41
ui/app/templates/components/job-page/periodic-child.hbs
Normal file
41
ui/app/templates/components/job-page/periodic-child.hbs
Normal file
|
@ -0,0 +1,41 @@
|
|||
{{#global-header class="page-header"}}
|
||||
{{#each breadcrumbs as |breadcrumb index|}}
|
||||
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
|
||||
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.trimmedName}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
<span data-test-job-stat="type"><strong>Type:</strong> {{job.type}} | </span>
|
||||
<span data-test-job-stat="priority"><strong>Priority:</strong> {{job.priority}} </span>
|
||||
<span data-test-job-stat="parent">
|
||||
<strong>Parent:</strong>
|
||||
{{#link-to "jobs.job" job.parent (query-params jobNamespace=job.parent.namespace.name)}}
|
||||
{{job.parent.name}}
|
||||
{{/link-to}}
|
||||
</span>
|
||||
{{#if (and job.namespace system.shouldShowNamespaces)}}
|
||||
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{job.namespace.name}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{job-page/parts/summary job=job}}
|
||||
|
||||
{{job-page/parts/placement-failures job=job}}
|
||||
|
||||
{{job-page/parts/task-groups
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/evaluations job=job}}
|
||||
{{/job-page/parts/body}}
|
34
ui/app/templates/components/job-page/periodic.hbs
Normal file
34
ui/app/templates/components/job-page/periodic.hbs
Normal file
|
@ -0,0 +1,34 @@
|
|||
{{#global-header class="page-header"}}
|
||||
{{#each breadcrumbs as |breadcrumb index|}}
|
||||
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
|
||||
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
<span class="tag is-hollow">periodic</span>
|
||||
<button data-test-force-launch class="button is-warning is-small is-inline" onclick={{action "forceLaunch"}}>Force Launch</button>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
<span data-test-job-stat="version"><strong>Version:</strong> {{job.version}} | </span>
|
||||
<span data-test-job-stat="priority"><strong>Priority:</strong> {{job.priority}} </span>
|
||||
{{#if (and job.namespace system.shouldShowNamespaces)}}
|
||||
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{job.namespace.name}}</span>
|
||||
{{/if}}
|
||||
<span data-test-job-stat="cron"> | <strong>Cron:</strong> <code>{{job.periodicDetails.Spec}}</code></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{job-page/parts/summary job=job}}
|
||||
{{job-page/parts/children
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
{{/job-page/parts/body}}
|
37
ui/app/templates/components/job-page/service.hbs
Normal file
37
ui/app/templates/components/job-page/service.hbs
Normal file
|
@ -0,0 +1,37 @@
|
|||
{{#global-header class="page-header"}}
|
||||
{{#each breadcrumbs as |breadcrumb index|}}
|
||||
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
|
||||
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
<span data-test-job-stat="type"><strong>Type:</strong> {{job.type}} | </span>
|
||||
<span data-test-job-stat="priority"><strong>Priority:</strong> {{job.priority}} </span>
|
||||
{{#if (and job.namespace system.shouldShowNamespaces)}}
|
||||
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{job.namespace.name}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{job-page/parts/summary job=job}}
|
||||
|
||||
{{job-page/parts/placement-failures job=job}}
|
||||
|
||||
{{job-page/parts/running-deployment job=job}}
|
||||
|
||||
{{job-page/parts/task-groups
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/evaluations job=job}}
|
||||
{{/job-page/parts/body}}
|
37
ui/app/templates/components/job-page/system.hbs
Normal file
37
ui/app/templates/components/job-page/system.hbs
Normal file
|
@ -0,0 +1,37 @@
|
|||
{{#global-header class="page-header"}}
|
||||
{{#each breadcrumbs as |breadcrumb index|}}
|
||||
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
|
||||
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
<span data-test-job-stat="type"><strong>Type:</strong> {{job.type}} | </span>
|
||||
<span data-test-job-stat="priority"><strong>Priority:</strong> {{job.priority}} </span>
|
||||
{{#if (and job.namespace system.shouldShowNamespaces)}}
|
||||
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{job.namespace.name}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{job-page/parts/summary job=job}}
|
||||
|
||||
{{job-page/parts/placement-failures job=job}}
|
||||
|
||||
{{job-page/parts/running-deployment job=job}}
|
||||
|
||||
{{job-page/parts/task-groups
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
|
||||
{{job-page/parts/evaluations job=job}}
|
||||
{{/job-page/parts/body}}
|
|
@ -2,7 +2,7 @@
|
|||
<td data-test-job-status>
|
||||
<span class="tag {{job.statusClass}}">{{job.status}}</span>
|
||||
</td>
|
||||
<td data-test-job-type>{{job.type}}</td>
|
||||
<td data-test-job-type>{{job.displayType}}</td>
|
||||
<td data-test-job-priority>{{job.priority}}</td>
|
||||
<td data-test-job-task-groups>
|
||||
{{#if job.isReloading}}
|
||||
|
@ -12,5 +12,11 @@
|
|||
{{/if}}
|
||||
</td>
|
||||
<td data-test-job-allocations>
|
||||
<div class="inline-chart">{{allocation-status-bar allocationContainer=job isNarrow=true}}</div>
|
||||
<div class="inline-chart">
|
||||
{{#if job.hasChildren}}
|
||||
{{children-status-bar job=job isNarrow=true}}
|
||||
{{else}}
|
||||
{{allocation-status-bar allocationContainer=job isNarrow=true}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
{{#t.sort-by prop="type"}}Type{{/t.sort-by}}
|
||||
{{#t.sort-by prop="priority"}}Priority{{/t.sort-by}}
|
||||
<th>Groups</th>
|
||||
<th class="is-3">Allocation Status</th>
|
||||
<th class="is-3">Summary</th>
|
||||
{{/t.head}}
|
||||
{{#t.body key="model.id" as |row|}}
|
||||
{{job-row data-test-job-row job=row.model onClick=(action "gotoJob" row.model)}}
|
||||
|
|
|
@ -1,162 +1,8 @@
|
|||
{{#global-header class="page-header"}}
|
||||
{{#each breadcrumbs as |breadcrumb index|}}
|
||||
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
|
||||
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
|
||||
{{partial "jobs/job/subnav"}}
|
||||
<section class="section">
|
||||
<h1 class="title">
|
||||
{{model.name}}
|
||||
<span class="bumper-left tag {{model.statusClass}}" data-test-job-status>{{model.status}}</span>
|
||||
{{#if model.periodic}}
|
||||
<span class="tag is-hollow">periodic</span>
|
||||
{{else if model.parameterized}}
|
||||
<span class="tag is-hollow">parameterized</span>
|
||||
{{/if}}
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
<span data-test-job-stat="type"><strong>Type:</strong> {{model.type}} | </span>
|
||||
<span data-test-job-stat="priority"><strong>Priority:</strong> {{model.priority}} </span>
|
||||
{{#if (and model.namespace system.shouldShowNamespaces)}}
|
||||
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{model.namespace.name}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
<div>Allocation Status <span class="badge is-white">{{taskGroups.length}}</span></div>
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
{{#allocation-status-bar allocationContainer=model class="split-view" as |chart|}}
|
||||
<ol data-test-allocations-legend class="legend">
|
||||
{{#each chart.data as |datum index|}}
|
||||
<li class="{{datum.className}} {{if (eq datum.index chart.activeDatum.index) "is-active"}} {{if (eq datum.value 0) "is-empty"}}">
|
||||
<span class="color-swatch {{if datum.className datum.className (concat "swatch-" index)}}" />
|
||||
<span class="value" data-test-legend-value="{{datum.className}}">{{datum.value}}</span>
|
||||
<span class="label">
|
||||
{{datum.label}}
|
||||
</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
{{/allocation-status-bar}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if model.hasPlacementFailures}}
|
||||
<div class="boxed-section is-danger" data-test-placement-failures>
|
||||
<div class="boxed-section-head">
|
||||
Placement Failures
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
{{#each model.taskGroups as |taskGroup|}}
|
||||
{{placement-failure taskGroup=taskGroup}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.runningDeployment}}
|
||||
<div class="boxed-section is-info" data-test-active-deployment>
|
||||
<div class="boxed-section-head">
|
||||
<div class="boxed-section-row">
|
||||
Active Deployment
|
||||
<span class="badge is-white is-subtle bumper-left" data-test-active-deployment-stat="id">{{model.runningDeployment.shortId}}</span>
|
||||
{{#if model.runningDeployment.version.submitTime}}
|
||||
<span class="pull-right submit-time" data-test-active-deployment-stat="submit-time">{{moment-from-now model.runningDeployment.version.submitTime}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="boxed-section-row">
|
||||
<span class="tag is-info is-outlined">Running</span>
|
||||
{{#if model.runningDeployment.requiresPromotion}}
|
||||
<span class="tag bumper-left is-warning no-text-transform">Deployment is running but requires promotion</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section-body with-foot">
|
||||
{{#job-deployment-details deployment=model.runningDeployment as |d|}}
|
||||
{{d.metrics}}
|
||||
{{#if isShowingDeploymentDetails}}
|
||||
{{d.taskGroups}}
|
||||
{{d.allocations}}
|
||||
{{/if}}
|
||||
{{/job-deployment-details}}
|
||||
</div>
|
||||
<div class="boxed-section-foot">
|
||||
<a class="pull-right" {{action (toggle "isShowingDeploymentDetails" this)}} data-test-deployment-toggle-details>
|
||||
{{if isShowingDeploymentDetails "Hide" "Show"}} deployment task groups and allocations
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Task Groups
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{#list-pagination
|
||||
source=sortedTaskGroups
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending as |p|}}
|
||||
{{#list-table
|
||||
source=p.list
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending as |t|}}
|
||||
{{#t.head}}
|
||||
{{#t.sort-by prop="name"}}Name{{/t.sort-by}}
|
||||
{{#t.sort-by prop="count"}}Count{{/t.sort-by}}
|
||||
{{#t.sort-by prop="queuedOrStartingAllocs" class="is-3"}}Allocation Status{{/t.sort-by}}
|
||||
{{#t.sort-by prop="reservedCPU"}}Reserved CPU{{/t.sort-by}}
|
||||
{{#t.sort-by prop="reservedMemory"}}Reserved Memory{{/t.sort-by}}
|
||||
{{#t.sort-by prop="reservedEphemeralDisk"}}Reserved Disk{{/t.sort-by}}
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
{{task-group-row data-test-task-group taskGroup=row.model onClick=(action "gotoTaskGroup" row.model)}}
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
{{/list-pagination}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Evaluations
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed evaluations">
|
||||
{{#list-table source=sortedEvaluations as |t|}}
|
||||
{{#t.head}}
|
||||
<th>ID</th>
|
||||
<th>Priority</th>
|
||||
<th>Triggered By</th>
|
||||
<th>Status</th>
|
||||
<th>Placement Failures</th>
|
||||
{{/t.head}}
|
||||
{{#t.body as |row|}}
|
||||
<tr data-test-evaluation="{{row.model.shortId}}">
|
||||
<td data-test-id>{{row.model.shortId}}</td>
|
||||
<td data-test-priority>{{row.model.priority}}</td>
|
||||
<td data-test-triggered-by>{{row.model.triggeredBy}}</td>
|
||||
<td data-test-status>{{row.model.status}}</td>
|
||||
<td data-test-blocked>
|
||||
{{#if (eq row.model.status "blocked")}}
|
||||
N/A - In Progress
|
||||
{{else if row.model.hasPlacementFailures}}
|
||||
True
|
||||
{{else}}
|
||||
False
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/t.body}}
|
||||
{{/list-table}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/gutter-menu}}
|
||||
{{component (concat "job-page/" model.templateType)
|
||||
job=model
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
onNamespaceChange=(action "gotoJobs")
|
||||
gotoJob=(action "gotoJob")
|
||||
gotoTaskGroup=(action "gotoTaskGroup")}}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="tabs is-subnav">
|
||||
<div data-test-subnav="job" class="tabs is-subnav">
|
||||
<ul>
|
||||
<li data-test-tab="overview">{{#link-to "jobs.job.index" job activeClass="is-active"}}Overview{{/link-to}}</li>
|
||||
<li data-test-tab="definition">{{#link-to "jobs.job.definition" job activeClass="is-active"}}Definition{{/link-to}}</li>
|
||||
|
|
|
@ -100,12 +100,21 @@
|
|||
</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>
|
||||
{{#if allocations.length}}
|
||||
<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>
|
||||
</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 Allocations</h3>
|
||||
<p class="empty-message-body">No allocations have been placed.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/list-pagination}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@ export function findLeader(schema) {
|
|||
}
|
||||
|
||||
export default function() {
|
||||
const server = this;
|
||||
this.timing = 0; // delay for each request, automatically set to 0 during testing
|
||||
|
||||
this.namespace = 'v1';
|
||||
|
@ -58,6 +59,22 @@ export default function() {
|
|||
return this.serialize(deployments.where({ jobId: params.id }));
|
||||
});
|
||||
|
||||
this.post('/job/:id/periodic/force', function(schema, { params }) {
|
||||
// Create the child job
|
||||
const parent = schema.jobs.find(params.id);
|
||||
|
||||
// Use the server instead of the schema to leverage the job factory
|
||||
server.create('job', 'periodicChild', {
|
||||
parentId: parent.id,
|
||||
namespaceId: parent.namespaceId,
|
||||
namespace: parent.namespace,
|
||||
createAllocations: parent.createAllocations,
|
||||
});
|
||||
|
||||
// Return bogus, since the response is normally just eval information
|
||||
return new Response(200, {}, '{}');
|
||||
});
|
||||
|
||||
this.get('/deployment/:id');
|
||||
|
||||
this.get('/job/:id/evaluations', function({ evaluations }, { params }) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Factory, faker } from 'ember-cli-mirage';
|
||||
import { Factory, faker, trait } from 'ember-cli-mirage';
|
||||
|
||||
export default Factory.extend({
|
||||
// Hidden property used to compute the Summary hash
|
||||
|
@ -6,17 +6,27 @@ export default Factory.extend({
|
|||
|
||||
JobID: '',
|
||||
|
||||
Summary: function() {
|
||||
return this.groupNames.reduce((summary, group) => {
|
||||
summary[group] = {
|
||||
Queued: faker.random.number(10),
|
||||
Complete: faker.random.number(10),
|
||||
Failed: faker.random.number(10),
|
||||
Running: faker.random.number(10),
|
||||
Starting: faker.random.number(10),
|
||||
Lost: faker.random.number(10),
|
||||
};
|
||||
return summary;
|
||||
}, {});
|
||||
},
|
||||
withSummary: trait({
|
||||
Summary: function() {
|
||||
return this.groupNames.reduce((summary, group) => {
|
||||
summary[group] = {
|
||||
Queued: faker.random.number(10),
|
||||
Complete: faker.random.number(10),
|
||||
Failed: faker.random.number(10),
|
||||
Running: faker.random.number(10),
|
||||
Starting: faker.random.number(10),
|
||||
Lost: faker.random.number(10),
|
||||
};
|
||||
return summary;
|
||||
}, {});
|
||||
},
|
||||
}),
|
||||
|
||||
withChildren: trait({
|
||||
Children: () => ({
|
||||
Pending: faker.random.number(10),
|
||||
Running: faker.random.number(10),
|
||||
Dead: faker.random.number(10),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Factory, faker } from 'ember-cli-mirage';
|
||||
import { Factory, faker, trait } from 'ember-cli-mirage';
|
||||
import { provide, provider, pickOne } from '../utils';
|
||||
import { DATACENTERS } from '../common';
|
||||
|
||||
|
@ -22,10 +22,48 @@ export default Factory.extend({
|
|||
faker.list.random(...DATACENTERS)
|
||||
),
|
||||
|
||||
periodic: () => Math.random() > 0.5,
|
||||
parameterized() {
|
||||
return !this.periodic;
|
||||
},
|
||||
childrenCount: () => faker.random.number({ min: 1, max: 5 }),
|
||||
|
||||
periodic: trait({
|
||||
type: 'batch',
|
||||
periodic: true,
|
||||
// periodic details object
|
||||
// serializer update for bool vs details object
|
||||
periodicDetails: () => ({
|
||||
Enabled: true,
|
||||
ProhibitOverlap: true,
|
||||
Spec: '*/5 * * * * *',
|
||||
SpecType: 'cron',
|
||||
TimeZone: 'UTC',
|
||||
}),
|
||||
}),
|
||||
|
||||
parameterized: trait({
|
||||
type: 'batch',
|
||||
parameterized: true,
|
||||
// parameterized details object
|
||||
// serializer update for bool vs details object
|
||||
parameterizedDetails: () => ({
|
||||
MetaOptional: null,
|
||||
MetaRequired: null,
|
||||
Payload: Math.random() > 0.5 ? 'required' : null,
|
||||
}),
|
||||
}),
|
||||
|
||||
periodicChild: trait({
|
||||
// Periodic children need a parent job,
|
||||
// It is the Periodic job's responsibility to create
|
||||
// periodicChild jobs and provide a parent job.
|
||||
type: 'batch',
|
||||
}),
|
||||
|
||||
parameterizedChild: trait({
|
||||
// Parameterized children need a parent job,
|
||||
// It is the Parameterized job's responsibility to create
|
||||
// parameterizedChild jobs and provide a parent job.
|
||||
type: 'batch',
|
||||
payload: window.btoa(faker.lorem.sentence()),
|
||||
}),
|
||||
|
||||
createIndex: i => i,
|
||||
modifyIndex: () => faker.random.number({ min: 10, max: 2000 }),
|
||||
|
@ -70,7 +108,8 @@ export default Factory.extend({
|
|||
});
|
||||
}
|
||||
|
||||
const jobSummary = server.create('job-summary', {
|
||||
const hasChildren = job.periodic || job.parameterized;
|
||||
const jobSummary = server.create('job-summary', hasChildren ? 'withChildren' : 'withSummary', {
|
||||
groupNames: groups.mapBy('name'),
|
||||
job,
|
||||
});
|
||||
|
@ -102,5 +141,25 @@ export default Factory.extend({
|
|||
modifyIndex: 4000,
|
||||
});
|
||||
}
|
||||
|
||||
if (job.periodic) {
|
||||
// Create periodicChild jobs
|
||||
server.createList('job', job.childrenCount, 'periodicChild', {
|
||||
parentId: job.id,
|
||||
namespaceId: job.namespaceId,
|
||||
namespace: job.namespace,
|
||||
createAllocations: job.createAllocations,
|
||||
});
|
||||
}
|
||||
|
||||
if (job.parameterized) {
|
||||
// Create parameterizedChild jobs
|
||||
server.createList('job', job.childrenCount, 'parameterizedChild', {
|
||||
parentId: job.id,
|
||||
namespaceId: job.namespaceId,
|
||||
namespace: job.namespace,
|
||||
createAllocations: job.createAllocations,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
"precommit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"ui/{app,tests,config,lib,mirage}/**/*.js": [
|
||||
"{app,tests,config,lib,mirage}/**/*.js": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"ui/app/styles/**/*.*": [
|
||||
"app/styles/**/*.*": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
]
|
||||
|
|
|
@ -1,361 +1,38 @@
|
|||
import { get } from '@ember/object';
|
||||
import { click, findAll, currentURL, find, visit } from 'ember-native-dom-helpers';
|
||||
import moment from 'moment';
|
||||
import { test } from 'qunit';
|
||||
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
|
||||
import moduleForJob from 'nomad-ui/tests/helpers/module-for-job';
|
||||
|
||||
const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0);
|
||||
moduleForJob('Acceptance | job detail (batch)', () => server.create('job', { type: 'batch' }));
|
||||
moduleForJob('Acceptance | job detail (system)', () => server.create('job', { type: 'system' }));
|
||||
moduleForJob('Acceptance | job detail (periodic)', () => server.create('job', 'periodic'));
|
||||
|
||||
let job;
|
||||
moduleForJob('Acceptance | job detail (parameterized)', () =>
|
||||
server.create('job', 'parameterized')
|
||||
);
|
||||
|
||||
moduleForAcceptance('Acceptance | job detail', {
|
||||
beforeEach() {
|
||||
server.create('node');
|
||||
job = server.create('job', { type: 'service' });
|
||||
visit(`/jobs/${job.id}`);
|
||||
moduleForJob('Acceptance | job detail (periodic child)', () => {
|
||||
const parent = server.create('job', 'periodic');
|
||||
return server.db.jobs.where({ parentId: parent.id })[0];
|
||||
});
|
||||
|
||||
moduleForJob('Acceptance | job detail (parameterized child)', () => {
|
||||
const parent = server.create('job', 'parameterized');
|
||||
return server.db.jobs.where({ parentId: parent.id })[0];
|
||||
});
|
||||
|
||||
moduleForJob('Acceptance | job detail (service)', () => server.create('job', { type: 'service' }), {
|
||||
'the subnav links to deployment': (job, assert) => {
|
||||
click(find('[data-test-tab="deployments"] a'));
|
||||
andThen(() => {
|
||||
assert.equal(currentURL(), `/jobs/${job.id}/deployments`);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
test('visiting /jobs/:job_id', function(assert) {
|
||||
assert.equal(currentURL(), `/jobs/${job.id}`);
|
||||
});
|
||||
let job;
|
||||
|
||||
test('breadcrumbs includes job name and link back to the jobs list', function(assert) {
|
||||
assert.equal(
|
||||
find('[data-test-breadcrumb="Jobs"]').textContent,
|
||||
'Jobs',
|
||||
'First breadcrumb says jobs'
|
||||
);
|
||||
assert.equal(
|
||||
find(`[data-test-breadcrumb="${job.name}"]`).textContent,
|
||||
job.name,
|
||||
'Second breadcrumb says the job name'
|
||||
);
|
||||
|
||||
click(find('[data-test-breadcrumb="Jobs"]'));
|
||||
andThen(() => {
|
||||
assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs');
|
||||
});
|
||||
});
|
||||
|
||||
test('the subnav includes links to definition, versions, and deployments when type = service', function(
|
||||
assert
|
||||
) {
|
||||
const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent);
|
||||
assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link');
|
||||
assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link');
|
||||
assert.ok(subnavLabels.some(label => label === 'Deployments'), 'Deployments link');
|
||||
});
|
||||
|
||||
test('the subnav includes links to definition and versions when type != service', function(assert) {
|
||||
job = server.create('job', { type: 'batch' });
|
||||
visit(`/jobs/${job.id}`);
|
||||
|
||||
andThen(() => {
|
||||
const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent);
|
||||
assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link');
|
||||
assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link');
|
||||
assert.notOk(subnavLabels.some(label => label === 'Deployments'), 'Deployments link');
|
||||
});
|
||||
});
|
||||
|
||||
test('the job detail page should contain basic information about the job', function(assert) {
|
||||
assert.ok(find('[data-test-job-status]').textContent.includes(job.status), 'Status');
|
||||
assert.ok(find('[data-test-job-stat="type"]').textContent.includes(job.type), 'Type');
|
||||
assert.ok(find('[data-test-job-stat="priority"]').textContent.includes(job.priority), 'Priority');
|
||||
assert.notOk(find('[data-test-job-stat="namespace"]'), 'Namespace is not included');
|
||||
});
|
||||
|
||||
test('the job detail page should list all task groups', function(assert) {
|
||||
assert.equal(
|
||||
findAll('[data-test-task-group]').length,
|
||||
server.db.taskGroups.where({ jobId: job.id }).length
|
||||
);
|
||||
});
|
||||
|
||||
test('each row in the task group table should show basic information about the task group', function(
|
||||
assert
|
||||
) {
|
||||
const taskGroup = job.taskGroupIds.map(id => server.db.taskGroups.find(id)).sortBy('name')[0];
|
||||
const taskGroupRow = find('[data-test-task-group]');
|
||||
const tasks = server.db.tasks.where({ taskGroupId: taskGroup.id });
|
||||
const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0);
|
||||
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-name]').textContent.trim(),
|
||||
taskGroup.name,
|
||||
'Name'
|
||||
);
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-count]').textContent.trim(),
|
||||
taskGroup.count,
|
||||
'Count'
|
||||
);
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-cpu]').textContent.trim(),
|
||||
`${sum(tasks, 'Resources.CPU')} MHz`,
|
||||
'Reserved CPU'
|
||||
);
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-mem]').textContent.trim(),
|
||||
`${sum(tasks, 'Resources.MemoryMB')} MiB`,
|
||||
'Reserved Memory'
|
||||
);
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-disk]').textContent.trim(),
|
||||
`${taskGroup.ephemeralDisk.SizeMB} MiB`,
|
||||
'Reserved Disk'
|
||||
);
|
||||
});
|
||||
|
||||
test('the allocations diagram lists all allocation status figures', function(assert) {
|
||||
const jobSummary = server.db.jobSummaries.findBy({ jobId: job.id });
|
||||
const statusCounts = Object.keys(jobSummary.Summary).reduce(
|
||||
(counts, key) => {
|
||||
const group = jobSummary.Summary[key];
|
||||
counts.queued += group.Queued;
|
||||
counts.starting += group.Starting;
|
||||
counts.running += group.Running;
|
||||
counts.complete += group.Complete;
|
||||
counts.failed += group.Failed;
|
||||
counts.lost += group.Lost;
|
||||
return counts;
|
||||
},
|
||||
{ queued: 0, starting: 0, running: 0, complete: 0, failed: 0, lost: 0 }
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="queued"]').textContent,
|
||||
statusCounts.queued,
|
||||
`${statusCounts.queued} are queued`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="starting"]').textContent,
|
||||
statusCounts.starting,
|
||||
`${statusCounts.starting} are starting`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="running"]').textContent,
|
||||
statusCounts.running,
|
||||
`${statusCounts.running} are running`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="complete"]').textContent,
|
||||
statusCounts.complete,
|
||||
`${statusCounts.complete} are complete`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="failed"]').textContent,
|
||||
statusCounts.failed,
|
||||
`${statusCounts.failed} are failed`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="lost"]').textContent,
|
||||
statusCounts.lost,
|
||||
`${statusCounts.lost} are lost`
|
||||
);
|
||||
});
|
||||
|
||||
test('there is no active deployment section when the job has no active deployment', function(
|
||||
assert
|
||||
) {
|
||||
// TODO: it would be better to not visit two different job pages in one test, but this
|
||||
// way is much more convenient.
|
||||
job = server.create('job', { noActiveDeployment: true, type: 'service' });
|
||||
visit(`/jobs/${job.id}`);
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(find('[data-test-active-deployment]'), 'No active deployment');
|
||||
});
|
||||
});
|
||||
|
||||
test('the active deployment section shows up for the currently running deployment', function(
|
||||
assert
|
||||
) {
|
||||
job = server.create('job', { activeDeployment: true, type: 'service' });
|
||||
const deployment = server.db.deployments.where({ jobId: job.id })[0];
|
||||
const taskGroupSummaries = server.db.deploymentTaskGroupSummaries.where({
|
||||
deploymentId: deployment.id,
|
||||
});
|
||||
const version = server.db.jobVersions.findBy({
|
||||
jobId: job.id,
|
||||
version: deployment.versionNumber,
|
||||
});
|
||||
visit(`/jobs/${job.id}`);
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(find('[data-test-active-deployment]'), 'Active deployment');
|
||||
assert.equal(
|
||||
find('[data-test-active-deployment-stat="id"]').textContent.trim(),
|
||||
deployment.id.split('-')[0],
|
||||
'The active deployment is the most recent running deployment'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-active-deployment-stat="submit-time"]').textContent.trim(),
|
||||
moment(version.submitTime / 1000000).fromNow(),
|
||||
'Time since the job was submitted is in the active deployment header'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="canaries"]').textContent.trim(),
|
||||
`${sum(taskGroupSummaries, 'placedCanaries')} / ${sum(
|
||||
taskGroupSummaries,
|
||||
'desiredCanaries'
|
||||
)}`,
|
||||
'Canaries, both places and desired, are in the metrics'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="placed"]').textContent.trim(),
|
||||
sum(taskGroupSummaries, 'placedAllocs'),
|
||||
'Placed allocs aggregates across task groups'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="desired"]').textContent.trim(),
|
||||
sum(taskGroupSummaries, 'desiredTotal'),
|
||||
'Desired allocs aggregates across task groups'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="healthy"]').textContent.trim(),
|
||||
sum(taskGroupSummaries, 'healthyAllocs'),
|
||||
'Healthy allocs aggregates across task groups'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="unhealthy"]').textContent.trim(),
|
||||
sum(taskGroupSummaries, 'unhealthyAllocs'),
|
||||
'Unhealthy allocs aggregates across task groups'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-notification]').textContent.trim(),
|
||||
deployment.statusDescription,
|
||||
'Status description is in the metrics block'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('the active deployment section can be expanded to show task groups and allocations', function(
|
||||
assert
|
||||
) {
|
||||
job = server.create('job', { activeDeployment: true, type: 'service' });
|
||||
visit(`/jobs/${job.id}`);
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(find('[data-test-deployment-task-groups]'), 'Task groups not found');
|
||||
assert.notOk(find('[data-test-deployment-allocations]'), 'Allocations not found');
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
click('[data-test-deployment-toggle-details]');
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(find('[data-test-deployment-task-groups]'), 'Task groups found');
|
||||
assert.ok(find('[data-test-deployment-allocations]'), 'Allocations found');
|
||||
});
|
||||
});
|
||||
|
||||
test('the evaluations table lists evaluations sorted by modify index', function(assert) {
|
||||
job = server.create('job');
|
||||
const evaluations = server.db.evaluations
|
||||
.where({ jobId: job.id })
|
||||
.sortBy('modifyIndex')
|
||||
.reverse();
|
||||
|
||||
visit(`/jobs/${job.id}`);
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
findAll('[data-test-evaluation]').length,
|
||||
evaluations.length,
|
||||
'A row for each evaluation'
|
||||
);
|
||||
|
||||
evaluations.forEach((evaluation, index) => {
|
||||
const row = findAll('[data-test-evaluation]')[index];
|
||||
assert.equal(
|
||||
row.querySelector('[data-test-id]').textContent,
|
||||
evaluation.id.split('-')[0],
|
||||
`Short ID, row ${index}`
|
||||
);
|
||||
});
|
||||
|
||||
const firstEvaluation = evaluations[0];
|
||||
const row = find('[data-test-evaluation]');
|
||||
assert.equal(
|
||||
row.querySelector('[data-test-priority]').textContent,
|
||||
'' + firstEvaluation.priority,
|
||||
'Priority'
|
||||
);
|
||||
assert.equal(
|
||||
row.querySelector('[data-test-triggered-by]').textContent,
|
||||
firstEvaluation.triggeredBy,
|
||||
'Triggered By'
|
||||
);
|
||||
assert.equal(
|
||||
row.querySelector('[data-test-status]').textContent,
|
||||
firstEvaluation.status,
|
||||
'Status'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the job has placement failures, they are called out', function(assert) {
|
||||
job = server.create('job', { failedPlacements: true });
|
||||
const failedEvaluation = server.db.evaluations
|
||||
.where({ jobId: job.id })
|
||||
.filter(evaluation => evaluation.failedTGAllocs)
|
||||
.sortBy('modifyIndex')
|
||||
.reverse()[0];
|
||||
|
||||
const failedTaskGroupNames = Object.keys(failedEvaluation.failedTGAllocs);
|
||||
|
||||
visit(`/jobs/${job.id}`);
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(find('[data-test-placement-failures]'), 'Placement failures section found');
|
||||
|
||||
const taskGroupLabels = findAll('[data-test-placement-failure-task-group]').map(title =>
|
||||
title.textContent.trim()
|
||||
);
|
||||
failedTaskGroupNames.forEach(name => {
|
||||
assert.ok(
|
||||
taskGroupLabels.find(label => label.includes(name)),
|
||||
`${name} included in placement failures list`
|
||||
);
|
||||
assert.ok(
|
||||
taskGroupLabels.find(label =>
|
||||
label.includes(failedEvaluation.failedTGAllocs[name].CoalescedFailures + 1)
|
||||
),
|
||||
'The number of unplaced allocs = CoalescedFailures + 1'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('when the job has no placement failures, the placement failures section is gone', function(
|
||||
assert
|
||||
) {
|
||||
job = server.create('job', { noFailedPlacements: true });
|
||||
visit(`/jobs/${job.id}`);
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(find('[data-test-placement-failures]'), 'Placement failures section not found');
|
||||
});
|
||||
});
|
||||
|
||||
test('when the job is not found, an error message is shown, but the URL persists', function(
|
||||
assert
|
||||
) {
|
||||
test('when the job is not found, an error message is shown, but the URL persists', function(assert) {
|
||||
visit('/jobs/not-a-real-job');
|
||||
|
||||
andThen(() => {
|
||||
|
@ -378,14 +55,12 @@ moduleForAcceptance('Acceptance | job detail (with namespaces)', {
|
|||
beforeEach() {
|
||||
server.createList('namespace', 2);
|
||||
server.create('node');
|
||||
job = server.create('job', { namespaceId: server.db.namespaces[1].name });
|
||||
job = server.create('job', { type: 'service', namespaceId: server.db.namespaces[1].name });
|
||||
server.createList('job', 3, { namespaceId: server.db.namespaces[0].name });
|
||||
},
|
||||
});
|
||||
|
||||
test('when there are namespaces, the job detail page states the namespace for the job', function(
|
||||
assert
|
||||
) {
|
||||
test('when there are namespaces, the job detail page states the namespace for the job', function(assert) {
|
||||
const namespace = server.db.namespaces.find(job.namespaceId);
|
||||
visit(`/jobs/${job.id}?namespace=${namespace.name}`);
|
||||
|
||||
|
@ -397,9 +72,7 @@ test('when there are namespaces, the job detail page states the namespace for th
|
|||
});
|
||||
});
|
||||
|
||||
test('when switching namespaces, the app redirects to /jobs with the new namespace', function(
|
||||
assert
|
||||
) {
|
||||
test('when switching namespaces, the app redirects to /jobs with the new namespace', function(assert) {
|
||||
const namespace = server.db.namespaces.find(job.namespaceId);
|
||||
const otherNamespace = server.db.namespaces.toArray().find(ns => ns !== namespace).name;
|
||||
const label = otherNamespace === 'default' ? 'Default Namespace' : otherNamespace;
|
||||
|
|
|
@ -59,7 +59,7 @@ test('each job row should contain information about the job', function(assert) {
|
|||
job.status,
|
||||
'Status'
|
||||
);
|
||||
assert.equal(jobRow.querySelector('[data-test-job-type]').textContent, job.type, 'Type');
|
||||
assert.equal(jobRow.querySelector('[data-test-job-type]').textContent, typeForJob(job), 'Type');
|
||||
assert.equal(
|
||||
jobRow.querySelector('[data-test-job-priority]').textContent,
|
||||
job.priority,
|
||||
|
@ -99,9 +99,7 @@ test('when there are no jobs, there is an empty message', function(assert) {
|
|||
});
|
||||
});
|
||||
|
||||
test('when there are jobs, but no matches for a search result, there is an empty message', function(
|
||||
assert
|
||||
) {
|
||||
test('when there are jobs, but no matches for a search result, there is an empty message', function(assert) {
|
||||
server.create('job', { name: 'cat 1' });
|
||||
server.create('job', { name: 'cat 2' });
|
||||
|
||||
|
@ -117,9 +115,7 @@ test('when there are jobs, but no matches for a search result, there is an empty
|
|||
});
|
||||
});
|
||||
|
||||
test('when the namespace query param is set, only matching jobs are shown and the namespace value is forwarded to app state', function(
|
||||
assert
|
||||
) {
|
||||
test('when the namespace query param is set, only matching jobs are shown and the namespace value is forwarded to app state', function(assert) {
|
||||
server.createList('namespace', 2);
|
||||
const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id });
|
||||
const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id });
|
||||
|
@ -144,9 +140,7 @@ test('when the namespace query param is set, only matching jobs are shown and th
|
|||
});
|
||||
});
|
||||
|
||||
test('when accessing jobs is forbidden, show a message with a link to the tokens page', function(
|
||||
assert
|
||||
) {
|
||||
test('when accessing jobs is forbidden, show a message with a link to the tokens page', function(assert) {
|
||||
server.pretender.get('/v1/jobs', () => [403, {}, null]);
|
||||
|
||||
visit('/jobs');
|
||||
|
@ -163,3 +157,7 @@ test('when accessing jobs is forbidden, show a message with a link to the tokens
|
|||
assert.equal(currentURL(), '/settings/tokens');
|
||||
});
|
||||
});
|
||||
|
||||
function typeForJob(job) {
|
||||
return job.periodic ? 'periodic' : job.parameterized ? 'parameterized' : job.type;
|
||||
}
|
||||
|
|
45
ui/tests/helpers/module-for-job.js
Normal file
45
ui/tests/helpers/module-for-job.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { test } from 'qunit';
|
||||
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
|
||||
|
||||
export default function moduleForJob(title, jobFactory, additionalTests) {
|
||||
let job;
|
||||
|
||||
moduleForAcceptance(title, {
|
||||
beforeEach() {
|
||||
server.create('node');
|
||||
job = jobFactory();
|
||||
visit(`/jobs/${job.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
test('visiting /jobs/:job_id', function(assert) {
|
||||
assert.equal(currentURL(), `/jobs/${job.id}`);
|
||||
});
|
||||
|
||||
test('the subnav links to overview', function(assert) {
|
||||
click(find('[data-test-tab="overview"] a'));
|
||||
andThen(() => {
|
||||
assert.equal(currentURL(), `/jobs/${job.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('the subnav links to definition', function(assert) {
|
||||
click(find('[data-test-tab="definition"] a'));
|
||||
andThen(() => {
|
||||
assert.equal(currentURL(), `/jobs/${job.id}/definition`);
|
||||
});
|
||||
});
|
||||
|
||||
test('the subnav links to versions', function(assert) {
|
||||
click(find('[data-test-tab="versions"] a'));
|
||||
andThen(() => {
|
||||
assert.equal(currentURL(), `/jobs/${job.id}/versions`);
|
||||
});
|
||||
});
|
||||
|
||||
for (var testName in additionalTests) {
|
||||
test(testName, function(assert) {
|
||||
additionalTests[testName](job, assert);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import { run } from '@ember/runloop';
|
|||
import { merge } from '@ember/polyfills';
|
||||
import Application from '../../app';
|
||||
import config from '../../config/environment';
|
||||
import registerPowerSelectHelpers from '../../tests/helpers/ember-power-select';
|
||||
import registerPowerSelectHelpers from 'ember-power-select/test-support/helpers';
|
||||
|
||||
registerPowerSelectHelpers();
|
||||
|
||||
|
|
137
ui/tests/integration/job-page/parts/body-test.js
Normal file
137
ui/tests/integration/job-page/parts/body-test.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
import { run } from '@ember/runloop';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import { click, find, findAll } from 'ember-native-dom-helpers';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
|
||||
moduleForComponent('job-page/parts/body', 'Integration | Component | job-page/parts/body', {
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
window.localStorage.clear();
|
||||
this.server = startMirage();
|
||||
this.server.createList('namespace', 3);
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
},
|
||||
});
|
||||
|
||||
test('includes a subnav for the job', function(assert) {
|
||||
this.set('job', {});
|
||||
this.set('onNamespaceChange', () => {});
|
||||
|
||||
this.render(hbs`
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<div class="inner-content">Inner content</div>
|
||||
{{/job-page/parts/body}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(find('[data-test-subnav="job"]'), 'Job subnav is rendered');
|
||||
});
|
||||
});
|
||||
|
||||
test('the subnav includes the deployments link when the job is a service', function(assert) {
|
||||
const store = getOwner(this).lookup('service:store');
|
||||
let job;
|
||||
|
||||
run(() => {
|
||||
job = store.createRecord('job', {
|
||||
id: 'service-job',
|
||||
type: 'service',
|
||||
});
|
||||
});
|
||||
|
||||
this.set('job', job);
|
||||
this.set('onNamespaceChange', () => {});
|
||||
|
||||
this.render(hbs`
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<div class="inner-content">Inner content</div>
|
||||
{{/job-page/parts/body}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent);
|
||||
assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link');
|
||||
assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link');
|
||||
assert.ok(subnavLabels.some(label => label === 'Deployments'), 'Deployments link');
|
||||
});
|
||||
});
|
||||
|
||||
test('the subnav does not include the deployments link when the job is not a service', function(assert) {
|
||||
const store = getOwner(this).lookup('service:store');
|
||||
let job;
|
||||
|
||||
run(() => {
|
||||
job = store.createRecord('job', {
|
||||
id: 'batch-job',
|
||||
type: 'batch',
|
||||
});
|
||||
});
|
||||
|
||||
this.set('job', job);
|
||||
this.set('onNamespaceChange', () => {});
|
||||
|
||||
this.render(hbs`
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<div class="inner-content">Inner content</div>
|
||||
{{/job-page/parts/body}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent);
|
||||
assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link');
|
||||
assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link');
|
||||
assert.notOk(subnavLabels.some(label => label === 'Deployments'), 'Deployments link');
|
||||
});
|
||||
});
|
||||
|
||||
test('body yields content to a section after the subnav', function(assert) {
|
||||
this.set('job', {});
|
||||
this.set('onNamespaceChange', () => {});
|
||||
|
||||
this.render(hbs`
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<div class="inner-content">Inner content</div>
|
||||
{{/job-page/parts/body}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(
|
||||
find('[data-test-page-content] .section > .inner-content'),
|
||||
'Content is rendered in a section in a gutter menu'
|
||||
);
|
||||
assert.ok(
|
||||
find('[data-test-subnav="job"] + .section > .inner-content'),
|
||||
'Content is rendered immediately after the subnav'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('onNamespaceChange action is called when the namespace changes in the nested gutter menu', function(assert) {
|
||||
const namespaceSpy = sinon.spy();
|
||||
|
||||
this.set('job', {});
|
||||
this.set('onNamespaceChange', namespaceSpy);
|
||||
|
||||
this.render(hbs`
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<div class="inner-content">Inner content</div>
|
||||
{{/job-page/parts/body}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
clickTrigger('[data-test-namespace-switcher]');
|
||||
click(findAll('.ember-power-select-option')[1]);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(namespaceSpy.calledOnce, 'Switching namespaces calls the onNamespaceChange action');
|
||||
});
|
||||
});
|
||||
});
|
206
ui/tests/integration/job-page/parts/children-test.js
Normal file
206
ui/tests/integration/job-page/parts/children-test.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
import { getOwner } from '@ember/application';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { run } from '@ember/runloop';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import { findAll, find, click } from 'ember-native-dom-helpers';
|
||||
import sinon from 'sinon';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
|
||||
moduleForComponent('job-page/parts/children', 'Integration | Component | job-page/parts/children', {
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
window.localStorage.clear();
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
},
|
||||
});
|
||||
|
||||
const props = (job, options = {}) =>
|
||||
assign(
|
||||
{
|
||||
job,
|
||||
sortProperty: 'name',
|
||||
sortDescending: true,
|
||||
currentPage: 1,
|
||||
gotoJob: () => {},
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
test('lists each child', function(assert) {
|
||||
let parent;
|
||||
|
||||
this.server.create('job', 'periodic', {
|
||||
id: 'parent',
|
||||
childrenCount: 3,
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
run(() => {
|
||||
parent = this.store.peekAll('job').findBy('plainId', 'parent');
|
||||
});
|
||||
|
||||
this.setProperties(props(parent));
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/children
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.equal(
|
||||
findAll('[data-test-job-name]').length,
|
||||
parent.get('children.length'),
|
||||
'A row for each child'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('eventually paginates', function(assert) {
|
||||
let parent;
|
||||
|
||||
this.server.create('job', 'periodic', {
|
||||
id: 'parent',
|
||||
childrenCount: 11,
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
run(() => {
|
||||
parent = this.store.peekAll('job').findBy('plainId', 'parent');
|
||||
});
|
||||
|
||||
this.setProperties(props(parent));
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/children
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
const childrenCount = parent.get('children.length');
|
||||
assert.ok(childrenCount > 10, 'Parent has more children than one page size');
|
||||
assert.equal(findAll('[data-test-job-name]').length, 10, 'Table length maxes out at 10');
|
||||
assert.ok(find('.pagination-next'), 'Next button is rendered');
|
||||
|
||||
assert.ok(
|
||||
new RegExp(`1.10.+?${childrenCount}`).test(find('.pagination-numbers').textContent.trim())
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('is sorted based on the sortProperty and sortDescending properties', function(assert) {
|
||||
let parent;
|
||||
|
||||
this.server.create('job', 'periodic', {
|
||||
id: 'parent',
|
||||
childrenCount: 3,
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
run(() => {
|
||||
parent = this.store.peekAll('job').findBy('plainId', 'parent');
|
||||
});
|
||||
|
||||
this.setProperties(props(parent));
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/children
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
const sortedChildren = parent.get('children').sortBy('name');
|
||||
const childRows = findAll('[data-test-job-name]');
|
||||
|
||||
sortedChildren.reverse().forEach((child, index) => {
|
||||
assert.equal(
|
||||
childRows[index].textContent.trim(),
|
||||
child.get('name'),
|
||||
`Child ${index} is ${child.get('name')}`
|
||||
);
|
||||
});
|
||||
|
||||
this.set('sortDescending', false);
|
||||
|
||||
sortedChildren.forEach((child, index) => {
|
||||
assert.equal(
|
||||
childRows[index].textContent.trim(),
|
||||
child.get('name'),
|
||||
`Child ${index} is ${child.get('name')}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('gotoJob is called when a job row is clicked', function(assert) {
|
||||
let parent;
|
||||
const gotoJobSpy = sinon.spy();
|
||||
|
||||
this.server.create('job', 'periodic', {
|
||||
id: 'parent',
|
||||
childrenCount: 1,
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
run(() => {
|
||||
parent = this.store.peekAll('job').findBy('plainId', 'parent');
|
||||
});
|
||||
|
||||
this.setProperties(
|
||||
props(parent, {
|
||||
gotoJob: gotoJobSpy,
|
||||
})
|
||||
);
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/children
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
click('tr.job-row');
|
||||
assert.ok(
|
||||
gotoJobSpy.withArgs(parent.get('children.firstObject')).calledOnce,
|
||||
'Clicking the job row calls the gotoJob action'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
65
ui/tests/integration/job-page/parts/evaluations-test.js
Normal file
65
ui/tests/integration/job-page/parts/evaluations-test.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { run } from '@ember/runloop';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import { findAll } from 'ember-native-dom-helpers';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
|
||||
moduleForComponent(
|
||||
'job-page/parts/evaluations',
|
||||
'Integration | Component | job-page/parts/evaluations',
|
||||
{
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
window.localStorage.clear();
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
test('lists all evaluations for the job', function(assert) {
|
||||
let job;
|
||||
|
||||
this.server.create('job', { noFailedPlacements: true, createAllocations: false });
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
run(() => {
|
||||
job = this.store.peekAll('job').get('firstObject');
|
||||
});
|
||||
|
||||
this.setProperties({ job });
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/evaluations job=job}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
const evaluationRows = findAll('[data-test-evaluation]');
|
||||
assert.equal(
|
||||
evaluationRows.length,
|
||||
job.get('evaluations.length'),
|
||||
'All evaluations are listed'
|
||||
);
|
||||
|
||||
job
|
||||
.get('evaluations')
|
||||
.sortBy('modifyIndex')
|
||||
.reverse()
|
||||
.forEach((evaluation, index) => {
|
||||
assert.equal(
|
||||
evaluationRows[index].querySelector('[data-test-id]').textContent.trim(),
|
||||
evaluation.get('shortId'),
|
||||
`Evaluation ${index} is ${evaluation.get('shortId')}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
import { getOwner } from '@ember/application';
|
||||
import { run } from '@ember/runloop';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import { findAll, find } from 'ember-native-dom-helpers';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';
|
||||
|
||||
moduleForComponent(
|
||||
'job-page/parts/placement-failures',
|
||||
'Integration | Component | job-page/parts/placement-failures',
|
||||
{
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
fragmentSerializerInitializer(getOwner(this));
|
||||
window.localStorage.clear();
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
test('when the job has placement failures, they are called out', function(assert) {
|
||||
this.server.create('job', { failedPlacements: true, createAllocations: false });
|
||||
this.store.findAll('job').then(jobs => {
|
||||
jobs.forEach(job => job.reload());
|
||||
});
|
||||
|
||||
return wait().then(() => {
|
||||
run(() => {
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
});
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/placement-failures job=job}})
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
const failedEvaluation = this.get('job.evaluations')
|
||||
.filterBy('hasPlacementFailures')
|
||||
.sortBy('modifyIndex')
|
||||
.reverse()
|
||||
.get('firstObject');
|
||||
const failedTGAllocs = failedEvaluation.get('failedTGAllocs');
|
||||
|
||||
assert.ok(find('[data-test-placement-failures]'), 'Placement failures section found');
|
||||
|
||||
const taskGroupLabels = findAll('[data-test-placement-failure-task-group]').map(title =>
|
||||
title.textContent.trim()
|
||||
);
|
||||
|
||||
failedTGAllocs.forEach(alloc => {
|
||||
const name = alloc.get('name');
|
||||
assert.ok(
|
||||
taskGroupLabels.find(label => label.includes(name)),
|
||||
`${name} included in placement failures list`
|
||||
);
|
||||
assert.ok(
|
||||
taskGroupLabels.find(label => label.includes(alloc.get('coalescedFailures') + 1)),
|
||||
'The number of unplaced allocs = CoalescedFailures + 1'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('when the job has no placement failures, the placement failures section is gone', function(assert) {
|
||||
this.server.create('job', { noFailedPlacements: true, createAllocations: false });
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
run(() => {
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
});
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/placement-failures job=job}})
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.notOk(find('[data-test-placement-failures]'), 'Placement failures section not found');
|
||||
});
|
||||
});
|
||||
});
|
141
ui/tests/integration/job-page/parts/running-deployment-test.js
Normal file
141
ui/tests/integration/job-page/parts/running-deployment-test.js
Normal file
|
@ -0,0 +1,141 @@
|
|||
import { getOwner } from '@ember/application';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import { click, find } from 'ember-native-dom-helpers';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import moment from 'moment';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';
|
||||
|
||||
moduleForComponent(
|
||||
'job-page/parts/running-deployment',
|
||||
'Integration | Component | job-page/parts/running-deployment',
|
||||
{
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
fragmentSerializerInitializer(getOwner(this));
|
||||
window.localStorage.clear();
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
test('there is no active deployment section when the job has no active deployment', function(assert) {
|
||||
this.server.create('job', {
|
||||
type: 'service',
|
||||
noActiveDeployment: true,
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
this.render(hbs`
|
||||
{{job-page/parts/running-deployment job=job}})
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.notOk(find('[data-test-active-deployment]'), 'No active deployment');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('the active deployment section shows up for the currently running deployment', function(assert) {
|
||||
this.server.create('job', { type: 'service', createAllocations: false, activeDeployment: true });
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
this.render(hbs`
|
||||
{{job-page/parts/running-deployment job=job}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
const deployment = this.get('job.runningDeployment');
|
||||
const version = deployment.get('version');
|
||||
|
||||
assert.ok(find('[data-test-active-deployment]'), 'Active deployment');
|
||||
assert.equal(
|
||||
find('[data-test-active-deployment-stat="id"]').textContent.trim(),
|
||||
deployment.get('shortId'),
|
||||
'The active deployment is the most recent running deployment'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-active-deployment-stat="submit-time"]').textContent.trim(),
|
||||
moment(version.get('submitTime')).fromNow(),
|
||||
'Time since the job was submitted is in the active deployment header'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="canaries"]').textContent.trim(),
|
||||
`${deployment.get('placedCanaries')} / ${deployment.get('desiredCanaries')}`,
|
||||
'Canaries, both places and desired, are in the metrics'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="placed"]').textContent.trim(),
|
||||
deployment.get('placedAllocs'),
|
||||
'Placed allocs aggregates across task groups'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="desired"]').textContent.trim(),
|
||||
deployment.get('desiredTotal'),
|
||||
'Desired allocs aggregates across task groups'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="healthy"]').textContent.trim(),
|
||||
deployment.get('healthyAllocs'),
|
||||
'Healthy allocs aggregates across task groups'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-metric="unhealthy"]').textContent.trim(),
|
||||
deployment.get('unhealthyAllocs'),
|
||||
'Unhealthy allocs aggregates across task groups'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-deployment-notification]').textContent.trim(),
|
||||
deployment.get('statusDescription'),
|
||||
'Status description is in the metrics block'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('the active deployment section can be expanded to show task groups and allocations', function(assert) {
|
||||
this.server.create('node');
|
||||
this.server.create('job', { type: 'service', activeDeployment: true });
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
this.render(hbs`
|
||||
{{job-page/parts/running-deployment job=job}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.notOk(find('[data-test-deployment-task-groups]'), 'Task groups not found');
|
||||
assert.notOk(find('[data-test-deployment-allocations]'), 'Allocations not found');
|
||||
|
||||
click('[data-test-deployment-toggle-details]');
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(find('[data-test-deployment-task-groups]'), 'Task groups found');
|
||||
assert.ok(find('[data-test-deployment-allocations]'), 'Allocations found');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
154
ui/tests/integration/job-page/parts/summary-test.js
Normal file
154
ui/tests/integration/job-page/parts/summary-test.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
import { getOwner } from '@ember/application';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import { find } from 'ember-native-dom-helpers';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';
|
||||
|
||||
moduleForComponent('job-page/parts/summary', 'Integration | Component | job-page/parts/summary', {
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
fragmentSerializerInitializer(getOwner(this));
|
||||
window.localStorage.clear();
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
},
|
||||
});
|
||||
|
||||
test('jobs with children use the children diagram', function(assert) {
|
||||
this.server.create('job', 'periodic', {
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/summary job=job}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(find('[data-test-children-status-bar]'), 'Children status bar found');
|
||||
assert.notOk(find('[data-test-allocation-status-bar]'), 'Allocation status bar not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('jobs without children use the allocations diagram', function(assert) {
|
||||
this.server.create('job', {
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/summary job=job}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(find('[data-test-allocation-status-bar]'), 'Allocation status bar found');
|
||||
assert.notOk(find('[data-test-children-status-bar]'), 'Children status bar not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('the allocations diagram lists all allocation status figures', function(assert) {
|
||||
this.server.create('job', {
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/summary job=job}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="queued"]').textContent,
|
||||
this.get('job.queuedAllocs'),
|
||||
`${this.get('job.queuedAllocs')} are queued`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="starting"]').textContent,
|
||||
this.get('job.startingAllocs'),
|
||||
`${this.get('job.startingAllocs')} are starting`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="running"]').textContent,
|
||||
this.get('job.runningAllocs'),
|
||||
`${this.get('job.runningAllocs')} are running`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="complete"]').textContent,
|
||||
this.get('job.completeAllocs'),
|
||||
`${this.get('job.completeAllocs')} are complete`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="failed"]').textContent,
|
||||
this.get('job.failedAllocs'),
|
||||
`${this.get('job.failedAllocs')} are failed`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="lost"]').textContent,
|
||||
this.get('job.lostAllocs'),
|
||||
`${this.get('job.lostAllocs')} are lost`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('the children diagram lists all children status figures', function(assert) {
|
||||
this.server.create('job', 'periodic', {
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
this.set('job', this.store.peekAll('job').get('firstObject'));
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/summary job=job}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="queued"]').textContent,
|
||||
this.get('job.pendingChildren'),
|
||||
`${this.get('job.pendingChildren')} are pending`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="running"]').textContent,
|
||||
this.get('job.runningChildren'),
|
||||
`${this.get('job.runningChildren')} are running`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-legend-value="complete"]').textContent,
|
||||
this.get('job.deadChildren'),
|
||||
`${this.get('job.deadChildren')} are dead`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
170
ui/tests/integration/job-page/parts/task-groups-test.js
Normal file
170
ui/tests/integration/job-page/parts/task-groups-test.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
import { getOwner } from '@ember/application';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import { click, findAll, find } from 'ember-native-dom-helpers';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import sinon from 'sinon';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';
|
||||
|
||||
moduleForComponent(
|
||||
'job-page/parts/task-groups',
|
||||
'Integration | Component | job-page/parts/task-groups',
|
||||
{
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
fragmentSerializerInitializer(getOwner(this));
|
||||
window.localStorage.clear();
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const props = (job, options = {}) =>
|
||||
assign(
|
||||
{
|
||||
job,
|
||||
sortProperty: 'name',
|
||||
sortDescending: true,
|
||||
gotoTaskGroup: () => {},
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
test('the job detail page should list all task groups', function(assert) {
|
||||
this.server.create('job', {
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job').then(jobs => {
|
||||
jobs.forEach(job => job.reload());
|
||||
});
|
||||
|
||||
return wait().then(() => {
|
||||
const job = this.store.peekAll('job').get('firstObject');
|
||||
this.setProperties(props(job));
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/task-groups
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.equal(
|
||||
findAll('[data-test-task-group]').length,
|
||||
job.get('taskGroups.length'),
|
||||
'One row per task group'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('each row in the task group table should show basic information about the task group', function(assert) {
|
||||
this.server.create('job', {
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job').then(jobs => {
|
||||
jobs.forEach(job => job.reload());
|
||||
});
|
||||
|
||||
return wait().then(() => {
|
||||
const job = this.store.peekAll('job').get('firstObject');
|
||||
const taskGroup = job
|
||||
.get('taskGroups')
|
||||
.sortBy('name')
|
||||
.reverse()
|
||||
.get('firstObject');
|
||||
|
||||
this.setProperties(props(job));
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/task-groups
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
const taskGroupRow = find('[data-test-task-group]');
|
||||
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-name]').textContent.trim(),
|
||||
taskGroup.get('name'),
|
||||
'Name'
|
||||
);
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-count]').textContent.trim(),
|
||||
taskGroup.get('count'),
|
||||
'Count'
|
||||
);
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-cpu]').textContent.trim(),
|
||||
`${taskGroup.get('reservedCPU')} MHz`,
|
||||
'Reserved CPU'
|
||||
);
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-mem]').textContent.trim(),
|
||||
`${taskGroup.get('reservedMemory')} MiB`,
|
||||
'Reserved Memory'
|
||||
);
|
||||
assert.equal(
|
||||
taskGroupRow.querySelector('[data-test-task-group-disk]').textContent.trim(),
|
||||
`${taskGroup.get('reservedEphemeralDisk')} MiB`,
|
||||
'Reserved Disk'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('gotoTaskGroup is called when task group rows are clicked', function(assert) {
|
||||
this.server.create('job', {
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job').then(jobs => {
|
||||
jobs.forEach(job => job.reload());
|
||||
});
|
||||
|
||||
return wait().then(() => {
|
||||
const taskGroupSpy = sinon.spy();
|
||||
const job = this.store.peekAll('job').get('firstObject');
|
||||
const taskGroup = job
|
||||
.get('taskGroups')
|
||||
.sortBy('name')
|
||||
.reverse()
|
||||
.get('firstObject');
|
||||
|
||||
this.setProperties(
|
||||
props(job, {
|
||||
gotoTaskGroup: taskGroupSpy,
|
||||
})
|
||||
);
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/parts/task-groups
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
gotoTaskGroup=gotoTaskGroup}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
click('[data-test-task-group]');
|
||||
assert.ok(
|
||||
taskGroupSpy.withArgs(taskGroup).calledOnce,
|
||||
'Clicking the task group row calls the gotoTaskGroup action'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
86
ui/tests/integration/job-page/periodic-test.js
Normal file
86
ui/tests/integration/job-page/periodic-test.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { getOwner } from '@ember/application';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import { click, findAll } from 'ember-native-dom-helpers';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
|
||||
moduleForComponent('job-page/periodic', 'Integration | Component | job-page/periodic', {
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
window.localStorage.clear();
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
},
|
||||
});
|
||||
|
||||
test('Clicking Force Launch launches a new periodic child job', function(assert) {
|
||||
const childrenCount = 3;
|
||||
|
||||
this.server.create('job', 'periodic', {
|
||||
id: 'parent',
|
||||
childrenCount,
|
||||
createAllocations: false,
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
const job = this.store.peekAll('job').findBy('plainId', 'parent');
|
||||
this.setProperties({
|
||||
job,
|
||||
sortProperty: 'name',
|
||||
sortDescending: true,
|
||||
currentPage: 1,
|
||||
gotoJob: () => {},
|
||||
});
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/periodic
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
`);
|
||||
|
||||
return wait().then(() => {
|
||||
const currentJobCount = server.db.jobs.length;
|
||||
|
||||
assert.equal(
|
||||
findAll('[data-test-job-name]').length,
|
||||
childrenCount,
|
||||
'The new periodic job launch is in the children list'
|
||||
);
|
||||
|
||||
click('[data-test-force-launch]');
|
||||
|
||||
return wait().then(() => {
|
||||
const id = job.get('plainId');
|
||||
const namespace = job.get('namespace.name') || 'default';
|
||||
|
||||
assert.ok(
|
||||
server.pretender.handledRequests
|
||||
.filterBy('method', 'POST')
|
||||
.find(req => req.url === `/v1/job/${id}/periodic/force?namespace=${namespace}`),
|
||||
'POST URL was correct'
|
||||
);
|
||||
|
||||
assert.ok(server.db.jobs.length, currentJobCount + 1, 'POST request was made');
|
||||
|
||||
return wait().then(() => {
|
||||
assert.equal(
|
||||
findAll('[data-test-job-name]').length,
|
||||
childrenCount + 1,
|
||||
'The new periodic job launch is in the children list'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue