Skip to content

Commit

Permalink
ui: set the job namespace when redirecting after the job is dispatched (
Browse files Browse the repository at this point in the history
  • Loading branch information
lgfa29 authored Sep 7, 2021
1 parent d891a71 commit cf7675a
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 156 deletions.
3 changes: 3 additions & 0 deletions .changelog/11141.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: Fixed an issue when dispatching jobs from a non-default namespace
```
4 changes: 3 additions & 1 deletion ui/app/components/job-dispatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion ui/app/templates/components/job-page/parts/title.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<h1 class="title with-flex">
<div>
<div data-test-job-name>
{{or this.title this.job.name}}
<span class="bumper-left tag {{this.job.statusClass}}" data-test-job-status>{{this.job.status}}</span>
{{yield}}
Expand Down
326 changes: 173 additions & 153 deletions ui/tests/acceptance/job-dispatch-test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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');
});
});
});
}
Loading

0 comments on commit cf7675a

Please sign in to comment.