diff --git a/ui/app/components/job-page/parts/evaluations.js b/ui/app/components/job-page/parts/evaluations.js
deleted file mode 100644
index 33f6054a7..000000000
--- a/ui/app/components/job-page/parts/evaluations.js
+++ /dev/null
@@ -1,12 +0,0 @@
-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();
- }),
-});
diff --git a/ui/app/controllers/jobs/job/evaluations.js b/ui/app/controllers/jobs/job/evaluations.js
new file mode 100644
index 000000000..f79228e7f
--- /dev/null
+++ b/ui/app/controllers/jobs/job/evaluations.js
@@ -0,0 +1,24 @@
+import { alias } from '@ember/object/computed';
+import Controller, { inject as controller } from '@ember/controller';
+import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
+import Sortable from 'nomad-ui/mixins/sortable';
+
+export default Controller.extend(WithNamespaceResetting, Sortable, {
+ jobController: controller('jobs.job'),
+
+ queryParams: {
+ sortProperty: 'sort',
+ sortDescending: 'desc',
+ },
+
+ sortProperty: 'modifyIndex',
+ sortDescending: true,
+
+ job: alias('model'),
+ evaluations: alias('model.evaluations'),
+
+ breadcrumbs: alias('jobController.breadcrumbs'),
+
+ listToSort: alias('evaluations'),
+ sortedEvaluations: alias('listSorted'),
+});
diff --git a/ui/app/router.js b/ui/app/router.js
index 494a2173d..ce0a1bc74 100644
--- a/ui/app/router.js
+++ b/ui/app/router.js
@@ -13,6 +13,7 @@ Router.map(function() {
this.route('definition');
this.route('versions');
this.route('deployments');
+ this.route('evaluations');
});
});
diff --git a/ui/app/routes/jobs/job/evaluations.js b/ui/app/routes/jobs/job/evaluations.js
new file mode 100644
index 000000000..640809db3
--- /dev/null
+++ b/ui/app/routes/jobs/job/evaluations.js
@@ -0,0 +1,19 @@
+import Route from '@ember/routing/route';
+import { collect } from '@ember/object/computed';
+import { watchRelationship } from 'nomad-ui/utils/properties/watch';
+import WithWatchers from 'nomad-ui/mixins/with-watchers';
+
+export default Route.extend(WithWatchers, {
+ model() {
+ const job = this.modelFor('jobs.job');
+ return job.get('evaluations').then(() => job);
+ },
+
+ startWatchers(controller, model) {
+ controller.set('watchEvaluations', this.get('watchEvaluations').perform(model));
+ },
+
+ watchEvaluations: watchRelationship('evaluations'),
+
+ watchers: collect('watchEvaluations'),
+});
diff --git a/ui/app/templates/components/job-page/batch.hbs b/ui/app/templates/components/job-page/batch.hbs
index d92573add..1ea0cedf9 100644
--- a/ui/app/templates/components/job-page/batch.hbs
+++ b/ui/app/templates/components/job-page/batch.hbs
@@ -29,6 +29,4 @@
sortProperty=sortProperty
sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}}
-
- {{job-page/parts/evaluations job=job}}
{{/job-page/parts/body}}
diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs
index cd3d18f0b..d9104b571 100644
--- a/ui/app/templates/components/job-page/parameterized-child.hbs
+++ b/ui/app/templates/components/job-page/parameterized-child.hbs
@@ -36,8 +36,6 @@
sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}}
- {{job-page/parts/evaluations job=job}}
-
Payload
diff --git a/ui/app/templates/components/job-page/parts/evaluations.hbs b/ui/app/templates/components/job-page/parts/evaluations.hbs
deleted file mode 100644
index f49a6f10b..000000000
--- a/ui/app/templates/components/job-page/parts/evaluations.hbs
+++ /dev/null
@@ -1,38 +0,0 @@
-
- Evaluations
-
-
- {{#if sortedEvaluations.length}}
- {{#list-table source=sortedEvaluations as |t|}}
- {{#t.head}}
-
ID |
-
Priority |
-
Triggered By |
-
Status |
-
Placement Failures |
- {{/t.head}}
- {{#t.body as |row|}}
-
- {{row.model.shortId}} |
- {{row.model.priority}} |
- {{row.model.triggeredBy}} |
- {{row.model.status}} |
-
- {{#if (eq row.model.status "blocked")}}
- N/A - In Progress
- {{else if row.model.hasPlacementFailures}}
- True
- {{else}}
- False
- {{/if}}
- |
-
- {{/t.body}}
- {{/list-table}}
- {{else}}
-
-
No Evaluations
-
This is most likely due to garbage collection.
-
- {{/if}}
-
diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs
index 946ce2579..117a86679 100644
--- a/ui/app/templates/components/job-page/periodic-child.hbs
+++ b/ui/app/templates/components/job-page/periodic-child.hbs
@@ -35,6 +35,4 @@
sortProperty=sortProperty
sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}}
-
- {{job-page/parts/evaluations job=job}}
{{/job-page/parts/body}}
diff --git a/ui/app/templates/components/job-page/service.hbs b/ui/app/templates/components/job-page/service.hbs
index 80e2ab721..b724b7ab5 100644
--- a/ui/app/templates/components/job-page/service.hbs
+++ b/ui/app/templates/components/job-page/service.hbs
@@ -31,6 +31,4 @@
sortProperty=sortProperty
sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}}
-
- {{job-page/parts/evaluations job=job}}
{{/job-page/parts/body}}
diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs
index d92573add..1ea0cedf9 100644
--- a/ui/app/templates/components/job-page/system.hbs
+++ b/ui/app/templates/components/job-page/system.hbs
@@ -29,6 +29,4 @@
sortProperty=sortProperty
sortDescending=sortDescending
gotoTaskGroup=gotoTaskGroup}}
-
- {{job-page/parts/evaluations job=job}}
{{/job-page/parts/body}}
diff --git a/ui/app/templates/components/list-table/sort-by.hbs b/ui/app/templates/components/list-table/sort-by.hbs
index 64521875e..7e513143a 100644
--- a/ui/app/templates/components/list-table/sort-by.hbs
+++ b/ui/app/templates/components/list-table/sort-by.hbs
@@ -1,3 +1,5 @@
-{{#link-to (query-params sortProperty=prop sortDescending=shouldSortDescending)}}
+{{#link-to
+ (query-params sortProperty=prop sortDescending=shouldSortDescending)
+ data-test-sort-by=prop}}
{{yield}}
{{/link-to}}
diff --git a/ui/app/templates/jobs/job/evaluations.hbs b/ui/app/templates/jobs/job/evaluations.hbs
new file mode 100644
index 000000000..7b46c0c25
--- /dev/null
+++ b/ui/app/templates/jobs/job/evaluations.hbs
@@ -0,0 +1,56 @@
+{{#global-header class="page-header"}}
+ {{#each breadcrumbs as |breadcrumb index|}}
+
+ {{#link-to params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
+
+ {{/each}}
+{{/global-header}}
+{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
+ {{partial "jobs/job/subnav"}}
+
+
+
+ Evaluations
+
+
+ {{#if sortedEvaluations.length}}
+ {{#list-table
+ source=sortedEvaluations
+ sortProperty=sortProperty
+ sortDescending=sortDescending as |t|}}
+ {{#t.head}}
+
ID |
+ {{#t.sort-by prop="priority"}}Priority{{/t.sort-by}}
+ {{#t.sort-by prop="triggeredBy"}}Triggered By{{/t.sort-by}}
+ {{#t.sort-by prop="status"}}Status{{/t.sort-by}}
+ {{#t.sort-by prop="hasPlacementFailures"}}Placement Failures{{/t.sort-by}}
+ {{/t.head}}
+ {{#t.body as |row|}}
+
+ {{row.model.shortId}} |
+ {{row.model.priority}} |
+ {{row.model.triggeredBy}} |
+ {{row.model.status}} |
+
+ {{#if (eq row.model.status "blocked")}}
+ N/A - In Progress
+ {{else if row.model.hasPlacementFailures}}
+ True
+ {{else}}
+ False
+ {{/if}}
+ |
+
+ {{/t.body}}
+ {{/list-table}}
+ {{else}}
+
+
No Evaluations
+
This is most likely due to garbage collection.
+
+ {{/if}}
+
+
+
+{{/gutter-menu}}
+
diff --git a/ui/app/templates/jobs/job/subnav.hbs b/ui/app/templates/jobs/job/subnav.hbs
index e4ef97f80..257072649 100644
--- a/ui/app/templates/jobs/job/subnav.hbs
+++ b/ui/app/templates/jobs/job/subnav.hbs
@@ -6,5 +6,6 @@
{{#if job.supportsDeployments}}
{{#link-to "jobs.job.deployments" job activeClass="is-active"}}Deployments{{/link-to}}
{{/if}}
+
{{#link-to "jobs.job.evaluations" job activeClass="is-active"}}Evaluations{{/link-to}}
diff --git a/ui/tests/acceptance/job-evaluations-test.js b/ui/tests/acceptance/job-evaluations-test.js
new file mode 100644
index 000000000..7fe187f5e
--- /dev/null
+++ b/ui/tests/acceptance/job-evaluations-test.js
@@ -0,0 +1,56 @@
+import { test } from 'qunit';
+import { findAll, click } from 'ember-native-dom-helpers';
+import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
+
+let job;
+let evaluations;
+
+moduleForAcceptance('Acceptance | job evaluations', {
+ beforeEach() {
+ job = server.create('job', { noFailedPlacements: true, createAllocations: false });
+ evaluations = server.db.evaluations.where({ jobId: job.id });
+
+ visit(`/jobs/${job.id}/evaluations`);
+ },
+});
+
+test('lists all evaluations for the job', function(assert) {
+ const evaluationRows = findAll('[data-test-evaluation]');
+ assert.equal(evaluationRows.length, evaluations.length, 'All evaluations are listed');
+
+ evaluations
+ .sortBy('modifyIndex')
+ .reverse()
+ .forEach((evaluation, index) => {
+ const shortId = evaluation.id.split('-')[0];
+ assert.equal(
+ evaluationRows[index].querySelector('[data-test-id]').textContent.trim(),
+ shortId,
+ `Evaluation ${index} is ${shortId}`
+ );
+ });
+});
+
+test('evaluations table is sortable', function(assert) {
+ click('[data-test-sort-by="priority"]');
+
+ andThen(() => {
+ assert.equal(
+ currentURL(),
+ `/jobs/${job.id}/evaluations?sort=priority`,
+ 'the URL persists the sort parameter'
+ );
+ const evaluationRows = findAll('[data-test-evaluation]');
+ evaluations
+ .sortBy('priority')
+ .reverse()
+ .forEach((evaluation, index) => {
+ const shortId = evaluation.id.split('-')[0];
+ assert.equal(
+ evaluationRows[index].querySelector('[data-test-id]').textContent.trim(),
+ shortId,
+ `Evaluation ${index} is ${shortId} with priority ${evaluation.priority}`
+ );
+ });
+ });
+});
diff --git a/ui/tests/integration/job-page/parts/evaluations-test.js b/ui/tests/integration/job-page/parts/evaluations-test.js
deleted file mode 100644
index 982c1d294..000000000
--- a/ui/tests/integration/job-page/parts/evaluations-test.js
+++ /dev/null
@@ -1,65 +0,0 @@
-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')}`
- );
- });
- });
- });
-});