Merge pull request #8035 from hashicorp/f-ui/ember-fetch

UI: Replace jQuery with fetch within Ember Data
This commit is contained in:
Michael Lange 2020-05-26 12:32:03 -07:00 committed by GitHub
commit 3f40b3e3cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 95 additions and 110 deletions

View file

@ -8,6 +8,10 @@ import { default as NoLeaderError, NO_LEADER } from '../utils/no-leader-error';
export const namespace = 'v1';
export default RESTAdapter.extend({
// TODO: This can be removed once jquery-integration is turned off for
// the entire app.
useFetch: true,
namespace,
system: service(),

View file

@ -10,35 +10,6 @@ export default ApplicationAdapter.extend({
watchList: service(),
store: service(),
ajaxOptions(url, type, options) {
const ajaxOptions = this._super(url, type, options);
// Since ajax has been changed to include query params in the URL,
// we have to remove query params that are in the URL from the data
// object so they don't get passed along twice.
const [newUrl, params] = ajaxOptions.url.split('?');
const queryParams = queryString.parse(params);
ajaxOptions.url = !params ? newUrl : `${newUrl}?${queryString.stringify(queryParams)}`;
Object.keys(queryParams).forEach(key => {
delete ajaxOptions.data[key];
});
const abortToken = (options || {}).abortToken;
if (abortToken) {
delete options.abortToken;
const previousBeforeSend = ajaxOptions.beforeSend;
ajaxOptions.beforeSend = function(jqXHR) {
abortToken.capture(jqXHR);
if (previousBeforeSend) {
previousBeforeSend(...arguments);
}
};
}
return ajaxOptions;
},
// Overriding ajax is not advised, but this is a minimal modification
// that sets off a series of events that results in query params being
// available in handleResponse below. Unfortunately, this is the only
@ -53,6 +24,11 @@ export default ApplicationAdapter.extend({
const params = { ...options.data };
delete params.index;
// Options data gets appended as query params as part of ajaxOptions.
// In order to prevent doubling params, data should only include index
// at this point since everything else is added to the URL in advance.
options.data = options.data.index ? { index: options.data.index } : {};
return this._super(`${url}?${queryString.stringify(params)}`, type, options);
},
@ -64,9 +40,9 @@ export default ApplicationAdapter.extend({
params.index = this.watchList.getIndexFor(url);
}
const abortToken = get(snapshotRecordArray || {}, 'adapterOptions.abortToken');
const signal = get(snapshotRecordArray || {}, 'adapterOptions.abortController.signal');
return this.ajax(url, 'GET', {
abortToken,
signal,
data: params,
});
},
@ -79,9 +55,9 @@ export default ApplicationAdapter.extend({
params.index = this.watchList.getIndexFor(url);
}
const abortToken = get(snapshot || {}, 'adapterOptions.abortToken');
const signal = get(snapshot || {}, 'adapterOptions.abortController.signal');
return this.ajax(url, 'GET', {
abortToken,
signal,
data: params,
}).catch(error => {
if (error instanceof AbortError) {
@ -93,18 +69,18 @@ export default ApplicationAdapter.extend({
query(store, type, query, snapshotRecordArray, options, additionalParams = {}) {
const url = this.buildURL(type.modelName, null, null, 'query', query);
let [, params] = url.split('?');
let [urlPath, params] = url.split('?');
params = assign(queryString.parse(params) || {}, this.buildQuery(), additionalParams, query);
if (get(options, 'adapterOptions.watch')) {
// The intended query without additional blocking query params is used
// to track the appropriate query index.
params.index = this.watchList.getIndexFor(`${url}?${queryString.stringify(query)}`);
params.index = this.watchList.getIndexFor(`${urlPath}?${queryString.stringify(query)}`);
}
const abortToken = get(options, 'adapterOptions.abortToken');
return this.ajax(url, 'GET', {
abortToken,
const signal = get(options, 'adapterOptions.abortController.signal');
return this.ajax(urlPath, 'GET', {
signal,
data: params,
}).then(payload => {
const adapter = store.adapterFor(type.modelName);
@ -133,8 +109,8 @@ export default ApplicationAdapter.extend({
});
},
reloadRelationship(model, relationshipName, options = { watch: false, abortToken: null }) {
const { watch, abortToken } = options;
reloadRelationship(model, relationshipName, options = { watch: false, abortController: null }) {
const { watch, abortController } = options;
const relationship = model.relationshipFor(relationshipName);
if (relationship.kind !== 'belongsTo' && relationship.kind !== 'hasMany') {
throw new Error(
@ -158,7 +134,7 @@ export default ApplicationAdapter.extend({
}
return this.ajax(url, 'GET', {
abortToken,
signal: abortController && abortController.signal,
data: params,
}).then(
json => {

View file

@ -1,5 +1,6 @@
import Component from '@ember/component';
import { task } from 'ember-concurrency';
import { ForbiddenError } from '@ember-data/adapter/error';
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
export default Component.extend({
@ -15,9 +16,11 @@ export default Component.extend({
yield this.get('job.latestDeployment.content').promote();
} catch (err) {
let message = messageFromAdapterError(err);
if (!message || message === 'Forbidden') {
if (err instanceof ForbiddenError) {
message = 'Your ACL token does not grant permission to promote deployments.';
}
this.handleError({
title: 'Could Not Promote Deployment',
description: message,

View file

@ -1,5 +1,6 @@
import Component from '@ember/component';
import { task } from 'ember-concurrency';
import { ForbiddenError } from '@ember-data/adapter/error';
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
export default Component.extend({
@ -38,7 +39,8 @@ export default Component.extend({
job.set('status', 'running');
} catch (err) {
let message = messageFromAdapterError(err);
if (!message || message === 'Forbidden') {
if (err instanceof ForbiddenError) {
message = 'Your ACL token does not grant permission to stop jobs.';
}

View file

@ -4,12 +4,7 @@ import { computed } from '@ember/object';
import RSVP from 'rsvp';
import { logger } from 'nomad-ui/utils/classes/log';
import timeout from 'nomad-ui/utils/timeout';
class MockAbortController {
abort() {
/* noop */
}
}
import { AbortController } from 'fetch';
export default Component.extend({
token: service(),
@ -52,8 +47,7 @@ export default Component.extend({
// If the log request can't settle in one second, the client
// must be unavailable and the server should be used instead
// AbortControllers don't exist in IE11, so provide a mock if it doesn't exist
const aborter = window.AbortController ? new AbortController() : new MockAbortController();
const aborter = new AbortController();
const timing = this.useServer ? this.serverTimeout : this.clientTimeout;
// Capture the state of useServer at logger create time to avoid a race

View file

@ -4,6 +4,15 @@ export function initialize() {
// Provides the app config to all templates
application.inject('controller', 'config', 'service:config');
application.inject('component', 'config', 'service:config');
const jQuery = window.jQuery;
jQuery.__ajax = jQuery.ajax;
jQuery.ajax = function() {
// eslint-disable-next-line
console.log('jQuery.ajax called:', ...arguments);
return jQuery.__ajax(...arguments);
};
}
export default {

View file

@ -1,11 +0,0 @@
export default class XHRToken {
capture(xhr) {
this._xhr = xhr;
}
abort() {
if (this._xhr) {
this._xhr.abort();
}
}
}

View file

@ -3,9 +3,9 @@ import { get } from '@ember/object';
import { assert } from '@ember/debug';
import RSVP from 'rsvp';
import { task } from 'ember-concurrency';
import { AbortController } from 'fetch';
import wait from 'nomad-ui/utils/wait';
import Watchable from 'nomad-ui/adapters/watchable';
import XHRToken from 'nomad-ui/utils/classes/xhr-token';
import config from 'nomad-ui/config/environment';
const isEnabled = config.APP.blockingQueries !== false;
@ -16,7 +16,7 @@ export function watchRecord(modelName) {
'To watch a record, the record adapter MUST extend Watchable',
this.store.adapterFor(modelName) instanceof Watchable
);
const token = new XHRToken();
const controller = new AbortController();
if (typeof id === 'object') {
id = get(id, 'id');
}
@ -25,7 +25,7 @@ export function watchRecord(modelName) {
yield RSVP.all([
this.store.findRecord(modelName, id, {
reload: true,
adapterOptions: { watch: true, abortToken: token },
adapterOptions: { watch: true, abortController: controller },
}),
wait(throttle),
]);
@ -33,7 +33,7 @@ export function watchRecord(modelName) {
yield e;
break;
} finally {
token.abort();
controller.abort();
}
}
}).drop();
@ -45,20 +45,23 @@ export function watchRelationship(relationshipName) {
'To watch a relationship, the adapter of the model provided to the watchRelationship task MUST extend Watchable',
this.store.adapterFor(model.constructor.modelName) instanceof Watchable
);
const token = new XHRToken();
const controller = new AbortController();
while (isEnabled && !Ember.testing) {
try {
yield RSVP.all([
this.store
.adapterFor(model.constructor.modelName)
.reloadRelationship(model, relationshipName, { watch: true, abortToken: token }),
.reloadRelationship(model, relationshipName, {
watch: true,
abortController: controller,
}),
wait(throttle),
]);
} catch (e) {
yield e;
break;
} finally {
token.abort();
controller.abort();
}
}
}).drop();
@ -70,13 +73,13 @@ export function watchAll(modelName) {
'To watch all, the respective adapter MUST extend Watchable',
this.store.adapterFor(modelName) instanceof Watchable
);
const token = new XHRToken();
const controller = new AbortController();
while (isEnabled && !Ember.testing) {
try {
yield RSVP.all([
this.store.findAll(modelName, {
reload: true,
adapterOptions: { watch: true, abortToken: token },
adapterOptions: { watch: true, abortController: controller },
}),
wait(throttle),
]);
@ -84,7 +87,7 @@ export function watchAll(modelName) {
yield e;
break;
} finally {
token.abort();
controller.abort();
}
}
}).drop();
@ -96,13 +99,13 @@ export function watchQuery(modelName) {
'To watch a query, the adapter for the type being queried MUST extend Watchable',
this.store.adapterFor(modelName) instanceof Watchable
);
const token = new XHRToken();
const controller = new AbortController();
while (isEnabled && !Ember.testing) {
try {
yield RSVP.all([
this.store.query(modelName, params, {
reload: true,
adapterOptions: { watch: true, abortToken: token },
adapterOptions: { watch: true, abortController: controller },
}),
wait(throttle),
]);
@ -110,7 +113,7 @@ export function watchQuery(modelName) {
yield e;
break;
} finally {
token.abort();
controller.abort();
}
}
}).drop();

View file

@ -44,7 +44,7 @@ export default function() {
// Annotate the response with the index
if (response instanceof Response) {
response.headers['X-Nomad-Index'] = index;
response.headers['x-nomad-index'] = index;
return response;
}
return new Response(200, { 'x-nomad-index': index }, response);
@ -316,7 +316,7 @@ export default function() {
});
this.get('/acl/token/self', function({ tokens }, req) {
const secret = req.requestHeaders['X-Nomad-Token'];
const secret = req.requestHeaders['x-nomad-token'];
const tokenForSecret = tokens.findBy({ secretId: secret });
// Return the token if it exists
@ -330,7 +330,7 @@ export default function() {
this.get('/acl/token/:id', function({ tokens }, req) {
const token = tokens.find(req.params.id);
const secret = req.requestHeaders['X-Nomad-Token'];
const secret = req.requestHeaders['x-nomad-token'];
const tokenForSecret = tokens.findBy({ secretId: secret });
// Return the token only if the request header matches the token
@ -345,7 +345,7 @@ export default function() {
this.get('/acl/policy/:id', function({ policies, tokens }, req) {
const policy = policies.find(req.params.id);
const secret = req.requestHeaders['X-Nomad-Token'];
const secret = req.requestHeaders['x-nomad-token'];
const tokenForSecret = tokens.findBy({ secretId: secret });
if (req.params.id === 'anonymous') {

View file

@ -1,4 +1,4 @@
import { currentURL, waitUntil } from '@ember/test-helpers';
import { currentURL, waitUntil, settled } from '@ember/test-helpers';
import { assign } from '@ember/polyfills';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
@ -569,6 +569,8 @@ module('Acceptance | client detail', function(hooks) {
assert.ok(ClientDetail.eligibilityToggle.isDisabled);
server.pretender.resolve(server.pretender.requestReferences[0].request);
await settled();
assert.notOk(ClientDetail.eligibilityToggle.isActive);
assert.notOk(ClientDetail.eligibilityToggle.isDisabled);

View file

@ -39,7 +39,7 @@ module('Acceptance | tokens', function(hooks) {
});
// TODO: unskip once store.unloadAll reliably waits for in-flight requests to settle
skip('the X-Nomad-Token header gets sent with requests once it is set', async function(assert) {
skip('the x-nomad-token header gets sent with requests once it is set', async function(assert) {
const { secretId } = managementToken;
await JobDetail.visit({ id: job.id });
@ -48,7 +48,7 @@ module('Acceptance | tokens', function(hooks) {
assert.ok(server.pretender.handledRequests.length > 1, 'Requests have been made');
server.pretender.handledRequests.forEach(req => {
assert.notOk(getHeader(req, 'X-Nomad-Token'), `No token for ${req.url}`);
assert.notOk(getHeader(req, 'x-nomad-token'), `No token for ${req.url}`);
});
const requestPosition = server.pretender.handledRequests.length;
@ -64,7 +64,7 @@ module('Acceptance | tokens', function(hooks) {
// Cross-origin requests can't have a token
newRequests.forEach(req => {
assert.equal(getHeader(req, 'X-Nomad-Token'), secretId, `Token set for ${req.url}`);
assert.equal(getHeader(req, 'x-nomad-token'), secretId, `Token set for ${req.url}`);
});
});

View file

@ -83,7 +83,7 @@ module('Integration | Component | job-page/periodic', function(hooks) {
});
test('Clicking force launch without proper permissions shows an error message', async function(assert) {
this.server.pretender.post('/v1/job/:id/periodic/force', () => [403, {}, null]);
this.server.pretender.post('/v1/job/:id/periodic/force', () => [403, {}, '']);
this.server.create('job', 'periodic', {
id: 'parent',
@ -138,7 +138,7 @@ module('Integration | Component | job-page/periodic', function(hooks) {
});
test('Stopping a job without proper permissions shows an error message', async function(assert) {
this.server.pretender.delete('/v1/job/:id', () => [403, {}, null]);
this.server.pretender.delete('/v1/job/:id', () => [403, {}, '']);
const mirageJob = this.server.create('job', 'periodic', {
childrenCount: 0,
@ -175,7 +175,7 @@ module('Integration | Component | job-page/periodic', function(hooks) {
});
test('Starting a job without proper permissions shows an error message', async function(assert) {
this.server.pretender.post('/v1/job/:id', () => [403, {}, null]);
this.server.pretender.post('/v1/job/:id', () => [403, {}, '']);
const mirageJob = this.server.create('job', 'periodic', {
childrenCount: 0,

View file

@ -69,7 +69,7 @@ module('Integration | Component | job-page/service', function(hooks) {
});
test('Stopping a job without proper permissions shows an error message', async function(assert) {
this.server.pretender.delete('/v1/job/:id', () => [403, {}, null]);
this.server.pretender.delete('/v1/job/:id', () => [403, {}, '']);
const mirageJob = makeMirageJob(this.server);
await this.store.findAll('job');
@ -97,7 +97,7 @@ module('Integration | Component | job-page/service', function(hooks) {
});
test('Starting a job without proper permissions shows an error message', async function(assert) {
this.server.pretender.post('/v1/job/:id', () => [403, {}, null]);
this.server.pretender.post('/v1/job/:id', () => [403, {}, '']);
const mirageJob = makeMirageJob(this.server, { status: 'dead' });
await this.store.findAll('job');
@ -189,7 +189,7 @@ module('Integration | Component | job-page/service', function(hooks) {
});
test('When promoting the active deployment fails, an error is shown', async function(assert) {
this.server.pretender.post('/v1/deployment/promote/:id', () => [403, {}, null]);
this.server.pretender.post('/v1/deployment/promote/:id', () => [403, {}, '']);
this.server.create('node');
const mirageJob = makeMirageJob(this.server, { activeDeployment: true });

View file

@ -4,7 +4,7 @@ import { settled } from '@ember/test-helpers';
import { setupTest } from 'ember-qunit';
import { module, test } from 'qunit';
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
import XHRToken from 'nomad-ui/utils/classes/xhr-token';
import { AbortController } from 'fetch';
module('Unit | Adapter | Job', function(hooks) {
setupTest(hooks);
@ -133,7 +133,7 @@ module('Unit | Adapter | Job', function(hooks) {
await settled();
assert.notOk(
pretender.handledRequests.mapBy('requestHeaders').some(headers => headers['X-Nomad-Token']),
pretender.handledRequests.mapBy('requestHeaders').some(headers => headers['x-nomad-token']),
'No token header present on either job request'
);
});
@ -152,7 +152,7 @@ module('Unit | Adapter | Job', function(hooks) {
assert.ok(
pretender.handledRequests
.mapBy('requestHeaders')
.every(headers => headers['X-Nomad-Token'] === secret),
.every(headers => headers['x-nomad-token'] === secret),
'The token header is present on both job requests'
);
});
@ -261,14 +261,14 @@ module('Unit | Adapter | Job', function(hooks) {
await this.initializeUI();
const { pretender } = this.server;
const token = new XHRToken();
const controller = new AbortController();
pretender.get('/v1/jobs', () => [200, {}, '[]'], true);
this.subject()
.findAll(null, { modelName: 'job' }, null, {
reload: true,
adapterOptions: { watch: true, abortToken: token },
adapterOptions: { watch: true, abortController: controller },
})
.catch(() => {});
@ -277,7 +277,7 @@ module('Unit | Adapter | Job', function(hooks) {
// Schedule the cancelation before waiting
run.next(() => {
token.abort();
controller.abort();
});
await settled();
@ -289,13 +289,13 @@ module('Unit | Adapter | Job', function(hooks) {
const { pretender } = this.server;
const jobId = JSON.stringify(['job-1', 'default']);
const token = new XHRToken();
const controller = new AbortController();
pretender.get('/v1/job/:id', () => [200, {}, '{}'], true);
this.subject().findRecord(null, { modelName: 'job' }, jobId, {
reload: true,
adapterOptions: { watch: true, abortToken: token },
adapterOptions: { watch: true, abortController: controller },
});
const { request: xhr } = pretender.requestReferences[0];
@ -303,7 +303,7 @@ module('Unit | Adapter | Job', function(hooks) {
// Schedule the cancelation before waiting
run.next(() => {
token.abort();
controller.abort();
});
await settled();
@ -315,18 +315,21 @@ module('Unit | Adapter | Job', function(hooks) {
const { pretender } = this.server;
const plainId = 'job-1';
const token = new XHRToken();
const controller = new AbortController();
const mockModel = makeMockModel(plainId);
pretender.get('/v1/job/:id/summary', () => [200, {}, '{}'], true);
this.subject().reloadRelationship(mockModel, 'summary', { watch: true, abortToken: token });
this.subject().reloadRelationship(mockModel, 'summary', {
watch: true,
abortController: controller,
});
const { request: xhr } = pretender.requestReferences[0];
assert.equal(xhr.status, 0, 'Request is still pending');
// Schedule the cancelation before waiting
run.next(() => {
token.abort();
controller.abort();
});
await settled();
@ -338,19 +341,19 @@ module('Unit | Adapter | Job', function(hooks) {
const { pretender } = this.server;
const jobId = JSON.stringify(['job-1', 'default']);
const token1 = new XHRToken();
const token2 = new XHRToken();
const controller1 = new AbortController();
const controller2 = new AbortController();
pretender.get('/v1/job/:id', () => [200, {}, '{}'], true);
this.subject().findRecord(null, { modelName: 'job' }, jobId, {
reload: true,
adapterOptions: { watch: true, abortToken: token1 },
adapterOptions: { watch: true, abortController: controller1 },
});
this.subject().findRecord(null, { modelName: 'job' }, jobId, {
reload: true,
adapterOptions: { watch: true, abortToken: token2 },
adapterOptions: { watch: true, abortController: controller2 },
});
const { request: xhr } = pretender.requestReferences[0];
@ -365,7 +368,7 @@ module('Unit | Adapter | Job', function(hooks) {
// Schedule the cancelation and resolution before waiting
run.next(() => {
token1.abort();
controller1.abort();
pretender.resolve(xhr2);
});

View file

@ -3,7 +3,7 @@ import { settled } from '@ember/test-helpers';
import { setupTest } from 'ember-qunit';
import { module, test } from 'qunit';
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
import XHRToken from 'nomad-ui/utils/classes/xhr-token';
import { AbortController } from 'fetch';
module('Unit | Adapter | Volume', function(hooks) {
setupTest(hooks);
@ -113,14 +113,14 @@ module('Unit | Adapter | Volume', function(hooks) {
await this.initializeUI();
const { pretender } = this.server;
const token = new XHRToken();
const controller = new AbortController();
pretender.get('/v1/volumes', () => [200, {}, '[]'], true);
this.subject()
.query(this.store, { modelName: 'volume' }, { type: 'csi' }, null, {
reload: true,
adapterOptions: { watch: true, abortToken: token },
adapterOptions: { watch: true, abortController: controller },
})
.catch(() => {});
@ -129,7 +129,7 @@ module('Unit | Adapter | Volume', function(hooks) {
// Schedule the cancelation before waiting
run.next(() => {
token.abort();
controller.abort();
});
await settled();