ui: Remove jQuery from the production build (#8088)

* ui: Split up client/http and replace $.ajax

This splits the client/http service more in the following ways:

1. Connections are now split out into its own service
2. The transport is now split out into its own service that returns a
listener based http transport
3. Various string parsing/stringifying functions are now split out into
utils

* Remove jQuery from our production build

* Move the coverage serving to the server.js file

* Self review amends

* Add X-Requested-With header

* Move some files around, externalize some functions

* Move connection tracking to use native Set

* Ensure HTTP parsing doesn't encode headers

In the future this will change to deal with all HTTP parsing in one
place, hence the commented out METHOD_PARSING etc

* Start to fix up integration tests to use requestParams
This commit is contained in:
John Cowen 2020-07-07 19:58:46 +01:00 committed by GitHub
parent 088e1d5693
commit f50438e76f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 684 additions and 526 deletions

View File

@ -81,6 +81,9 @@ export default Adapter.extend({
// });
},
error: function(err) {
if (err instanceof TypeError) {
throw err;
}
const errors = [
{
status: `${err.statusCode}`,

View File

@ -0,0 +1,64 @@
import Service, { inject as service } from '@ember/service';
export default Service.extend({
dom: service('dom'),
env: service('env'),
init: function() {
this._super(...arguments);
this._listeners = this.dom.listeners();
this.connections = new Set();
this.addVisibilityChange();
},
willDestroy: function() {
this._listeners.remove();
this.purge();
this._super(...arguments);
},
addVisibilityChange: function() {
// when the user hides the tab, abort all connections
this._listeners.add(this.dom.document(), {
visibilitychange: e => {
if (e.target.hidden) {
this.purge();
}
},
});
},
whenAvailable: function(e) {
// if the user has hidden the tab (hidden browser/tab switch)
// any aborted errors should restart
const doc = this.dom.document();
if (doc.hidden) {
return new Promise(resolve => {
const remove = this._listeners.add(doc, {
visibilitychange: function(event) {
remove();
// we resolve with the event that comes from
// whenAvailable not visibilitychange
resolve(e);
},
});
});
}
return Promise.resolve(e);
},
purge: function() {
[...this.connections].forEach(function(connection) {
// Cancelled
connection.abort(0);
});
this.connections = new Set();
},
acquire: function(request) {
this.connections.add(request);
if (this.connections.size > this.env.var('CONSUL_HTTP_MAX_CONNECTIONS')) {
const connection = this.connections.values().next().value;
this.connections.delete(connection);
// Too Many Requests
connection.abort(429);
}
},
release: function(request) {
this.connections.delete(request);
},
});

View File

@ -1,15 +1,13 @@
/*global $*/
import Service, { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import { get } from '@ember/object';
import { next } from '@ember/runloop';
import { CACHE_CONTROL, CONTENT_TYPE } from 'consul-ui/utils/http/headers';
import { HEADERS_TOKEN as CONSUL_TOKEN } from 'consul-ui/utils/http/consul';
import { env } from 'consul-ui/env';
import getObjectPool from 'consul-ui/utils/get-object-pool';
import Request from 'consul-ui/utils/http/request';
import createURL from 'consul-ui/utils/createURL';
import createURL from 'consul-ui/utils/http/create-url';
import createHeaders from 'consul-ui/utils/http/create-headers';
import createQueryParams from 'consul-ui/utils/http/create-query-params';
// reopen EventSources if a user changes tab
export const restartWhenAvailable = function(client) {
@ -26,250 +24,188 @@ export const restartWhenAvailable = function(client) {
throw e;
};
};
class HTTPError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
}
}
const dispose = function(request) {
if (request.headers()[CONTENT_TYPE.toLowerCase()] === 'text/event-stream') {
const xhr = request.connection();
// unsent and opened get aborted
// headers and loading means wait for it
// to finish for the moment
if (xhr.readyState) {
switch (xhr.readyState) {
case 0:
case 1:
xhr.abort();
break;
}
}
}
return request;
};
// TODO: Potentially url should check if any of the params
// passed to it are undefined (null is fine). We could then get rid of the
// multitude of checks we do throughout the adapters
// right now createURL converts undefined to '' so we need to check thats not needed
// anywhere (todo written here for visibility)
const url = createURL(encodeURIComponent);
const createHeaders = function(lines) {
return lines.reduce(function(prev, item) {
const temp = item.split(':');
if (temp.length > 1) {
prev[temp[0].trim()] = temp[1].trim();
const stringifyQueryParams = createQueryParams(encodeURIComponent);
const parseURL = createURL(encodeURIComponent, stringifyQueryParams);
const parseHeaders = createHeaders();
const parseBody = function(strs, ...values) {
let body = {};
const doubleBreak = strs.reduce(function(prev, item, i) {
// Ensure each line has no whitespace either end, including empty lines
item = item
.split('\n')
.map(item => item.trim())
.join('\n');
if (item.indexOf('\n\n') !== -1) {
return i;
}
return prev;
}, {});
}, -1);
if (doubleBreak !== -1) {
// This merges request bodies together, so you can specify multiple bodies
// in the request and it will merge them together.
// Turns out we never actually do this, so it might be worth removing as it complicates
// matters slightly as we assumed post bodies would be an object.
// This actually works as it just uses the value of the first object, if its an array
// it concats
body = values.splice(doubleBreak).reduce(function(prev, item, i) {
switch (true) {
case Array.isArray(item):
if (i === 0) {
prev = [];
}
return prev.concat(item);
case typeof item !== 'string':
return {
...prev,
...item,
};
default:
return item;
}
}, body);
}
return [body, ...values];
};
const CLIENT_HEADERS = [CACHE_CONTROL];
export default Service.extend({
dom: service('dom'),
connections: service('client/connections'),
transport: service('client/transports/xhr'),
settings: service('settings'),
init: function() {
this._super(...arguments);
this._listeners = this.dom.listeners();
const maxConnections = env('CONSUL_HTTP_MAX_CONNECTIONS');
set(this, 'connections', getObjectPool(dispose, maxConnections));
if (typeof maxConnections !== 'undefined') {
set(this, 'maxConnections', maxConnections);
// when the user hides the tab, abort all connections
this._listeners.add(this.dom.document(), {
visibilitychange: e => {
if (e.target.hidden) {
this.connections.purge();
}
},
});
}
},
willDestroy: function() {
this._listeners.remove();
this.connections.purge();
set(this, 'connections', undefined);
this._super(...arguments);
},
url: function() {
return url(...arguments);
return parseURL(...arguments);
},
body: function(strs, ...values) {
let body = {};
const doubleBreak = strs.reduce(function(prev, item, i) {
// Ensure each line has no whitespace either end, including empty lines
item = item
.split('\n')
.map(item => item.trim())
.join('\n');
if (item.indexOf('\n\n') !== -1) {
return i;
body: function() {
return parseBody(...arguments);
},
requestParams: function(strs, ...values) {
// first go to the end and remove/parse the http body
const [body, ...urlVars] = this.body(...arguments);
// with whats left get the method off the front
const [method, ...urlParts] = this.url(strs, ...urlVars).split(' ');
// with whats left use the rest of the line for the url
// with whats left after the line, use for the headers
const [url, ...headerParts] = urlParts.join(' ').split('\n');
const params = {
url: url.trim(),
method: method.trim(),
headers: {
[CONTENT_TYPE]: 'application/json; charset=utf-8',
...parseHeaders(headerParts),
},
body: null,
data: body,
};
// Remove and save things that shouldn't be sent in the request
params.clientHeaders = CLIENT_HEADERS.reduce(function(prev, item) {
if (typeof params.headers[item] !== 'undefined') {
prev[item.toLowerCase()] = params.headers[item];
delete params.headers[item];
}
return prev;
}, -1);
if (doubleBreak !== -1) {
// This merges request bodies together, so you can specify multiple bodies
// in the request and it will merge them together.
// Turns out we never actually do this, so it might be worth removing as it complicates
// matters slightly as we assumed post bodies would be an object.
// This actually works as it just uses the value of the first object, if its an array
// it concats
body = values.splice(doubleBreak).reduce(function(prev, item, i) {
switch (true) {
case Array.isArray(item):
if (i === 0) {
prev = [];
}
return prev.concat(item);
case typeof item !== 'string':
return {
...prev,
...item,
};
default:
return item;
}, {});
if (typeof body !== 'undefined') {
// Only read add HTTP body if we aren't GET
// Right now we do this to avoid having to put data in the templates
// for write-like actions
// potentially we should change things so you _have_ to do that
// as doing it this way is a little magical
if (params.method !== 'GET') {
if (params.headers[CONTENT_TYPE].indexOf('json') !== -1) {
params.body = JSON.stringify(params.data);
} else {
if (
(typeof params.data === 'string' && params.data.length > 0) ||
Object.keys(params.data).length > 0
) {
params.body = params.data;
}
}
}, body);
} else {
const str = stringifyQueryParams(params.data);
if (str.length > 0) {
if (params.url.indexOf('?') !== -1) {
params.url = `${params.url}&${str}`;
} else {
params.url = `${params.url}?${str}`;
}
}
}
}
return [body, ...values];
// temporarily reset the headers/content-type so it works the same
// as previously, should be able to remove this once the data layer
// rewrite is over and we can assert sending via form-encoded is fine
// also see adapters/kv content-types in requestForCreate/UpdateRecord
// also see https://github.com/hashicorp/consul/issues/3804
params.headers[CONTENT_TYPE] = 'application/json; charset=utf-8';
return params;
},
request: function(cb) {
const client = this;
return cb(function(strs, ...values) {
// first go to the end and remove/parse the http body
const [body, ...urlVars] = client.body(...arguments);
// with whats left get the method off the front
const [method, ...urlParts] = client.url(strs, ...urlVars).split(' ');
// with whats left use the rest of the line for the url
// with whats left after the line, use for the headers
const [url, ...headerParts] = urlParts.join(' ').split('\n');
return client.settings.findBySlug('token').then(function(token) {
const requestHeaders = createHeaders(headerParts);
const headers = {
// default to application/json
...{
[CONTENT_TYPE]: 'application/json; charset=utf-8',
},
// add any application level headers
...{
const params = client.requestParams(...arguments);
return client.settings.findBySlug('token').then(token => {
const options = {
...params,
headers: {
[CONSUL_TOKEN]: typeof token.SecretID === 'undefined' ? '' : token.SecretID,
...params.headers,
},
// but overwrite or add to those from anything in the specific request
...requestHeaders,
};
// We use cache-control in the response
// but we don't want to send it, but we artificially
// tag it onto the response below if it is set on the request
delete headers[CACHE_CONTROL];
return new Promise(function(resolve, reject) {
const options = {
url: url.trim(),
method: method,
contentType: headers[CONTENT_TYPE],
// type: 'json',
complete: function(xhr, textStatus) {
client.complete(this.id);
const request = client.transport.request(options);
return new Promise((resolve, reject) => {
const remove = client._listeners.add(request, {
open: e => {
client.acquire(e.target);
},
success: function(response, status, xhr) {
const headers = createHeaders(xhr.getAllResponseHeaders().split('\n'));
if (typeof requestHeaders[CACHE_CONTROL] !== 'undefined') {
// if cache-control was on the request, artificially tag
// it back onto the response, also see comment above
headers[CACHE_CONTROL] = requestHeaders[CACHE_CONTROL];
}
const respond = function(cb) {
return cb(headers, response);
message: e => {
const headers = {
...Object.entries(e.data.headers).reduce(function(prev, [key, value], i) {
if (!CLIENT_HEADERS.includes(key)) {
prev[key] = value;
}
return prev;
}, {}),
...params.clientHeaders,
};
// TODO: nextTick ?
resolve(respond);
const respond = function(cb) {
return cb(headers, e.data.response);
};
next(() => resolve(respond));
},
error: function(xhr, textStatus, err) {
let error;
if (err instanceof Error) {
error = err;
} else {
let status = xhr.status;
// TODO: Not sure if we actually need this, but ember-data checks it
if (textStatus === 'abort') {
status = 0;
}
if (textStatus === 'timeout') {
status = 408;
}
error = new HTTPError(status, xhr.responseText);
}
//TODO: nextTick ?
reject(error);
error: e => {
next(() => reject(e.error));
},
converters: {
'text json': function(response) {
try {
return $.parseJSON(response);
} catch (e) {
return response;
}
},
close: e => {
client.release(e.target);
remove();
},
};
if (typeof body !== 'undefined') {
// Only read add HTTP body if we aren't GET
// Right now we do this to avoid having to put data in the templates
// for write-like actions
// potentially we should change things so you _have_ to do that
// as doing it this way is a little magical
if (method !== 'GET' && headers[CONTENT_TYPE].indexOf('json') !== -1) {
options.data = JSON.stringify(body);
} else {
// TODO: Does this need urlencoding? Assuming jQuery does this
options.data = body;
}
}
// temporarily reset the headers/content-type so it works the same
// as previously, should be able to remove this once the data layer
// rewrite is over and we can assert sending via form-encoded is fine
// also see adapters/kv content-types in requestForCreate/UpdateRecord
// also see https://github.com/hashicorp/consul/issues/3804
options.contentType = 'application/json; charset=utf-8';
headers[CONTENT_TYPE] = options.contentType;
//
options.beforeSend = function(xhr) {
if (headers) {
Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]));
}
this.id = client.acquire(options, xhr);
};
return $.ajax(options);
});
request.fetch();
});
});
});
},
abort: function(id = null) {
this.connections.purge();
},
whenAvailable: function(e) {
// if we are using a connection limited protocol and the user has hidden the tab (hidden browser/tab switch)
// any aborted errors should restart
const doc = this.dom.document();
if (typeof this.maxConnections !== 'undefined' && doc.hidden) {
return new Promise(resolve => {
const remove = this._listeners.add(doc, {
visibilitychange: function(event) {
remove();
// we resolve with the event that comes from
// whenAvailable not visibilitychange
resolve(e);
},
});
});
}
return Promise.resolve(e);
return this.connections.whenAvailable(e);
},
acquire: function(options, xhr) {
const request = new Request(options.method, options.url, { body: options.data || {} }, xhr);
return this.connections.acquire(request, request.getId());
abort: function() {
return this.connections.purge(...arguments);
},
complete: function() {
acquire: function() {
return this.connections.acquire(...arguments);
},
release: function() {
return this.connections.release(...arguments);
},
});

View File

@ -0,0 +1,57 @@
import Service from '@ember/service';
import createHeaders from 'consul-ui/utils/http/create-headers';
import createXHR from 'consul-ui/utils/http/xhr';
import Request from 'consul-ui/utils/http/request';
import HTTPError from 'consul-ui/utils/http/error';
const xhr = createXHR(createHeaders());
export default Service.extend({
xhr: function(options) {
return xhr(options);
},
request: function(params) {
const request = new Request(params.method, params.url, { body: params.data || {} });
const options = {
...params,
beforeSend: function(xhr) {
request.open(xhr);
},
converters: {
'text json': function(response) {
try {
return JSON.parse(response);
} catch (e) {
return response;
}
},
},
success: function(headers, response, status, statusText) {
// Response-ish
request.respond({
headers: headers,
response: response,
status: status,
statusText: statusText,
});
},
error: function(headers, response, status, statusText, err) {
let error;
if (err instanceof Error) {
error = err;
} else {
error = new HTTPError(status, response);
}
request.error(error);
},
complete: function(status) {
request.close();
},
};
request.fetch = () => {
this.xhr(options);
};
return request;
},
});

View File

@ -1,35 +0,0 @@
export default function(encode) {
return function(strs, ...values) {
return strs
.map(function(item, i) {
let val = typeof values[i] === 'undefined' ? '' : values[i];
switch (true) {
case typeof val === 'string':
val = encode(val);
break;
case Array.isArray(val):
val = val
.map(function(item) {
return `${encode(item)}`;
}, '')
.join('/');
break;
case typeof val === 'object':
val = Object.keys(val)
.reduce(function(prev, key) {
if (val[key] === null) {
return prev.concat(`${encode(key)}`);
} else if (typeof val[key] !== 'undefined') {
return prev.concat(`${encode(key)}=${encode(val[key])}`);
}
return prev;
}, [])
.join('&');
break;
}
return `${item}${val}`;
})
.join('')
.trim();
};
}

View File

@ -1,52 +0,0 @@
export default function(dispose = function() {}, max, objects = []) {
return {
acquire: function(obj, id) {
// TODO: what should happen if an ID already exists
// should we ignore and release both? Or prevent from acquiring? Or generate a unique ID?
// what happens if we can't get an id via getId or .id?
// could potentially use Set
objects.push(obj);
if (typeof max !== 'undefined') {
if (objects.length > max) {
return dispose(objects.shift());
}
}
return id;
},
// release releases the obj from the pool but **doesn't** dispose it
release: function(obj) {
let index = -1;
let id;
if (typeof obj === 'string') {
id = obj;
} else {
id = obj.id;
}
objects.forEach(function(item, i) {
let itemId;
if (typeof item.getId === 'function') {
itemId = item.getId();
} else {
itemId = item.id;
}
if (itemId === id) {
index = i;
}
});
if (index !== -1) {
return objects.splice(index, 1)[0];
}
},
purge: function() {
let obj;
const objs = [];
while ((obj = objects.shift())) {
objs.push(dispose(obj));
}
return objs;
},
dispose: function(id) {
return dispose(this.release(id));
},
};
}

View File

@ -0,0 +1,11 @@
export default function() {
return function(lines) {
return lines.reduce(function(prev, item) {
const temp = item.split(':');
if (temp.length > 1) {
prev[temp[0].trim()] = temp[1].trim();
}
return prev;
}, {});
};
}

View File

@ -0,0 +1,27 @@
export default function(encode) {
return function stringify(obj, parent) {
return Object.entries(obj)
.reduce(function(prev, [key, value], i) {
// if the value is undefined do nothing
if (typeof value === 'undefined') {
return prev;
}
let prop = encode(key);
// if we have a parent, prefix the property with that
if (typeof parent !== 'undefined') {
prop = `${parent}[${prop}]`;
}
// if the value is null just print the prop
if (value === null) {
return prev.concat(prop);
}
// anything nested, recur
if (typeof value === 'object') {
return prev.concat(stringify(value, prop));
}
// anything else print prop=value
return prev.concat(`${prop}=${encode(value)}`);
}, [])
.join('&');
};
}

View File

@ -0,0 +1,72 @@
// const METHOD_PARSING = 0;
const PATH_PARSING = 1;
const QUERY_PARSING = 2;
const HEADER_PARSING = 3;
const BODY_PARSING = 4;
export default function(encode, queryParams) {
return function(strs, ...values) {
// TODO: Potentially url should check if any of the params
// passed to it are undefined (null is fine). We could then get rid of the
// multitude of checks we do throughout the adapters
// right now create-url converts undefined to '' so we need to check thats not needed
// anywhere
let state = PATH_PARSING;
return strs
.map(function(item, i, arr) {
if (i === 0) {
item = item.trimStart();
}
// if(item.indexOf(' ') !== -1 && state === METHOD_PARSING) {
// state = PATH_PARSING;
// }
if (item.indexOf('?') !== -1 && state === PATH_PARSING) {
state = QUERY_PARSING;
}
if (item.indexOf('\n\n') !== -1) {
state = BODY_PARSING;
}
if (item.indexOf('\n') !== -1 && state !== BODY_PARSING) {
state = HEADER_PARSING;
}
let val = typeof values[i] !== 'undefined' ? values[i] : '';
switch (state) {
case PATH_PARSING:
switch (true) {
// encode strings
case typeof val === 'string':
val = encode(val);
break;
// split encode and join arrays by `/`
case Array.isArray(val):
val = val
.map(function(item) {
return `${encode(item)}`;
}, '')
.join('/');
break;
}
break;
case QUERY_PARSING:
switch (true) {
case typeof val === 'string':
val = encode(val);
break;
// objects offload to queryParams for encoding
case typeof val === 'object':
val = queryParams(val);
break;
}
break;
case BODY_PARSING:
// ignore body until we parse it here
return item.split('\n\n')[0];
// case METHOD_PARSING:
case HEADER_PARSING:
// passthrough/ignore method and headers until we parse them here
}
return `${item}${val}`;
})
.join('')
.trim();
};
}

View File

@ -0,0 +1,6 @@
export default class extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
}
}

View File

@ -1,13 +1,13 @@
export default class {
constructor(method, url, headers, xhr) {
this._xhr = xhr;
import EventTarget from 'consul-ui/utils/dom/event-target/rsvp';
export default class extends EventTarget {
constructor(method, url, headers) {
super();
this._url = url;
this._method = method;
this._headers = headers;
this._headers = {
...headers,
'content-type': 'application/json',
'x-request-id': `${this._method} ${this._url}?${JSON.stringify(headers.body)}`,
};
if (typeof this._headers.body.index !== 'undefined') {
// this should probably be in a response
@ -17,13 +17,43 @@ export default class {
headers() {
return this._headers;
}
getId() {
return this._headers['x-request-id'];
open(xhr) {
this._xhr = xhr;
this.dispatchEvent({ type: 'open' });
}
abort() {
this._xhr.abort();
respond(data) {
this.dispatchEvent({ type: 'message', data: data });
}
error(error) {
// if the xhr was aborted (status = 0)
// and this requests was aborted with a different status
// switch the status
if (error.statusCode === 0 && typeof this.statusCode !== 'undefined') {
error.statusCode = this.statusCode;
}
this.dispatchEvent({ type: 'error', error: error });
}
close() {
this.dispatchEvent({ type: 'close' });
}
connection() {
return this._xhr;
}
abort(statusCode = 0) {
if (this.headers()['content-type'] === 'text/event-stream') {
this.statusCode = statusCode;
const xhr = this.connection();
// unsent and opened get aborted
// headers and loading means wait for it
// to finish for the moment
if (xhr.readyState) {
switch (xhr.readyState) {
case 0:
case 1:
xhr.abort();
break;
}
}
}
}
}

View File

@ -0,0 +1,29 @@
export default function(parseHeaders, XHR) {
return function(options) {
const xhr = new (XHR || XMLHttpRequest)();
xhr.onreadystatechange = function() {
if (this.readyState === 4) {
const headers = parseHeaders(this.getAllResponseHeaders().split('\n'));
if (this.status >= 200 && this.status < 400) {
const response = options.converters['text json'](this.response);
options.success(headers, response, this.status, this.statusText);
} else {
options.error(headers, this.responseText, this.status, this.statusText, this.error);
}
options.complete(this.status);
}
};
xhr.open(options.method, options.url, true);
if (typeof options.headers === 'undefined') {
options.headers = {};
}
const headers = {
...options.headers,
'X-Requested-With': 'XMLHttpRequest',
};
Object.entries(headers).forEach(([key, value]) => xhr.setRequestHeader(key, value));
options.beforeSend(xhr);
xhr.send(options.body);
return xhr;
};
}

View File

@ -12,7 +12,6 @@ const apiDoubleHeaders = require('@hashicorp/api-double/lib/headers');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const express = require('express');
//
module.exports = {
name: 'startup',
@ -20,9 +19,6 @@ module.exports = {
// TODO: see if we can move these into the project specific `/server` directory
// instead of inside an addon
// Serve the coverage folder for easy viewing during development
server.app.use('/coverage', express.static('coverage'));
// TODO: This should all be moved out into ember-cli-api-double
// and we should figure out a way to get to the settings here for
// so we can set this path name centrally in config

View File

@ -52,7 +52,6 @@
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@ember/jquery": "^1.1.0",
"@ember/optional-features": "^1.3.0",
"@glimmer/component": "^1.0.0",
"@glimmer/tracking": "^1.0.0",

View File

@ -2,6 +2,7 @@
const fs = require('fs');
const promisify = require('util').promisify;
const read = promisify(fs.readFile);
const express = require('express');
module.exports = function(app, options) {
// During development the proxy server has no way of
@ -23,4 +24,6 @@ module.exports = function(app, options) {
}
next();
});
// Serve the coverage folder for easy viewing during development
app.use('/coverage', express.static('coverage'));
};

View File

@ -13,30 +13,28 @@ module('Integration | Adapter | kv', function(hooks) {
test(`requestForQuery returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:kv');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/kv/${id}?keys&dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/kv/${id}?keys&dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
id: id,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForQueryRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:kv');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/kv/${id}?dc=${dc}`;
let actual = adapter.requestForQueryRecord(client.url, {
const expected = `GET /v1/kv/${id}?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
dc: dc,
id: id,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForCreateRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:kv');

View File

@ -14,15 +14,14 @@ module('Integration | Adapter | oidc-provider', function(hooks) {
test('requestForQuery returns the correct url/method', function(assert) {
const adapter = this.owner.lookup('adapter:oidc-provider');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/internal/ui/oidc-auth-methods?dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/internal/ui/oidc-auth-methods?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test('requestForQueryRecord returns the correct url/method', function(assert) {
const adapter = this.owner.lookup('adapter:oidc-provider');

View File

@ -20,29 +20,27 @@ module('Integration | Adapter | policy', function(hooks) {
test(`requestForQuery returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:policy');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/acl/policies?dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/acl/policies?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForQueryRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:policy');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/acl/policy/${id}?dc=${dc}`;
let actual = adapter.requestForQueryRecord(client.url, {
const expected = `GET /v1/acl/policy/${id}?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
dc: dc,
id: id,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForCreateRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:policy');

View File

@ -13,29 +13,27 @@ module('Integration | Adapter | role', function(hooks) {
test(`requestForQuery returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:role');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/acl/roles?dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/acl/roles?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForQueryRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:role');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/acl/role/${id}?dc=${dc}`;
let actual = adapter.requestForQueryRecord(client.url, {
const expected = `GET /v1/acl/role/${id}?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
dc: dc,
id: id,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForCreateRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:role');

View File

@ -13,44 +13,41 @@ module('Integration | Adapter | service', function(hooks) {
test(`requestForQuery returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:service');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/internal/ui/services?dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/internal/ui/services?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForQuery returns the correct url/method when called with gateway when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:service');
const client = this.owner.lookup('service:client/http');
const gateway = 'gateway';
const expected = `GET /v1/internal/ui/gateway-services-nodes/${gateway}?dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/internal/ui/gateway-services-nodes/${gateway}?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
ns: nspace,
gateway: gateway,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForQueryRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:service');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/health/service/${id}?dc=${dc}`;
let actual = adapter.requestForQueryRecord(client.url, {
const expected = `GET /v1/health/service/${id}?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
dc: dc,
id: id,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
});
test("requestForQueryRecord throws if you don't specify an id", function(assert) {

View File

@ -14,30 +14,28 @@ module('Integration | Adapter | session', function(hooks) {
const adapter = this.owner.lookup('adapter:session');
const client = this.owner.lookup('service:client/http');
const node = 'node-id';
const expected = `GET /v1/session/node/${node}?dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/session/node/${node}?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
id: node,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForQueryRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:session');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/session/info/${id}?dc=${dc}`;
let actual = adapter.requestForQueryRecord(client.url, {
const expected = `GET /v1/session/info/${id}?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
dc: dc,
id: id,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForDeleteRecord returns the correct url/method when the nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:session');

View File

@ -13,57 +13,53 @@ module('Integration | Adapter | token', function(hooks) {
test(`requestForQuery returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:token');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/acl/tokens?dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/acl/tokens?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForQuery returns the correct url/method when a policy is specified when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:token');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/acl/tokens?policy=${id}&dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/acl/tokens?policy=${id}&dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
policy: id,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForQuery returns the correct url/method when a role is specified when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:token');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/acl/tokens?role=${id}&dc=${dc}`;
let actual = adapter.requestForQuery(client.url, {
const expected = `GET /v1/acl/tokens?role=${id}&dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc,
role: id,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForQueryRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:token');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/acl/token/${id}?dc=${dc}`;
let actual = adapter.requestForQueryRecord(client.url, {
const expected = `GET /v1/acl/token/${id}?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
let actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
dc: dc,
id: id,
ns: nspace,
});
actual = actual.split('\n');
assert.equal(actual.shift().trim(), expected);
actual = actual.join('\n').trim();
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test(`requestForCreateRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:token');

View File

@ -1,9 +1,36 @@
import Application from '../app';
import config from '../config/environment';
import { setApplication } from '@ember/test-helpers';
import { registerWaiter } from '@ember/test';
import './helpers/flash-message';
import start from 'ember-exam/test-support/start';
import ClientConnections from 'consul-ui/services/client/connections';
let activeRequests = 0;
registerWaiter(function() {
return activeRequests === 0;
});
ClientConnections.reopen({
addVisibilityChange: function() {
// for the moment don't listen for tab hiding during testing
// TODO: make this controllable from testing so we can fake a tab hide
},
purge: function() {
const res = this._super(...arguments);
activeRequests = 0;
return res;
},
acquire: function() {
activeRequests++;
return this._super(...arguments);
},
release: function() {
const res = this._super(...arguments);
activeRequests--;
return res;
},
});
const application = Application.create(config.APP);
application.inject('component:copy-button', 'clipboard', 'service:clipboard/local-storage');
setApplication(application);

View File

@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | client/connections', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let service = this.owner.lookup('service:client/connections');
assert.ok(service);
});
});

View File

@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | client/transports/xhr', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let service = this.owner.lookup('service:client/transports/xhr');
assert.ok(service);
});
});

View File

@ -1,98 +0,0 @@
import getObjectPool from 'consul-ui/utils/get-object-pool';
import { module, skip } from 'qunit';
import test from 'ember-sinon-qunit/test-support/test';
module('Unit | Utility | get object pool', function() {
skip('Decide what to do if you add 2 objects with the same id');
test('acquire adds objects', function(assert) {
const actual = [];
const expected = {
hi: 'there',
id: 'hi-there-123',
};
const expected2 = {
hi: 'there',
id: 'hi-there-456',
};
const pool = getObjectPool(function() {}, 10, actual);
pool.acquire(expected, expected.id);
assert.deepEqual(actual[0], expected);
pool.acquire(expected2, expected2.id);
assert.deepEqual(actual[1], expected2);
});
test('acquire adds objects and returns the id', function(assert) {
const arr = [];
const expected = 'hi-there-123';
const obj = {
hi: 'there',
id: expected,
};
const pool = getObjectPool(function() {}, 10, arr);
const actual = pool.acquire(obj, expected);
assert.equal(actual, expected);
});
test('acquire adds objects, and disposes when there is no room', function(assert) {
const actual = [];
const expected = {
hi: 'there',
id: 'hi-there-123',
};
const expected2 = {
hi: 'there',
id: 'hi-there-456',
};
const dispose = this.stub()
.withArgs(expected)
.returnsArg(0);
const pool = getObjectPool(dispose, 1, actual);
pool.acquire(expected, expected.id);
assert.deepEqual(actual[0], expected);
pool.acquire(expected2, expected2.id);
assert.deepEqual(actual[0], expected2);
assert.ok(dispose.calledOnce);
});
test('it disposes', function(assert) {
const arr = [];
const expected = {
hi: 'there',
id: 'hi-there-123',
};
const expected2 = {
hi: 'there',
id: 'hi-there-456',
};
const dispose = this.stub().returnsArg(0);
const pool = getObjectPool(dispose, 2, arr);
const id = pool.acquire(expected, expected.id);
assert.deepEqual(arr[0], expected);
pool.acquire(expected2, expected2.id);
assert.deepEqual(arr[1], expected2);
const actual = pool.dispose(id);
assert.ok(dispose.calledOnce);
assert.equal(arr.length, 1, 'object was removed from array');
assert.deepEqual(actual, expected, 'returned object is expected object');
assert.deepEqual(arr[0], expected2, 'object in the pool is expected object');
});
test('it purges', function(assert) {
const arr = [];
const expected = {
hi: 'there',
id: 'hi-there-123',
};
const expected2 = {
hi: 'there',
id: 'hi-there-456',
};
const dispose = this.stub().returnsArg(0);
const pool = getObjectPool(dispose, 2, arr);
pool.acquire(expected, expected.id);
assert.deepEqual(arr[0], expected);
pool.acquire(expected2, expected2.id);
assert.deepEqual(arr[1], expected2);
const actual = pool.purge();
assert.ok(dispose.calledTwice, 'dispose was called on everything');
assert.equal(arr.length, 0, 'the pool is empty');
assert.deepEqual(actual[0], expected, 'the first purged object is correct');
assert.deepEqual(actual[1], expected2, 'the second purged object is correct');
});
});

View File

@ -0,0 +1,18 @@
import createHeaders from 'consul-ui/utils/http/create-headers';
import { module, test } from 'qunit';
module('Unit | Utility | http/create-headers', function() {
const parseHeaders = createHeaders();
test('it converts lines of header-like strings into an object', function(assert) {
const expected = {
'Content-Type': 'application/json',
'X-Consul-Index': '1',
};
const lines = `
Content-Type: application/json
X-Consul-Index: 1
`.split('\n');
const actual = parseHeaders(lines);
assert.deepEqual(actual, expected);
});
});

View File

@ -0,0 +1,43 @@
import createQueryParams from 'consul-ui/utils/http/create-query-params';
import { module, test } from 'qunit';
module('Unit | Utility | http/create-query-params', function() {
const stringifyQueryParams = createQueryParams(str => str);
test('it turns objects into query params formatted strings', function(assert) {
const expected = 'something=here&another=variable';
const actual = stringifyQueryParams({
something: 'here',
another: 'variable',
});
assert.equal(actual, expected);
});
test('it ignores undefined properties', function(assert) {
const expected = 'something=here';
const actual = stringifyQueryParams({
something: 'here',
another: undefined,
});
assert.equal(actual, expected);
});
test('it stringifies nested objects', function(assert) {
const expected = 'something=here&another[something]=here&another[another][something]=here';
const actual = stringifyQueryParams({
something: 'here',
another: {
something: 'here',
another: {
something: 'here',
},
},
});
assert.equal(actual, expected);
});
test('it only adds the property if the value is null', function(assert) {
const expected = 'something&another=here';
const actual = stringifyQueryParams({
something: null,
another: 'here',
});
assert.equal(actual, expected);
});
});

View File

@ -1,37 +1,43 @@
import { module, skip } from 'qunit';
import test from 'ember-sinon-qunit/test-support/test';
import createURL from 'consul-ui/utils/createURL';
import createURL from 'consul-ui/utils/http/create-url';
import createQueryParams from 'consul-ui/utils/http/create-query-params';
module('Unit | Utils | createURL', function() {
module('Unit | Utils | http/create-url', function() {
skip("it isn't isolated enough, mock encodeURIComponent");
const url = createURL(encodeURIComponent, createQueryParams(encodeURIComponent));
test('it passes the values to encode', function(assert) {
const url = createURL(encodeURIComponent);
const actual = url`/v1/url?${{ query: 'to encode', 'key with': ' spaces ' }}`;
const expected = '/v1/url?query=to%20encode&key%20with=%20spaces%20';
assert.equal(actual, expected);
});
test('it adds a query string key without an `=` if the query value is `null`', function(assert) {
const url = createURL(encodeURIComponent);
const actual = url`/v1/url?${{ 'key with space': null }}`;
const expected = '/v1/url?key%20with%20space';
assert.equal(actual, expected);
});
test('it returns a string when passing an array', function(assert) {
const url = createURL(encodeURIComponent);
const actual = url`/v1/url/${['raw values', 'to', 'encode']}`;
const expected = '/v1/url/raw%20values/to/encode';
assert.equal(actual, expected);
});
test('it returns a string when passing a string', function(assert) {
const url = createURL(encodeURIComponent);
const actual = url`/v1/url/${'raw values to encode'}`;
const expected = '/v1/url/raw%20values%20to%20encode';
assert.equal(actual, expected);
});
test("it doesn't add a query string prop/value is the value is undefined", function(assert) {
const url = createURL(encodeURIComponent);
const actual = url`/v1/url?${{ key: undefined }}`;
const expected = '/v1/url?';
assert.equal(actual, expected);
});
test("it doesn't encode headers", function(assert) {
const actual = url`
/v1/url/${'raw values to encode'}
Header: %value
`;
const expected = `/v1/url/raw%20values%20to%20encode
Header: %value`;
assert.equal(actual, expected);
});
});

View File

@ -0,0 +1,10 @@
import HttpError from 'consul-ui/utils/http/error';
import { module, test } from 'qunit';
module('Unit | Utility | http/error', function() {
// Replace this with your real tests.
test('it works', function(assert) {
const result = new HttpError();
assert.ok(result);
});
});

View File

@ -0,0 +1,10 @@
import httpXhr from 'consul-ui/utils/http/xhr';
import { module, test } from 'qunit';
module('Unit | Utility | http/xhr', function() {
// Replace this with your real tests.
test('it works', function(assert) {
let result = httpXhr();
assert.ok(result);
});
});

View File

@ -1026,18 +1026,6 @@
resolved "https://registry.yarnpkg.com/@ember/edition-utils/-/edition-utils-1.2.0.tgz#a039f542dc14c8e8299c81cd5abba95e2459cfa6"
integrity sha512-VmVq/8saCaPdesQmftPqbFtxJWrzxNGSQ+e8x8LLe3Hjm36pJ04Q8LeORGZkAeOhldoUX9seLGmSaHeXkIqoog==
"@ember/jquery@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@ember/jquery/-/jquery-1.1.0.tgz#33d062610a5ceaa5c5c8a3187f870d47d6595940"
integrity sha512-zePT3LiK4/2bS4xafrbOlwoLJrDFseOZ95OOuVDyswv8RjFL+9lar+uxX6+jxRb0w900BcQSWP/4nuFSK6HXXw==
dependencies:
broccoli-funnel "^2.0.2"
broccoli-merge-trees "^3.0.2"
ember-cli-babel "^7.11.1"
ember-cli-version-checker "^3.1.3"
jquery "^3.4.1"
resolve "^1.11.1"
"@ember/optional-features@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@ember/optional-features/-/optional-features-1.3.0.tgz#d7da860417b85a56cec88419f30da5ee1dde2756"
@ -5100,7 +5088,7 @@ ember-cli-babel@^7.1.0, ember-cli-babel@^7.1.2, ember-cli-babel@^7.1.3, ember-cl
ensure-posix-path "^1.0.2"
semver "^5.5.0"
ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.8.0:
ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.8.0:
version "7.18.0"
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.18.0.tgz#e979b73eee00cd93f63452c6170d045e8832f29c"
integrity sha512-OLPfYD8wSfCrmGHcUf8zEfySSvbAL+5Qp2RWLycJIMaBZhg+SncKj5kVkL3cPJR5n2hVHPdfmKTQIYjOYl6FnQ==