diff --git a/.changelog/11110.txt b/.changelog/11110.txt new file mode 100644 index 00000000000..4216ed46af2 --- /dev/null +++ b/.changelog/11110.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fixed an issue that prevented periodic and dispatch jobs in a non-default namespace to be properly rendered +``` diff --git a/ui/app/components/job-page/parts/children.js b/ui/app/components/job-page/parts/children.js index 1f2ba13c794..20f72910dc9 100644 --- a/ui/app/components/job-page/parts/children.js +++ b/ui/app/components/job-page/parts/children.js @@ -9,6 +9,7 @@ import classic from 'ember-classic-decorator'; @classic @classNames('boxed-section') export default class Children extends Component.extend(Sortable) { + @service system; @service userSettings; job = null; diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index 4bea45f1f2d..8f1cf1827fe 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -1,6 +1,6 @@ import Route from '@ember/routing/route'; import { collect } from '@ember/object/computed'; -import { watchRecord, watchRelationship, watchAll } from 'nomad-ui/utils/properties/watch'; +import { watchRecord, watchRelationship, watchQuery } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; export default class IndexRoute extends Route.extend(WithWatchers) { @@ -15,7 +15,9 @@ export default class IndexRoute extends Route.extend(WithWatchers) { evaluations: this.watchEvaluations.perform(model), latestDeployment: model.get('supportsDeployments') && this.watchLatestDeployment.perform(model), - list: model.get('hasChildren') && this.watchAll.perform(), + list: + model.get('hasChildren') && + this.watchAllJobs.perform({ namespace: model.namespace.get('name') }), }); } @@ -32,7 +34,7 @@ export default class IndexRoute extends Route.extend(WithWatchers) { } @watchRecord('job') watch; - @watchAll('job') watchAll; + @watchQuery('job') watchAllJobs; @watchRecord('job-summary') watchSummary; @watchRelationship('allocations') watchAllocations; @watchRelationship('evaluations') watchEvaluations; @@ -40,7 +42,7 @@ export default class IndexRoute extends Route.extend(WithWatchers) { @collect( 'watch', - 'watchAll', + 'watchAllJobs', 'watchSummary', 'watchAllocations', 'watchEvaluations', diff --git a/ui/app/templates/components/job-page/parts/children.hbs b/ui/app/templates/components/job-page/parts/children.hbs index e9d81d3be48..4070bfa06b3 100644 --- a/ui/app/templates/components/job-page/parts/children.hbs +++ b/ui/app/templates/components/job-page/parts/children.hbs @@ -31,9 +31,12 @@ @sortProperty={{this.sortProperty}} @sortDescending={{this.sortDescending}} @class="with-foot" as |t|> - + Name - Submitted At + {{#if this.system.shouldShowNamespaces}} + Namespace + {{/if}} + Submitted At Status Type Priority diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index c9a0821507a..74e02db9599 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -25,6 +25,7 @@ moduleForJob( .sortBy('submitTime') .reverse()[0]; + assert.ok(JobDetail.jobsHeader.hasSubmitTime); assert.equal( JobDetail.jobs[0].submitTime, moment(mostRecentLaunch.submitTime / 1000000).format('MMM DD HH:mm:ss ZZ') @@ -33,6 +34,25 @@ moduleForJob( } ); +moduleForJob( + 'Acceptance | job detail (periodic in namespace)', + 'children', + () => { + const namespace = server.create('namespace', { id: 'test' }); + const parent = server.create('job', 'periodic', { + shallow: true, + namespaceId: namespace.name, + }); + return parent; + }, + { + 'display namespace in children table': async function(job, assert) { + assert.ok(JobDetail.jobsHeader.hasNamespace); + assert.equal(JobDetail.jobs[0].namespace, job.namespace); + }, + } +); + moduleForJob( 'Acceptance | job detail (parameterized)', 'children', @@ -44,6 +64,7 @@ moduleForJob( .sortBy('submitTime') .reverse()[0]; + assert.ok(JobDetail.jobsHeader.hasSubmitTime); assert.equal( JobDetail.jobs[0].submitTime, moment(mostRecentLaunch.submitTime / 1000000).format('MMM DD HH:mm:ss ZZ') @@ -52,6 +73,25 @@ moduleForJob( } ); +moduleForJob( + 'Acceptance | job detail (parameterized in namespace)', + 'children', + () => { + const namespace = server.create('namespace', { id: 'test' }); + const parent = server.create('job', 'parameterized', { + shallow: true, + namespaceId: namespace.name, + }); + return parent; + }, + { + 'display namespace in children table': async function(job, assert) { + assert.ok(JobDetail.jobsHeader.hasNamespace); + assert.equal(JobDetail.jobs[0].namespace, job.namespace); + }, + } +); + moduleForJob('Acceptance | job detail (periodic child)', 'allocations', () => { const parent = server.create('job', 'periodic', { childrenCount: 1, shallow: true }); return server.db.jobs.where({ parentId: parent.id })[0]; diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 3a6c27b60ea..0bf9f70e38f 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -22,32 +22,51 @@ export default function moduleForJob(title, context, jobFactory, additionalTests hooks.beforeEach(async function() { server.create('node'); job = jobFactory(); - await JobDetail.visit({ id: job.id }); + if (!job.namespace || job.namespace === 'default') { + await JobDetail.visit({ id: job.id }); + } else { + await JobDetail.visit({ id: job.id, namespace: job.namespace }); + } }); test('visiting /jobs/:job_id', async function(assert) { - assert.equal(currentURL(), `/jobs/${encodeURIComponent(job.id)}`); + assert.equal( + currentURL(), + urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}`, job.namespace) + ); assert.equal(document.title, `Job ${job.name} - Nomad`); }); test('the subnav links to overview', async function(assert) { await JobDetail.tabFor('overview').visit(); - assert.equal(currentURL(), `/jobs/${encodeURIComponent(job.id)}`); + assert.equal( + currentURL(), + urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}`, job.namespace) + ); }); test('the subnav links to definition', async function(assert) { await JobDetail.tabFor('definition').visit(); - assert.equal(currentURL(), `/jobs/${encodeURIComponent(job.id)}/definition`); + assert.equal( + currentURL(), + urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}/definition`, job.namespace) + ); }); test('the subnav links to versions', async function(assert) { await JobDetail.tabFor('versions').visit(); - assert.equal(currentURL(), `/jobs/${encodeURIComponent(job.id)}/versions`); + assert.equal( + currentURL(), + urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}/versions`, job.namespace) + ); }); test('the subnav links to evaluations', async function(assert) { await JobDetail.tabFor('evaluations').visit(); - assert.equal(currentURL(), `/jobs/${encodeURIComponent(job.id)}/evaluations`); + assert.equal( + currentURL(), + urlWithNamespace(`/jobs/${encodeURIComponent(job.id)}/evaluations`, job.namespace) + ); }); test('the title buttons are dependent on job status', async function(assert) { @@ -99,3 +118,11 @@ export default function moduleForJob(title, context, jobFactory, additionalTests } }); } + +function urlWithNamespace(url, namespace) { + if (!namespace || namespace === 'default') { + return url; + } + + return `${url}?namespace=${namespace}`; +} diff --git a/ui/tests/pages/jobs/detail.js b/ui/tests/pages/jobs/detail.js index 577c473e82f..cc6b64634b9 100644 --- a/ui/tests/pages/jobs/detail.js +++ b/ui/tests/pages/jobs/detail.js @@ -66,9 +66,16 @@ export default create({ viewAllAllocations: text('[data-test-view-all-allocations]'), + jobsHeader: { + scope: '[data-test-jobs-header]', + hasSubmitTime: isPresent('[data-test-jobs-submit-time-header]'), + hasNamespace: isPresent('[data-test-jobs-namespace-header]'), + }, + jobs: collection('[data-test-job-row]', { id: attribute('data-test-job-row'), name: text('[data-test-job-name]'), + namespace: text('[data-test-job-namespace]'), link: attribute('href', '[data-test-job-name] a'), submitTime: text('[data-test-job-submit-time]'), status: text('[data-test-job-status]'),