From 9ecb25632df44e5554239ed201207898cb586df4 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 18 Sep 2020 17:33:43 -0700 Subject: [PATCH 1/5] Add job submit time to the job children list --- ui/app/components/job-row.js | 4 ++++ ui/app/models/job.js | 1 + ui/app/serializers/job.js | 2 ++ ui/app/templates/components/job-page/parts/children.hbs | 3 ++- ui/app/templates/components/job-row.hbs | 3 +++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ui/app/components/job-row.js b/ui/app/components/job-row.js index 954332b09..0ac60b158 100644 --- a/ui/app/components/job-row.js +++ b/ui/app/components/job-row.js @@ -12,6 +12,10 @@ export default class JobRow extends Component { job = null; + // One of independent, parent, or child. Used to customize the template + // based on the relationship of this job to others. + context = 'independent'; + onClick() {} click(event) { diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 7af777d4e..0f505201b 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -23,6 +23,7 @@ export default class Job extends Model { @attr('string') statusDescription; @attr('number') createIndex; @attr('number') modifyIndex; + @attr('date') submitTime; // True when the job is the parent periodic or parameterized jobs // Instances of periodic or parameterized jobs are false for both properties diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index d7f8c760a..2719bb981 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -7,6 +7,8 @@ export default class JobSerializer extends ApplicationSerializer { parameterized: 'ParameterizedJob', }; + separateNanos = ['SubmitTime']; + normalize(typeHash, hash) { hash.NamespaceID = hash.Namespace; diff --git a/ui/app/templates/components/job-page/parts/children.hbs b/ui/app/templates/components/job-page/parts/children.hbs index 49a92fd3d..4e5255523 100644 --- a/ui/app/templates/components/job-page/parts/children.hbs +++ b/ui/app/templates/components/job-page/parts/children.hbs @@ -14,6 +14,7 @@ @class="with-foot" as |t|> Name + Submitted At Status Type Priority @@ -21,7 +22,7 @@ Summary - +
diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index 12cbef357..fd85c6777 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -1,4 +1,7 @@ {{this.job.name}} +{{#if (eq @context "child")}} + {{format-month-ts this.job.submitTime}} +{{/if}} {{this.job.status}} From cb7da746b5e487f5921638dbaeb9b3d9f30d4150 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 18 Sep 2020 17:59:54 -0700 Subject: [PATCH 2/5] Sort periodic and parameterized job detail pages by most recently submitted --- ui/app/routes/jobs/job/index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index abd8b1d06..4bea45f1f 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -19,6 +19,18 @@ export default class IndexRoute extends Route.extend(WithWatchers) { }); } + setupController(controller, model) { + // Parameterized and periodic detail pages, which list children jobs, + // should sort by submit time. + if (model && ['periodic', 'parameterized'].includes(model.templateType)) { + controller.setProperties({ + sortProperty: 'submitTime', + sortDescending: true, + }); + } + return super.setupController(...arguments); + } + @watchRecord('job') watch; @watchAll('job') watchAll; @watchRecord('job-summary') watchSummary; From 1a42742d4014f9773bce66d4c01dc1c8bb891ab7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 18 Sep 2020 18:27:48 -0700 Subject: [PATCH 3/5] Update job launches table to use the page size select pattern --- ui/app/components/job-page/parts/children.js | 13 +++++++++++-- .../components/job-page/parts/children.hbs | 5 +++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/ui/app/components/job-page/parts/children.js b/ui/app/components/job-page/parts/children.js index f3b4b865a..1f2ba13c7 100644 --- a/ui/app/components/job-page/parts/children.js +++ b/ui/app/components/job-page/parts/children.js @@ -1,6 +1,7 @@ import Component from '@ember/component'; +import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; -import { alias } from '@ember/object/computed'; +import { alias, readOnly } from '@ember/object/computed'; import Sortable from 'nomad-ui/mixins/sortable'; import { classNames } from '@ember-decorators/component'; import classic from 'ember-classic-decorator'; @@ -8,6 +9,8 @@ import classic from 'ember-classic-decorator'; @classic @classNames('boxed-section') export default class Children extends Component.extend(Sortable) { + @service userSettings; + job = null; // Provide a value that is bound to a query param @@ -18,7 +21,7 @@ export default class Children extends Component.extend(Sortable) { // Provide an action with access to the router gotoJob() {} - pageSize = 10; + @readOnly('userSettings.pageSize') pageSize; @computed('job.taskGroups.[]') get taskGroups() { @@ -32,4 +35,10 @@ export default class Children extends Component.extend(Sortable) { @alias('children') listToSort; @alias('listSorted') sortedChildren; + + resetPagination() { + if (this.currentPage != null) { + this.set('currentPage', 1); + } + } } diff --git a/ui/app/templates/components/job-page/parts/children.hbs b/ui/app/templates/components/job-page/parts/children.hbs index 4e5255523..6d97868c2 100644 --- a/ui/app/templates/components/job-page/parts/children.hbs +++ b/ui/app/templates/components/job-page/parts/children.hbs @@ -26,12 +26,13 @@
+
From 862137da064fdca863e055b541ceeea3efdd0a9a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 18 Sep 2020 21:42:04 -0700 Subject: [PATCH 4/5] Test coverage for page select and submit time on periodic page --- .../job-page/parts/children-test.js | 7 ++- .../components/job-page/periodic-test.js | 52 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/ui/tests/integration/components/job-page/parts/children-test.js b/ui/tests/integration/components/job-page/parts/children-test.js index 4fbf2dcae..29b42eac3 100644 --- a/ui/tests/integration/components/job-page/parts/children-test.js +++ b/ui/tests/integration/components/job-page/parts/children-test.js @@ -64,6 +64,9 @@ module('Integration | Component | job-page/parts/children', function(hooks) { }); test('eventually paginates', async function(assert) { + const pageSize = 10; + window.localStorage.nomadPageSize = pageSize; + this.server.create('job', 'periodic', { id: 'parent', childrenCount: 11, @@ -86,8 +89,8 @@ module('Integration | Component | job-page/parts/children', function(hooks) { `); 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(childrenCount > pageSize, 'Parent has more children than one page size'); + assert.equal(findAll('[data-test-job-name]').length, pageSize, 'Table length maxes out at 10'); assert.ok(find('.pagination-next'), 'Next button is rendered'); assert.ok( diff --git a/ui/tests/integration/components/job-page/periodic-test.js b/ui/tests/integration/components/job-page/periodic-test.js index ddb81cdcd..daece3ed8 100644 --- a/ui/tests/integration/components/job-page/periodic-test.js +++ b/ui/tests/integration/components/job-page/periodic-test.js @@ -2,7 +2,11 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { click, find, findAll, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; +import moment from 'moment'; +import { create, collection } from 'ember-cli-page-object'; import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import pageSizeSelect from 'nomad-ui/tests/acceptance/behaviors/page-size-select'; +import pageSizeSelectPageObject from 'nomad-ui/tests/pages/components/page-size-select'; import { jobURL, stopJob, @@ -13,6 +17,13 @@ import { } from './helpers'; import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; +// A minimum viable page object to use with the pageSizeSelect behavior +const PeriodicJobPage = create({ + pageSize: 25, + jobs: collection('[data-test-job-row]'), + pageSizeSelect: pageSizeSelectPageObject(), +}); + module('Integration | Component | job-page/periodic', function(hooks) { setupRenderingTest(hooks); @@ -195,4 +206,45 @@ module('Integration | Component | job-page/periodic', function(hooks) { await startJob(); expectError(assert, 'Could Not Start Job'); }); + + test('Each job row includes the submitted time', async function(assert) { + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: 1, + createAllocations: false, + }); + + await this.store.findAll('job'); + + const job = this.store.peekAll('job').findBy('plainId', 'parent'); + + this.setProperties(commonProperties(job)); + await this.render(commonTemplate); + + assert.equal( + find('[data-test-submit-time]').textContent, + moment(job.get('children.firstObject.submitTime')).format('MMM DD HH:mm:ss ZZ'), + 'The new periodic job launch is in the children list' + ); + }); + + pageSizeSelect({ + resourceName: 'job', + pageObject: PeriodicJobPage, + pageObjectList: PeriodicJobPage.jobs, + async setup() { + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: PeriodicJobPage.pageSize, + createAllocations: false, + }); + + await this.store.findAll('job'); + + const job = this.store.peekAll('job').findBy('plainId', 'parent'); + + this.setProperties(commonProperties(job)); + await this.render(commonTemplate); + }, + }); }); From 760459704159fae882fc1b74479d66e8aa28e4c8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 18 Sep 2020 22:20:09 -0700 Subject: [PATCH 5/5] Acceptance test coverage for the submit time sorting on the periodic and parameterized detail pages --- ui/app/templates/components/job-row.hbs | 2 +- ui/mirage/factories/job.js | 2 + ui/tests/acceptance/job-detail-test.js | 39 +++++++++++++++++-- ui/tests/helpers/module-for-job.js | 2 +- .../components/job-page/periodic-test.js | 2 +- ui/tests/pages/jobs/detail.js | 14 +++++++ 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index fd85c6777..c879a1d84 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -1,6 +1,6 @@ {{this.job.name}} {{#if (eq @context "child")}} - {{format-month-ts this.job.submitTime}} + {{format-month-ts this.job.submitTime}} {{/if}} {{this.job.status}} diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index c4958bb5b..4d9512457 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -4,6 +4,7 @@ import faker from 'nomad-ui/mirage/faker'; import { provide, pickOne } from '../utils'; import { DATACENTERS } from '../common'; +const REF_TIME = new Date(); const JOB_PREFIXES = provide(5, faker.hacker.abbreviation); const JOB_TYPES = ['service', 'batch', 'system']; const JOB_STATUSES = ['pending', 'running', 'dead']; @@ -19,6 +20,7 @@ export default Factory.extend({ }, version: 1, + submitTime: () => faker.date.past(2 / 365, REF_TIME) * 1000000, // When provided, the resourceSpec will inform how many task groups to create // and how much of each resource that task group reserves. diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index c4a36601e..d6cd81ba7 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -3,6 +3,7 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { selectChoose } from 'ember-power-select/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; +import moment from 'moment'; import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; import moduleForJob from 'nomad-ui/tests/helpers/module-for-job'; import JobDetail from 'nomad-ui/tests/pages/jobs/detail'; @@ -14,12 +15,42 @@ moduleForJob('Acceptance | job detail (batch)', 'allocations', () => moduleForJob('Acceptance | job detail (system)', 'allocations', () => server.create('job', { type: 'system', shallow: true }) ); -moduleForJob('Acceptance | job detail (periodic)', 'children', () => - server.create('job', 'periodic', { shallow: true }) +moduleForJob( + 'Acceptance | job detail (periodic)', + 'children', + () => server.create('job', 'periodic', { shallow: true }), + { + 'the default sort is submitTime descending': async function(job, assert) { + const mostRecentLaunch = server.db.jobs + .where({ parentId: job.id }) + .sortBy('submitTime') + .reverse()[0]; + + assert.equal( + JobDetail.jobs[0].submitTime, + moment(mostRecentLaunch.submitTime / 1000000).format('MMM DD HH:mm:ss ZZ') + ); + }, + } ); -moduleForJob('Acceptance | job detail (parameterized)', 'children', () => - server.create('job', 'parameterized', { shallow: true }) +moduleForJob( + 'Acceptance | job detail (parameterized)', + 'children', + () => server.create('job', 'parameterized', { shallow: true }), + { + 'the default sort is submitTime descending': async (job, assert) => { + const mostRecentLaunch = server.db.jobs + .where({ parentId: job.id }) + .sortBy('submitTime') + .reverse()[0]; + + assert.equal( + JobDetail.jobs[0].submitTime, + moment(mostRecentLaunch.submitTime / 1000000).format('MMM DD HH:mm:ss ZZ') + ); + }, + } ); moduleForJob('Acceptance | job detail (periodic child)', 'allocations', () => { diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 55a5aa4f4..7efbd7105 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -93,7 +93,7 @@ export default function moduleForJob(title, context, jobFactory, additionalTests for (var testName in additionalTests) { test(testName, async function(assert) { - await additionalTests[testName](job, assert); + await additionalTests[testName].call(this, job, assert); }); } }); diff --git a/ui/tests/integration/components/job-page/periodic-test.js b/ui/tests/integration/components/job-page/periodic-test.js index daece3ed8..f4a38c934 100644 --- a/ui/tests/integration/components/job-page/periodic-test.js +++ b/ui/tests/integration/components/job-page/periodic-test.js @@ -222,7 +222,7 @@ module('Integration | Component | job-page/periodic', function(hooks) { await this.render(commonTemplate); assert.equal( - find('[data-test-submit-time]').textContent, + find('[data-test-job-submit-time]').textContent, moment(job.get('children.firstObject.submitTime')).format('MMM DD HH:mm:ss ZZ'), 'The new periodic job launch is in the children list' ); diff --git a/ui/tests/pages/jobs/detail.js b/ui/tests/pages/jobs/detail.js index 8fbea8749..e2af9a0b8 100644 --- a/ui/tests/pages/jobs/detail.js +++ b/ui/tests/pages/jobs/detail.js @@ -51,6 +51,20 @@ export default create({ viewAllAllocations: text('[data-test-view-all-allocations]'), + jobs: collection('[data-test-job-row]', { + id: attribute('data-test-job-row'), + name: text('[data-test-job-name]'), + link: attribute('href', '[data-test-job-name] a'), + submitTime: text('[data-test-job-submit-time]'), + status: text('[data-test-job-status]'), + type: text('[data-test-job-type]'), + priority: text('[data-test-job-priority]'), + taskGroups: text('[data-test-job-task-groups]'), + + clickRow: clickable(), + clickName: clickable('[data-test-job-name] a'), + }), + error: { isPresent: isPresent('[data-test-error]'), title: text('[data-test-error-title]'),