Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions ui/app/adapters/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,18 @@ export default class TokenAdapter extends ApplicationAdapter {
return `${this.buildURL()}/${singularize(modelName)}/${identifier}`;
}

async findSelf() {
async findSelf(regionOverride = null) {
// the application adapter automatically adds the region parameter to all requests,
// but only if the /regions endpoint has been resolved first. Since this request is async,
// we can ensure that the regions are loaded before making the token/self request.
await this.system.regions;

const response = await this.ajax(`${this.buildURL()}/token/self`, 'GET');
const options = regionOverride ? { regionOverride } : {};
const response = await this.ajax(
`${this.buildURL()}/token/self`,
'GET',
options
);
const normalized = this.store.normalize('token', response);
const tokenRecord = this.store.push(normalized);
return tokenRecord;
Expand Down
6 changes: 2 additions & 4 deletions ui/app/components/region-switcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ export default class RegionSwitcher extends Component {

@action
async gotoRegion(region) {
// Note: redundant but as long as we're using PowerSelect, the implicit set('activeRegion')
// is not something we can await, so we do it explicitly here.
this.system.set('activeRegion', region);
await this.get('token.fetchSelfTokenAndPolicies').perform().catch();
// Fetch token for the new region before transitioning
await this.get('token.fetchSelfTokenAndPolicies').perform(region).catch();

this.router.transitionTo({ queryParams: { region } });
}
Expand Down
32 changes: 19 additions & 13 deletions ui/app/models/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
fragmentArray,
fragmentOwner,
} from 'ember-data-model-fragments/attributes';
import { computed } from '@ember/object';
import { computed, notifyPropertyChange } from '@ember/object';

export default class Task extends Fragment {
@fragmentOwner() taskGroup;
Expand Down Expand Up @@ -61,6 +61,19 @@ export default class Task extends Fragment {

@fragmentArray('volume-mount', { defaultValue: () => [] }) volumeMounts;

_pathLinkedVariableJobID() {
let jobID = this._job.plainId;
const parentID = this._job.belongsTo('parent').id()
? JSON.parse(this._job.belongsTo('parent').id())[0]
: null;

if (parentID) {
jobID = parentID;
}

return jobID;
}

async _fetchParentJob() {
let job = this.store.peekRecord('job', this.taskGroup.job.id);
if (!job) {
Expand All @@ -69,17 +82,17 @@ export default class Task extends Fragment {
});
}
this._job = job;
await this._job.variables;
// pathLinkedVariable is consumed by a sync template condition, so notify after async load.
notifyPropertyChange(this, 'pathLinkedVariable');
}

get pathLinkedVariable() {
if (!this._job) {
this._fetchParentJob();
return null;
} else {
let jobID = this._job.plainId;
if (this._job.parent.get('plainId')) {
jobID = this._job.parent.get('plainId');
}
let jobID = this._pathLinkedVariableJobID();
return this._job.variables?.findBy(
'path',
`nomad/jobs/${jobID}/${this.taskGroup.name}/${this.name}`,
Expand All @@ -93,14 +106,7 @@ export default class Task extends Fragment {
await this._fetchParentJob();
}
await this._job.variables;
let jobID = this._job.plainId;
// not getting plainID because we dont know the resolution status of the task's job's parent yet
let parentID = this._job.belongsTo('parent').id()
? JSON.parse(this._job.belongsTo('parent').id())[0]
: null;
if (parentID) {
jobID = parentID;
}
let jobID = this._pathLinkedVariableJobID();
return await this._job.variables?.findBy(
'path',
`nomad/jobs/${jobID}/${this.taskGroup.name}/${this.name}`,
Expand Down
2 changes: 1 addition & 1 deletion ui/app/routes/allocations/allocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default class AllocationRoute extends Route.extend(WithWatchers) {
await allocation.reload();
}
const jobId = allocation.belongsTo('job').id();
await this.store.findRecord('job', jobId);
await this.store.findRecord('job', jobId, { reload: true });

// Force fragment-array materialization before first render so Ember does
// not lazily write `services` during template computation.
Expand Down
19 changes: 10 additions & 9 deletions ui/app/routes/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { handleRouteRedirects } from '../utils/route-redirector';
export default class ApplicationRoute extends Route {
@service config;
@service system;
@service store;
@service token;
@service router;

Expand Down Expand Up @@ -89,16 +88,18 @@ export default class ApplicationRoute extends Route {
const queryParam = transition.to.queryParams.region;
const defaultRegion = this.get('system.defaultRegion.region');
const currentRegion = this.get('system.activeRegion') || defaultRegion;
const nextRegion = queryParam || defaultRegion;
const regionChanged = nextRegion !== currentRegion;

// Only reset the store if the region actually changed
if (
(queryParam && queryParam !== currentRegion) ||
(!queryParam && currentRegion !== defaultRegion)
) {
this.store.unloadAll();
}
// Update the active region - the adapter will use this for all API requests
this.set('system.activeRegion', nextRegion);

this.set('system.activeRegion', queryParam || defaultRegion);
// Refresh for child routes if region changes and only if query-param-only transitions
if (regionChanged && transition.queryParamsOnly) {
later(() => {
this.refresh();
}, 0);
}

return promises;
}
Expand Down
4 changes: 2 additions & 2 deletions ui/app/routes/jobs/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export default class JobRoute extends Route.extend(WithWatchers) {
.findRecord('job', fullId, { reload: true })
.then((job) => {
const relatedModelsQueries = [
job.get('allocations'),
job.get('evaluations'),
job.hasMany('allocations').reload(),
job.hasMany('evaluations').reload(),
this.store.findAll('namespace'),
];

Expand Down
8 changes: 4 additions & 4 deletions ui/app/services/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ export default class TokenService extends Service {
}
}

@task(function* () {
@task(function* (regionOverride = null) {
const TokenAdapter = getOwner(this).lookup('adapter:token');
try {
var token = yield TokenAdapter.findSelf();
var token = yield TokenAdapter.findSelf(regionOverride);
if (token.accessor === 'acls-disabled') {
this.set('aclEnabled', false);
return null;
Expand Down Expand Up @@ -122,8 +122,8 @@ export default class TokenService extends Service {

@alias('fetchSelfTokenPolicies.lastSuccessful.value') selfTokenPolicies;

@task(function* () {
yield this.fetchSelfToken.perform();
@task(function* (regionOverride = null) {
yield this.fetchSelfToken.perform(regionOverride);
this.kickoffTokenTTLMonitoring();
if (this.aclEnabled) {
yield this.fetchSelfTokenPolicies.perform();
Expand Down
35 changes: 30 additions & 5 deletions ui/tests/acceptance/job-definition-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror';
import Definition from 'nomad-ui/tests/pages/jobs/job/definition';
import { JOB_JSON } from 'nomad-ui/tests/utils/generate-raw-json-job';

let job;

Expand Down Expand Up @@ -152,6 +151,7 @@ module('Acceptance | job definition | full specification', function (hooks) {
});

test('it allows users to select between full specification and JSON definition', async function (assert) {
assert.expect(7);
const specification_response = {
Format: 'hcl2',
JobID: 'example',
Expand All @@ -163,24 +163,49 @@ module('Acceptance | job definition | full specification', function (hooks) {
Variables: '',
Version: 0,
};
this.server.get('/job/:id', () => JOB_JSON);

this.server.get('/job/:id/submission', () => specification_response);

await Definition.visit({ id: job.id });
await percySnapshot(assert);

assert
.dom('[data-test-select="job-spec"]')
.exists('A select button exists and defaults to full definition');
.exists('A select button exists and defaults to job spec');
let codeMirror = this.getCodeMirrorInstance();
assert.deepEqual(
assert.equal(
codeMirror.getValue(),
specification_response.Source,
'Shows the full definition as written by the user',
);

await click('[data-test-select-full]');
codeMirror = this.getCodeMirrorInstance();
assert.propContains(JSON.parse(codeMirror.getValue()), JOB_JSON);
const fullDefinition = JSON.parse(codeMirror.getValue());
assert
.dom('[data-test-select="full-definition"]')
.exists('View switches to full definition mode');
assert.equal(
fullDefinition.ID,
job.id,
'Full definition shows the correct job ID',
);
assert.equal(
fullDefinition.Name,
job.name,
'Full definition shows the correct job name',
);
assert.ok(
fullDefinition.TaskGroups,
'Full definition includes task groups',
);

await click('[data-test-select="full-definition"] button:first-child');
codeMirror = this.getCodeMirrorInstance();
assert.equal(
codeMirror.getValue(),
specification_response.Source,
'Switching back to job spec restores the original specification source',
);
});
});
113 changes: 113 additions & 0 deletions ui/tests/acceptance/regions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module('Acceptance | regions (only one)', function (hooks) {
setupMirage(hooks);

hooks.beforeEach(function () {
window.localStorage.clear();
this.server.create('agent');
this.server.create('node-pool');
this.server.create('node');
Expand Down Expand Up @@ -105,7 +106,11 @@ module('Acceptance | regions (many)', function (hooks) {
});

test('the region switcher is rendered in the nav bar and the region is in the page title', async function (assert) {
let managementToken = this.server.create('token');
window.localStorage.nomadTokenSecret = managementToken.secretId;

await JobsList.visit();
await settled();

assert.ok(
Layout.navbar.regionSwitcher.isPresent,
Expand Down Expand Up @@ -146,6 +151,114 @@ module('Acceptance | regions (many)', function (hooks) {
);
});

test('switching regions on a query-param-only transition refreshes the active route model', async function (assert) {
const newRegion = this.server.db.regions[1].id;

await JobsList.visit();

const jobsRequestsBeforeSwitch =
this.server.pretender.handledRequests.filter((request) =>
request.url.includes('/v1/jobs'),
).length;

await selectChoose('[data-test-region-switcher-parent]', newRegion);
await settled();

const jobsRequestsAfterSwitch =
this.server.pretender.handledRequests.filter((request) =>
request.url.includes('/v1/jobs'),
);

assert.ok(
jobsRequestsAfterSwitch.length > jobsRequestsBeforeSwitch,
'Jobs model request is issued again after region query-param switch'
);

assert.ok(
jobsRequestsAfterSwitch
.slice(jobsRequestsBeforeSwitch)
.some((request) => request.url.includes(`region=${newRegion}`)),
'Refreshed jobs request uses the selected region'
);
});

test('switching regions on job detail reloads job, allocations, and evaluations', async function (assert) {
const newRegion = this.server.db.regions[1].id;

await JobsList.visit();
const jobId = JobsList.jobs.objectAt(0).id;
await JobsList.jobs.objectAt(0).clickRow();
await settled();

const isForJobPath = (request, path) => {
const url = new URL(request.url, window.location.origin);
return url.pathname === `/v1/job/${jobId}${path}`;
};

const jobRequestsBeforeSwitch =
this.server.pretender.handledRequests.filter((request) =>
isForJobPath(request, ''),
).length;

const allocationRequestsBeforeSwitch =
this.server.pretender.handledRequests.filter((request) =>
isForJobPath(request, '/allocations'),
).length;

const evaluationRequestsBeforeSwitch =
this.server.pretender.handledRequests.filter((request) =>
isForJobPath(request, '/evaluations'),
).length;

await selectChoose('[data-test-region-switcher-parent]', newRegion);
await settled();

const jobRequestsAfterSwitch =
this.server.pretender.handledRequests.filter((request) =>
isForJobPath(request, ''),
);
const allocationRequestsAfterSwitch =
this.server.pretender.handledRequests.filter((request) =>
isForJobPath(request, '/allocations'),
);
const evaluationRequestsAfterSwitch =
this.server.pretender.handledRequests.filter((request) =>
isForJobPath(request, '/evaluations'),
);

assert.ok(
jobRequestsAfterSwitch.length > jobRequestsBeforeSwitch,
'Job record is fetched again after switching regions on job detail'
);
assert.ok(
allocationRequestsAfterSwitch.length > allocationRequestsBeforeSwitch,
'Job allocations are fetched again after switching regions on job detail'
);
assert.ok(
evaluationRequestsAfterSwitch.length > evaluationRequestsBeforeSwitch,
'Job evaluations are fetched again after switching regions on job detail'
);

assert.ok(
jobRequestsAfterSwitch
.slice(jobRequestsBeforeSwitch)
.some((request) => request.url.includes(`region=${newRegion}`)),
'Refetched job request includes selected region'
);
assert.ok(
allocationRequestsAfterSwitch
.slice(allocationRequestsBeforeSwitch)
.some((request) => request.url.includes(`region=${newRegion}`)),
'Refetched allocations request includes selected region'
);
assert.ok(
evaluationRequestsAfterSwitch
.slice(evaluationRequestsBeforeSwitch)
.some((request) => request.url.includes(`region=${newRegion}`)),
'Refetched evaluations request includes selected region'
);
});

test('switching regions to the default region, unsets the region query param', async function (assert) {
let managementToken = this.server.create('token');
window.localStorage.nomadTokenSecret = managementToken.secretId;
Expand Down
Loading