diff --git a/.changelog/11141.txt b/.changelog/11141.txt new file mode 100644 index 000000000000..87d5dca47c49 --- /dev/null +++ b/.changelog/11141.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fixed an issue when dispatching jobs from a non-default namespace +``` diff --git a/ui/app/components/job-dispatch.js b/ui/app/components/job-dispatch.js index 020c8374da17..0cf027359bb8 100644 --- a/ui/app/components/job-dispatch.js +++ b/ui/app/components/job-dispatch.js @@ -98,7 +98,9 @@ export default class JobDispatch extends Component { const dispatch = yield this.args.job.dispatch(paramValues, this.payload); // Navigate to the newly created instance. - this.router.transitionTo('jobs.job', dispatch.DispatchedJobID); + this.router.transitionTo('jobs.job', dispatch.DispatchedJobID, { + queryParams: { namespace: this.args.job.get('namespace.name') }, + }); } catch (err) { const error = messageFromAdapterError(err) || 'Could not dispatch job'; this.errors.pushObject(error); diff --git a/ui/app/templates/components/job-page/parts/title.hbs b/ui/app/templates/components/job-page/parts/title.hbs index b42a4912d961..c6e1b06ff5ec 100644 --- a/ui/app/templates/components/job-page/parts/title.hbs +++ b/ui/app/templates/components/job-page/parts/title.hbs @@ -1,5 +1,5 @@

-
+
{{or this.title this.job.name}} {{this.job.status}} {{yield}} diff --git a/ui/tests/acceptance/job-dispatch-test.js b/ui/tests/acceptance/job-dispatch-test.js index 8dff73409071..00d4aa888b26 100644 --- a/ui/tests/acceptance/job-dispatch-test.js +++ b/ui/tests/acceptance/job-dispatch-test.js @@ -1,3 +1,4 @@ +/* eslint-disable ember/no-test-module-for */ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -9,197 +10,216 @@ import { currentURL } from '@ember/test-helpers'; const REQUIRED_INDICATOR = '*'; -let job, namespace, managementToken, clientToken; +moduleForJobDispatch('Acceptance | job dispatch', () => { + server.createList('namespace', 2); + const namespace = server.db.namespaces[0]; -module('Acceptance | job dispatch', function(hooks) { - setupApplicationTest(hooks); - setupCodeMirror(hooks); - setupMirage(hooks); - - hooks.beforeEach(function() { - // Required for placing allocations (a result of dispatching jobs) - server.create('node'); - server.createList('namespace', 2); - - namespace = server.db.namespaces[0]; - job = server.create('job', 'parameterized', { - status: 'running', - namespaceId: namespace.name, - }); - - managementToken = server.create('token'); - clientToken = server.create('token'); - - window.localStorage.nomadTokenSecret = managementToken.secretId; + return server.create('job', 'parameterized', { + status: 'running', + namespaceId: namespace.name, }); +}); - test('it passes an accessibility audit', async function(assert) { - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); - await a11yAudit(assert); - }); +moduleForJobDispatch('Acceptance | job dispatch (with namespace)', () => { + server.createList('namespace', 2); + const namespace = server.db.namespaces[1]; - test('the dispatch button is displayed with management token', async function(assert) { - await JobDetail.visit({ id: job.id, namespace: namespace.name }); - assert.notOk(JobDetail.dispatchButton.isDisabled); + return server.create('job', 'parameterized', { + status: 'running', + namespaceId: namespace.name, }); +}); - test('the dispatch button is displayed when allowed', async function(assert) { - window.localStorage.nomadTokenSecret = clientToken.secretId; - - const policy = server.create('policy', { - id: 'dispatch', - name: 'dispatch', - rulesJSON: { - Namespaces: [ - { - Name: namespace.name, - Capabilities: ['list-jobs', 'dispatch-job'], - }, - ], - }, - }); - - clientToken.policyIds = [policy.id]; - clientToken.save(); - - await JobDetail.visit({ id: job.id, namespace: namespace.name }); - assert.notOk(JobDetail.dispatchButton.isDisabled); +function moduleForJobDispatch(title, jobFactory) { + let job, namespace, managementToken, clientToken; - // Reset clientToken policies. - clientToken.policyIds = []; - clientToken.save(); - }); + module(title, function(hooks) { + setupApplicationTest(hooks); + setupCodeMirror(hooks); + setupMirage(hooks); - test('the dispatch button is disabled when not allowed', async function(assert) { - window.localStorage.nomadTokenSecret = clientToken.secretId; + hooks.beforeEach(function() { + // Required for placing allocations (a result of dispatching jobs) + server.create('node'); - await JobDetail.visit({ id: job.id, namespace: namespace.name }); - assert.ok(JobDetail.dispatchButton.isDisabled); - }); + job = jobFactory(); + namespace = server.db.namespaces.find(job.namespaceId); - test('all meta fields are displayed', async function(assert) { - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); - assert.equal( - JobDispatch.metaFields.length, - job.parameterizedJob.MetaOptional.length + job.parameterizedJob.MetaRequired.length - ); - }); + managementToken = server.create('token'); + clientToken = server.create('token'); - test('required meta fields are properly indicated', async function(assert) { - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + window.localStorage.nomadTokenSecret = managementToken.secretId; + }); - JobDispatch.metaFields.forEach(f => { - const hasIndicator = f.label.includes(REQUIRED_INDICATOR); - const isRequired = job.parameterizedJob.MetaRequired.includes(f.field.id); + test('it passes an accessibility audit', async function(assert) { + await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await a11yAudit(assert); + }); - if (isRequired) { - assert.ok(hasIndicator, `${f.label} contains required indicator.`); - } else { - assert.notOk(hasIndicator, `${f.label} doesn't contain required indicator.`); - } + test('the dispatch button is displayed with management token', async function(assert) { + await JobDetail.visit({ id: job.id, namespace: namespace.name }); + assert.notOk(JobDetail.dispatchButton.isDisabled); }); - }); - test('job without meta fields', async function(assert) { - const jobWithoutMeta = server.create('job', 'parameterized', { - status: 'running', - namespaceId: namespace.name, - parameterizedJob: { - MetaRequired: null, - MetaOptional: null, - }, + test('the dispatch button is displayed when allowed', async function(assert) { + window.localStorage.nomadTokenSecret = clientToken.secretId; + + const policy = server.create('policy', { + id: 'dispatch', + name: 'dispatch', + rulesJSON: { + Namespaces: [ + { + Name: namespace.name, + Capabilities: ['list-jobs', 'dispatch-job'], + }, + ], + }, + }); + + clientToken.policyIds = [policy.id]; + clientToken.save(); + + await JobDetail.visit({ id: job.id, namespace: namespace.name }); + assert.notOk(JobDetail.dispatchButton.isDisabled); + + // Reset clientToken policies. + clientToken.policyIds = []; + clientToken.save(); }); - await JobDispatch.visit({ id: jobWithoutMeta.id, namespace: namespace.name }); - assert.ok(JobDispatch.dispatchButton.isPresent); - }); + test('the dispatch button is disabled when not allowed', async function(assert) { + window.localStorage.nomadTokenSecret = clientToken.secretId; - test('payload text area is hidden when forbidden', async function(assert) { - job.parameterizedJob.Payload = 'forbidden'; - job.save(); + await JobDetail.visit({ id: job.id, namespace: namespace.name }); + assert.ok(JobDetail.dispatchButton.isDisabled); + }); - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + test('all meta fields are displayed', async function(assert) { + await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + assert.equal( + JobDispatch.metaFields.length, + job.parameterizedJob.MetaOptional.length + job.parameterizedJob.MetaRequired.length + ); + }); - assert.ok(JobDispatch.payload.emptyMessage.isPresent); - assert.notOk(JobDispatch.payload.editor.isPresent); - }); + test('required meta fields are properly indicated', async function(assert) { + await JobDispatch.visit({ id: job.id, namespace: namespace.name }); - test('payload is indicated as required', async function(assert) { - const jobPayloadRequired = server.create('job', 'parameterized', { - status: 'running', - namespaceId: namespace.name, - parameterizedJob: { - Payload: 'required', - }, + JobDispatch.metaFields.forEach(f => { + const hasIndicator = f.label.includes(REQUIRED_INDICATOR); + const isRequired = job.parameterizedJob.MetaRequired.includes(f.field.id); + + if (isRequired) { + assert.ok(hasIndicator, `${f.label} contains required indicator.`); + } else { + assert.notOk(hasIndicator, `${f.label} doesn't contain required indicator.`); + } + }); }); - const jobPayloadOptional = server.create('job', 'parameterized', { - status: 'running', - namespaceId: namespace.name, - parameterizedJob: { - Payload: 'optional', - }, + + test('job without meta fields', async function(assert) { + const jobWithoutMeta = server.create('job', 'parameterized', { + status: 'running', + namespaceId: namespace.name, + parameterizedJob: { + MetaRequired: null, + MetaOptional: null, + }, + }); + + await JobDispatch.visit({ id: jobWithoutMeta.id, namespace: namespace.name }); + assert.ok(JobDispatch.dispatchButton.isPresent); }); - await JobDispatch.visit({ id: jobPayloadRequired.id, namespace: namespace.name }); + test('payload text area is hidden when forbidden', async function(assert) { + job.parameterizedJob.Payload = 'forbidden'; + job.save(); - let payloadTitle = JobDispatch.payload.title; - assert.ok( - payloadTitle.includes(REQUIRED_INDICATOR), - `${payloadTitle} contains required indicator.` - ); + await JobDispatch.visit({ id: job.id, namespace: namespace.name }); - await JobDispatch.visit({ id: jobPayloadOptional.id, namespace: namespace.name }); + assert.ok(JobDispatch.payload.emptyMessage.isPresent); + assert.notOk(JobDispatch.payload.editor.isPresent); + }); - payloadTitle = JobDispatch.payload.title; - assert.notOk( - payloadTitle.includes(REQUIRED_INDICATOR), - `${payloadTitle} doesn't contain required indicator.` - ); - }); + test('payload is indicated as required', async function(assert) { + const jobPayloadRequired = server.create('job', 'parameterized', { + status: 'running', + namespaceId: namespace.name, + parameterizedJob: { + Payload: 'required', + }, + }); + const jobPayloadOptional = server.create('job', 'parameterized', { + status: 'running', + namespaceId: namespace.name, + parameterizedJob: { + Payload: 'optional', + }, + }); + + await JobDispatch.visit({ id: jobPayloadRequired.id, namespace: namespace.name }); + + let payloadTitle = JobDispatch.payload.title; + assert.ok( + payloadTitle.includes(REQUIRED_INDICATOR), + `${payloadTitle} contains required indicator.` + ); + + await JobDispatch.visit({ id: jobPayloadOptional.id, namespace: namespace.name }); + + payloadTitle = JobDispatch.payload.title; + assert.notOk( + payloadTitle.includes(REQUIRED_INDICATOR), + `${payloadTitle} doesn't contain required indicator.` + ); + }); - test('dispatch a job', async function(assert) { - function countDispatchChildren() { - return server.db.jobs.where(j => j.id.startsWith(`${job.id}/`)).length; - } + test('dispatch a job', async function(assert) { + function countDispatchChildren() { + return server.db.jobs.where(j => j.id.startsWith(`${job.id}/`)).length; + } - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.visit({ id: job.id, namespace: namespace.name }); - // Fill form. - JobDispatch.metaFields.map(f => f.field.input('meta value')); - JobDispatch.payload.editor.fillIn('payload'); + // Fill form. + JobDispatch.metaFields.map(f => f.field.input('meta value')); + JobDispatch.payload.editor.fillIn('payload'); - const childrenCountBefore = countDispatchChildren(); - await JobDispatch.dispatchButton.click(); - const childrenCountAfter = countDispatchChildren(); + const childrenCountBefore = countDispatchChildren(); + await JobDispatch.dispatchButton.click(); + const childrenCountAfter = countDispatchChildren(); - assert.equal(childrenCountAfter, childrenCountBefore + 1); - assert.ok(currentURL().startsWith(`/jobs/${encodeURIComponent(`${job.id}/`)}`)); - }); + assert.equal(childrenCountAfter, childrenCountBefore + 1); + assert.ok(currentURL().startsWith(`/jobs/${encodeURIComponent(`${job.id}/`)}`)); + assert.ok(JobDetail.jobName); + }); - test('fail when required meta field is empty', async function(assert) { - // Make sure we have a required meta param. - job.parameterizedJob.MetaRequired = ['required']; - job.parameterizedJob.Payload = 'forbidden'; - job.save(); + test('fail when required meta field is empty', async function(assert) { + // Make sure we have a required meta param. + job.parameterizedJob.MetaRequired = ['required']; + job.parameterizedJob.Payload = 'forbidden'; + job.save(); - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.visit({ id: job.id, namespace: namespace.name }); - // Fill only optional meta params. - JobDispatch.optionalMetaFields.map(f => f.field.input('meta value')); + // Fill only optional meta params. + JobDispatch.optionalMetaFields.map(f => f.field.input('meta value')); - await JobDispatch.dispatchButton.click(); + await JobDispatch.dispatchButton.click(); - assert.ok(JobDispatch.hasError, 'Dispatch error message is shown'); - }); + assert.ok(JobDispatch.hasError, 'Dispatch error message is shown'); + }); - test('fail when required payload is empty', async function(assert) { - job.parameterizedJob.MetaRequired = []; - job.parameterizedJob.Payload = 'required'; - job.save(); + test('fail when required payload is empty', async function(assert) { + job.parameterizedJob.MetaRequired = []; + job.parameterizedJob.Payload = 'required'; + job.save(); - await JobDispatch.visit({ id: job.id, namespace: namespace.name }); - await JobDispatch.dispatchButton.click(); + await JobDispatch.visit({ id: job.id, namespace: namespace.name }); + await JobDispatch.dispatchButton.click(); - assert.ok(JobDispatch.hasError, 'Dispatch error message is shown'); + assert.ok(JobDispatch.hasError, 'Dispatch error message is shown'); + }); }); -}); +} diff --git a/ui/tests/integration/components/job-page/periodic-test.js b/ui/tests/integration/components/job-page/periodic-test.js index f4a38c934c33..edcec6b5442e 100644 --- a/ui/tests/integration/components/job-page/periodic-test.js +++ b/ui/tests/integration/components/job-page/periodic-test.js @@ -75,7 +75,7 @@ module('Integration | Component | job-page/periodic', function(hooks) { const currentJobCount = server.db.jobs.length; assert.equal( - findAll('[data-test-job-name]').length, + findAll('[data-test-job-row] [data-test-job-name]').length, childrenCount, '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 9d2b679c584c..cc6b64634b91 100644 --- a/ui/tests/pages/jobs/detail.js +++ b/ui/tests/pages/jobs/detail.js @@ -17,6 +17,8 @@ import recommendationAccordion from 'nomad-ui/tests/pages/components/recommendat export default create({ visit: visitable('/jobs/:id'), + jobName: text('[data-test-job-name]'), + tabs: collection('[data-test-tab]', { id: attribute('data-test-tab'), visit: clickable('a'),