ui: set the job namespace when redirecting after the job is dispatched (#11141)
This commit is contained in:
parent
3d68bf81d6
commit
305f0b5702
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
ui: Fixed an issue when dispatching jobs from a non-default namespace
|
||||||
|
```
|
|
@ -98,7 +98,9 @@ export default class JobDispatch extends Component {
|
||||||
const dispatch = yield this.args.job.dispatch(paramValues, this.payload);
|
const dispatch = yield this.args.job.dispatch(paramValues, this.payload);
|
||||||
|
|
||||||
// Navigate to the newly created instance.
|
// 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) {
|
} catch (err) {
|
||||||
const error = messageFromAdapterError(err) || 'Could not dispatch job';
|
const error = messageFromAdapterError(err) || 'Could not dispatch job';
|
||||||
this.errors.pushObject(error);
|
this.errors.pushObject(error);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<h1 class="title with-flex">
|
<h1 class="title with-flex">
|
||||||
<div>
|
<div data-test-job-name>
|
||||||
{{or this.title this.job.name}}
|
{{or this.title this.job.name}}
|
||||||
<span class="bumper-left tag {{this.job.statusClass}}" data-test-job-status>{{this.job.status}}</span>
|
<span class="bumper-left tag {{this.job.statusClass}}" data-test-job-status>{{this.job.status}}</span>
|
||||||
{{yield}}
|
{{yield}}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable ember/no-test-module-for */
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
|
@ -9,197 +10,216 @@ import { currentURL } from '@ember/test-helpers';
|
||||||
|
|
||||||
const REQUIRED_INDICATOR = '*';
|
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) {
|
return server.create('job', 'parameterized', {
|
||||||
setupApplicationTest(hooks);
|
status: 'running',
|
||||||
setupCodeMirror(hooks);
|
namespaceId: namespace.name,
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it passes an accessibility audit', async function(assert) {
|
|
||||||
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
|
|
||||||
await a11yAudit(assert);
|
|
||||||
});
|
|
||||||
|
|
||||||
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('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();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the dispatch button is disabled when not allowed', async function(assert) {
|
|
||||||
window.localStorage.nomadTokenSecret = clientToken.secretId;
|
|
||||||
|
|
||||||
await JobDetail.visit({ id: job.id, namespace: namespace.name });
|
|
||||||
assert.ok(JobDetail.dispatchButton.isDisabled);
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('required meta fields are properly indicated', async function(assert) {
|
|
||||||
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
|
|
||||||
|
|
||||||
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.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('payload text area is hidden when forbidden', async function(assert) {
|
|
||||||
job.parameterizedJob.Payload = 'forbidden';
|
|
||||||
job.save();
|
|
||||||
|
|
||||||
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
|
|
||||||
|
|
||||||
assert.ok(JobDispatch.payload.emptyMessage.isPresent);
|
|
||||||
assert.notOk(JobDispatch.payload.editor.isPresent);
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
const childrenCountBefore = countDispatchChildren();
|
|
||||||
await JobDispatch.dispatchButton.click();
|
|
||||||
const childrenCountAfter = countDispatchChildren();
|
|
||||||
|
|
||||||
assert.equal(childrenCountAfter, childrenCountBefore + 1);
|
|
||||||
assert.ok(currentURL().startsWith(`/jobs/${encodeURIComponent(`${job.id}/`)}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
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 });
|
|
||||||
|
|
||||||
// Fill only optional meta params.
|
|
||||||
JobDispatch.optionalMetaFields.map(f => f.field.input('meta value'));
|
|
||||||
|
|
||||||
await JobDispatch.dispatchButton.click();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
|
|
||||||
await JobDispatch.dispatchButton.click();
|
|
||||||
|
|
||||||
assert.ok(JobDispatch.hasError, 'Dispatch error message is shown');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
moduleForJobDispatch('Acceptance | job dispatch (with namespace)', () => {
|
||||||
|
server.createList('namespace', 2);
|
||||||
|
const namespace = server.db.namespaces[1];
|
||||||
|
|
||||||
|
return server.create('job', 'parameterized', {
|
||||||
|
status: 'running',
|
||||||
|
namespaceId: namespace.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function moduleForJobDispatch(title, jobFactory) {
|
||||||
|
let job, namespace, managementToken, clientToken;
|
||||||
|
|
||||||
|
module(title, function(hooks) {
|
||||||
|
setupApplicationTest(hooks);
|
||||||
|
setupCodeMirror(hooks);
|
||||||
|
setupMirage(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(function() {
|
||||||
|
// Required for placing allocations (a result of dispatching jobs)
|
||||||
|
server.create('node');
|
||||||
|
|
||||||
|
job = jobFactory();
|
||||||
|
namespace = server.db.namespaces.find(job.namespaceId);
|
||||||
|
|
||||||
|
managementToken = server.create('token');
|
||||||
|
clientToken = server.create('token');
|
||||||
|
|
||||||
|
window.localStorage.nomadTokenSecret = managementToken.secretId;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it passes an accessibility audit', async function(assert) {
|
||||||
|
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
|
||||||
|
await a11yAudit(assert);
|
||||||
|
});
|
||||||
|
|
||||||
|
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('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();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the dispatch button is disabled when not allowed', async function(assert) {
|
||||||
|
window.localStorage.nomadTokenSecret = clientToken.secretId;
|
||||||
|
|
||||||
|
await JobDetail.visit({ id: job.id, namespace: namespace.name });
|
||||||
|
assert.ok(JobDetail.dispatchButton.isDisabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('required meta fields are properly indicated', async function(assert) {
|
||||||
|
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
|
||||||
|
|
||||||
|
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.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('payload text area is hidden when forbidden', async function(assert) {
|
||||||
|
job.parameterizedJob.Payload = 'forbidden';
|
||||||
|
job.save();
|
||||||
|
|
||||||
|
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
|
||||||
|
|
||||||
|
assert.ok(JobDispatch.payload.emptyMessage.isPresent);
|
||||||
|
assert.notOk(JobDispatch.payload.editor.isPresent);
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
const childrenCountBefore = countDispatchChildren();
|
||||||
|
await JobDispatch.dispatchButton.click();
|
||||||
|
const childrenCountAfter = countDispatchChildren();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
|
||||||
|
|
||||||
|
// Fill only optional meta params.
|
||||||
|
JobDispatch.optionalMetaFields.map(f => f.field.input('meta value'));
|
||||||
|
|
||||||
|
await JobDispatch.dispatchButton.click();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
|
||||||
|
await JobDispatch.dispatchButton.click();
|
||||||
|
|
||||||
|
assert.ok(JobDispatch.hasError, 'Dispatch error message is shown');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ module('Integration | Component | job-page/periodic', function(hooks) {
|
||||||
const currentJobCount = server.db.jobs.length;
|
const currentJobCount = server.db.jobs.length;
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
findAll('[data-test-job-name]').length,
|
findAll('[data-test-job-row] [data-test-job-name]').length,
|
||||||
childrenCount,
|
childrenCount,
|
||||||
'The new periodic job launch is in the children list'
|
'The new periodic job launch is in the children list'
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,6 +17,8 @@ import recommendationAccordion from 'nomad-ui/tests/pages/components/recommendat
|
||||||
export default create({
|
export default create({
|
||||||
visit: visitable('/jobs/:id'),
|
visit: visitable('/jobs/:id'),
|
||||||
|
|
||||||
|
jobName: text('[data-test-job-name]'),
|
||||||
|
|
||||||
tabs: collection('[data-test-tab]', {
|
tabs: collection('[data-test-tab]', {
|
||||||
id: attribute('data-test-tab'),
|
id: attribute('data-test-tab'),
|
||||||
visit: clickable('a'),
|
visit: clickable('a'),
|
||||||
|
|
Loading…
Reference in New Issue