From cb0c5309c97cb974765c0b655de44705eadc41c0 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Thu, 21 Feb 2019 10:36:15 +0000 Subject: [PATCH] UI: Add EventSource ready for implementing blocking queries (#5070) - Maintain http headers as JSON-API meta for all API requests (#4946) - Add EventSource ready for implementing blocking queries - EventSource project implementation to enable blocking queries for service and node listings (#5267) - Add setting to enable/disable blocking queries (#5352) --- ui-v2/.eslintignore | 1 + ui-v2/app/adapters/application.js | 3 + ui-v2/app/controllers/dc/nodes/index.js | 3 +- ui-v2/app/controllers/dc/services/index.js | 15 +- ui-v2/app/controllers/settings.js | 29 ++ .../app/instance-initializers/event-source.js | 61 +++ ui-v2/app/mixins/with-event-source.js | 16 + ui-v2/app/mixins/with-filtering.js | 2 +- ui-v2/app/mixins/with-health-filtering.js | 2 +- ui-v2/app/router.js | 6 +- ui-v2/app/routes/settings.js | 16 +- ui-v2/app/services/client/http.js | 3 + ui-v2/app/services/lazy-proxy.js | 27 + .../services/repository/type/event-source.js | 92 ++++ .../templates/components/hashicorp-consul.hbs | 2 - ui-v2/app/templates/settings.hbs | 27 +- ui-v2/app/utils/dom/event-source/blocking.js | 92 ++++ ui-v2/app/utils/dom/event-source/cache.js | 31 ++ ui-v2/app/utils/dom/event-source/callable.js | 70 +++ ui-v2/app/utils/dom/event-source/index.js | 36 ++ ui-v2/app/utils/dom/event-source/proxy.js | 50 ++ .../app/utils/dom/event-source/reopenable.js | 23 + ui-v2/app/utils/dom/event-source/resolver.js | 29 ++ ui-v2/app/utils/dom/event-source/storage.js | 40 ++ .../event-target/event-target-shim/LICENSE | 22 + .../event-target/event-target-shim/event.js | 470 ++++++++++++++++++ ui-v2/app/utils/dom/event-target/rsvp.js | 63 +++ ui-v2/app/utils/form/builder.js | 4 + .../tests/acceptance/dc/list-blocking.feature | 34 ++ .../tests/acceptance/page-navigation.feature | 4 +- .../steps/dc/list-blocking-steps.js | 10 + .../utils/dom/event-source/callable-test.js | 84 ++++ ui-v2/tests/steps.js | 38 +- ui-v2/tests/unit/controllers/settings-test.js | 12 + ui-v2/tests/unit/routes/settings-test.js | 1 + .../utils/dom/event-source/blocking-test.js | 87 ++++ .../unit/utils/dom/event-source/cache-test.js | 145 ++++++ .../utils/dom/event-source/callable-test.js | 65 +++ .../unit/utils/dom/event-source/index-test.js | 28 ++ .../unit/utils/dom/event-source/proxy-test.js | 10 + .../utils/dom/event-source/reopenable-test.js | 46 ++ .../utils/dom/event-source/resolver-test.js | 10 + .../utils/dom/event-source/storage-test.js | 10 + .../unit/utils/dom/event-target/rsvp-test.js | 13 + ui-v2/yarn.lock | 102 +++- 45 files changed, 1878 insertions(+), 56 deletions(-) create mode 100644 ui-v2/.eslintignore create mode 100644 ui-v2/app/controllers/settings.js create mode 100644 ui-v2/app/instance-initializers/event-source.js create mode 100644 ui-v2/app/mixins/with-event-source.js create mode 100644 ui-v2/app/services/lazy-proxy.js create mode 100644 ui-v2/app/services/repository/type/event-source.js create mode 100644 ui-v2/app/utils/dom/event-source/blocking.js create mode 100644 ui-v2/app/utils/dom/event-source/cache.js create mode 100644 ui-v2/app/utils/dom/event-source/callable.js create mode 100644 ui-v2/app/utils/dom/event-source/index.js create mode 100644 ui-v2/app/utils/dom/event-source/proxy.js create mode 100644 ui-v2/app/utils/dom/event-source/reopenable.js create mode 100644 ui-v2/app/utils/dom/event-source/resolver.js create mode 100644 ui-v2/app/utils/dom/event-source/storage.js create mode 100644 ui-v2/app/utils/dom/event-target/event-target-shim/LICENSE create mode 100644 ui-v2/app/utils/dom/event-target/event-target-shim/event.js create mode 100644 ui-v2/app/utils/dom/event-target/rsvp.js create mode 100644 ui-v2/tests/acceptance/dc/list-blocking.feature create mode 100644 ui-v2/tests/acceptance/steps/dc/list-blocking-steps.js create mode 100644 ui-v2/tests/integration/utils/dom/event-source/callable-test.js create mode 100644 ui-v2/tests/unit/controllers/settings-test.js create mode 100644 ui-v2/tests/unit/utils/dom/event-source/blocking-test.js create mode 100644 ui-v2/tests/unit/utils/dom/event-source/cache-test.js create mode 100644 ui-v2/tests/unit/utils/dom/event-source/callable-test.js create mode 100644 ui-v2/tests/unit/utils/dom/event-source/index-test.js create mode 100644 ui-v2/tests/unit/utils/dom/event-source/proxy-test.js create mode 100644 ui-v2/tests/unit/utils/dom/event-source/reopenable-test.js create mode 100644 ui-v2/tests/unit/utils/dom/event-source/resolver-test.js create mode 100644 ui-v2/tests/unit/utils/dom/event-source/storage-test.js create mode 100644 ui-v2/tests/unit/utils/dom/event-target/rsvp-test.js diff --git a/ui-v2/.eslintignore b/ui-v2/.eslintignore new file mode 100644 index 0000000000..06978bbca0 --- /dev/null +++ b/ui-v2/.eslintignore @@ -0,0 +1 @@ +app/utils/dom/event-target/event-target-shim/event.js diff --git a/ui-v2/app/adapters/application.js b/ui-v2/app/adapters/application.js index 97e86bd55c..d5f073972a 100644 --- a/ui-v2/app/adapters/application.js +++ b/ui-v2/app/adapters/application.js @@ -114,6 +114,9 @@ export default Adapter.extend({ if (typeof query.separator !== 'undefined') { delete query.separator; } + if (typeof query.index !== 'undefined') { + delete query.index; + } delete _query[DATACENTER_QUERY_PARAM]; return query; }, diff --git a/ui-v2/app/controllers/dc/nodes/index.js b/ui-v2/app/controllers/dc/nodes/index.js index cbbd7b90d9..8265570612 100644 --- a/ui-v2/app/controllers/dc/nodes/index.js +++ b/ui-v2/app/controllers/dc/nodes/index.js @@ -1,9 +1,10 @@ import Controller from '@ember/controller'; import { computed } from '@ember/object'; +import WithEventSource from 'consul-ui/mixins/with-event-source'; import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering'; import WithSearching from 'consul-ui/mixins/with-searching'; import { get } from '@ember/object'; -export default Controller.extend(WithSearching, WithHealthFiltering, { +export default Controller.extend(WithEventSource, WithSearching, WithHealthFiltering, { init: function() { this.searchParams = { healthyNode: 's', diff --git a/ui-v2/app/controllers/dc/services/index.js b/ui-v2/app/controllers/dc/services/index.js index 1a81a10c01..6da28085fc 100644 --- a/ui-v2/app/controllers/dc/services/index.js +++ b/ui-v2/app/controllers/dc/services/index.js @@ -1,6 +1,7 @@ import Controller from '@ember/controller'; import { get, computed } from '@ember/object'; import { htmlSafe } from '@ember/string'; +import WithEventSource from 'consul-ui/mixins/with-event-source'; import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering'; import WithSearching from 'consul-ui/mixins/with-searching'; const max = function(arr, prop) { @@ -25,7 +26,7 @@ const width = function(num) { const widthDeclaration = function(num) { return htmlSafe(`width: ${num}px`); }; -export default Controller.extend(WithSearching, WithHealthFiltering, { +export default Controller.extend(WithEventSource, WithSearching, WithHealthFiltering, { init: function() { this.searchParams = { service: 's', @@ -52,14 +53,14 @@ export default Controller.extend(WithSearching, WithHealthFiltering, { remainingWidth: computed('maxWidth', function() { return htmlSafe(`width: calc(50% - ${Math.round(get(this, 'maxWidth') / 2)}px)`); }), - maxPassing: computed('items', function() { - return max(get(this, 'items'), 'ChecksPassing'); + maxPassing: computed('filtered', function() { + return max(get(this, 'filtered'), 'ChecksPassing'); }), - maxWarning: computed('items', function() { - return max(get(this, 'items'), 'ChecksWarning'); + maxWarning: computed('filtered', function() { + return max(get(this, 'filtered'), 'ChecksWarning'); }), - maxCritical: computed('items', function() { - return max(get(this, 'items'), 'ChecksCritical'); + maxCritical: computed('filtered', function() { + return max(get(this, 'filtered'), 'ChecksCritical'); }), passingWidth: computed('maxPassing', function() { return widthDeclaration(width(get(this, 'maxPassing'))); diff --git a/ui-v2/app/controllers/settings.js b/ui-v2/app/controllers/settings.js new file mode 100644 index 0000000000..d70fc372fd --- /dev/null +++ b/ui-v2/app/controllers/settings.js @@ -0,0 +1,29 @@ +import Controller from '@ember/controller'; +import { get, set } from '@ember/object'; +import { inject as service } from '@ember/service'; + +export default Controller.extend({ + repo: service('settings'), + dom: service('dom'), + actions: { + change: function(e, value, item) { + const event = get(this, 'dom').normalizeEvent(e, value); + // TODO: Switch to using forms like the rest of the app + // setting utils/form/builder for things to be done before we + // can do that. For the moment just do things normally its a simple + // enough form at the moment + + const target = event.target; + const blocking = get(this, 'item.client.blocking'); + switch (target.name) { + case 'client[blocking]': + if (typeof blocking === 'undefined') { + set(this, 'item.client', {}); + } + set(this, 'item.client.blocking', !blocking); + this.send('update', get(this, 'item')); + break; + } + }, + }, +}); diff --git a/ui-v2/app/instance-initializers/event-source.js b/ui-v2/app/instance-initializers/event-source.js new file mode 100644 index 0000000000..705accdd5b --- /dev/null +++ b/ui-v2/app/instance-initializers/event-source.js @@ -0,0 +1,61 @@ +import config from '../config/environment'; + +const enabled = 'CONSUL_UI_DISABLE_REALTIME'; +export function initialize(container) { + if (config[enabled] || window.localStorage.getItem(enabled) !== null) { + return; + } + ['node', 'service'] + .map(function(item) { + // create repositories that return a promise resolving to an EventSource + return { + service: `repository/${item}/event-source`, + extend: 'repository/type/event-source', + // Inject our original respository that is used by this class + // within the callable of the EventSource + services: { + content: `repository/${item}`, + }, + }; + }) + .concat([ + // These are the routes where we overwrite the 'default' + // repo service. Default repos are repos that return a promise resovlving to + // an ember-data record or recordset + { + route: 'dc/nodes/index', + services: { + repo: 'repository/node/event-source', + }, + }, + { + route: 'dc/services/index', + services: { + repo: 'repository/service/event-source', + }, + }, + ]) + .forEach(function(definition) { + if (typeof definition.extend !== 'undefined') { + // Create the class instances that we need + container.register( + `service:${definition.service}`, + container.resolveRegistration(`service:${definition.extend}`).extend({}) + ); + } + Object.keys(definition.services).forEach(function(name) { + const servicePath = definition.services[name]; + // inject its dependencies, this could probably detect the type + // but hardcode this for the moment + if (typeof definition.route !== 'undefined') { + container.inject(`route:${definition.route}`, name, `service:${servicePath}`); + } else { + container.inject(`service:${definition.service}`, name, `service:${servicePath}`); + } + }); + }); +} + +export default { + initialize, +}; diff --git a/ui-v2/app/mixins/with-event-source.js b/ui-v2/app/mixins/with-event-source.js new file mode 100644 index 0000000000..1fccd1dc83 --- /dev/null +++ b/ui-v2/app/mixins/with-event-source.js @@ -0,0 +1,16 @@ +import Mixin from '@ember/object/mixin'; + +export default Mixin.create({ + reset: function(exiting) { + if (exiting) { + Object.keys(this).forEach(prop => { + if (this[prop] && typeof this[prop].close === 'function') { + this[prop].close(); + // ember doesn't delete on 'resetController' by default + delete this[prop]; + } + }); + } + return this._super(...arguments); + }, +}); diff --git a/ui-v2/app/mixins/with-filtering.js b/ui-v2/app/mixins/with-filtering.js index 42dc8c888d..26d08940d9 100644 --- a/ui-v2/app/mixins/with-filtering.js +++ b/ui-v2/app/mixins/with-filtering.js @@ -15,7 +15,7 @@ const toKeyValue = function(el) { }; export default Mixin.create({ filters: {}, - filtered: computed('items', 'filters', function() { + filtered: computed('items.[]', 'filters', function() { const filters = get(this, 'filters'); return get(this, 'items').filter(item => { return this.filter(item, filters); diff --git a/ui-v2/app/mixins/with-health-filtering.js b/ui-v2/app/mixins/with-health-filtering.js index e18bab7d21..06ad378261 100644 --- a/ui-v2/app/mixins/with-health-filtering.js +++ b/ui-v2/app/mixins/with-health-filtering.js @@ -29,7 +29,7 @@ export default Mixin.create(WithFiltering, { as: 'filter', }, }, - healthFilters: computed('items', function() { + healthFilters: computed('items.[]', function() { const items = get(this, 'items'); const objs = ['', 'passing', 'warning', 'critical'].map(function(item) { const count = countStatus(items, item); diff --git a/ui-v2/app/router.js b/ui-v2/app/router.js index 0a7a3bef82..628a713c7a 100644 --- a/ui-v2/app/router.js +++ b/ui-v2/app/router.js @@ -88,9 +88,9 @@ export const routes = { _options: { path: '/' }, }, // The settings page is global. - // settings: { - // _options: { path: '/setting' }, - // }, + settings: { + _options: { path: '/setting' }, + }, notfound: { _options: { path: '/*path' }, }, diff --git a/ui-v2/app/routes/settings.js b/ui-v2/app/routes/settings.js index c5f9701b61..0dc59dc02c 100644 --- a/ui-v2/app/routes/settings.js +++ b/ui-v2/app/routes/settings.js @@ -3,8 +3,8 @@ import { inject as service } from '@ember/service'; import { hash } from 'rsvp'; import { get } from '@ember/object'; -import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; -export default Route.extend(WithBlockingActions, { +export default Route.extend({ + client: service('client/http'), repo: service('settings'), dcRepo: service('repository/dc'), model: function(params) { @@ -24,8 +24,12 @@ export default Route.extend(WithBlockingActions, { this._super(...arguments); controller.setProperties(model); }, - // overwrite afterUpdate and afterDelete hooks - // to avoid the default 'return to listing page' - afterUpdate: function() {}, - afterDelete: function() {}, + actions: { + update: function(item) { + if (!get(item, 'client.blocking')) { + get(this, 'client').abort(); + } + get(this, 'repo').persist(item); + }, + }, }); diff --git a/ui-v2/app/services/client/http.js b/ui-v2/app/services/client/http.js index 1290d2be72..fa452d5009 100644 --- a/ui-v2/app/services/client/http.js +++ b/ui-v2/app/services/client/http.js @@ -63,6 +63,9 @@ export default Service.extend({ }); } }, + abort: function(id = null) { + get(this, 'connections').purge(); + }, whenAvailable: function(e) { const doc = get(this, 'dom').document(); // if we are using a connection limited protocol and the user has hidden the tab (hidden browser/tab switch) diff --git a/ui-v2/app/services/lazy-proxy.js b/ui-v2/app/services/lazy-proxy.js new file mode 100644 index 0000000000..18b8a86560 --- /dev/null +++ b/ui-v2/app/services/lazy-proxy.js @@ -0,0 +1,27 @@ +import Service from '@ember/service'; +import { get } from '@ember/object'; + +export default Service.extend({ + shouldProxy: function(content, method) { + return false; + }, + init: function() { + this._super(...arguments); + const content = get(this, 'content'); + for (let prop in content) { + if (typeof content[prop] === 'function') { + if (this.shouldProxy(content, prop)) { + this[prop] = function() { + return this.execute(content, prop).then(method => { + return method.apply(this, arguments); + }); + }; + } else if (typeof this[prop] !== 'function') { + this[prop] = function() { + return content[prop](...arguments); + }; + } + } + } + }, +}); diff --git a/ui-v2/app/services/repository/type/event-source.js b/ui-v2/app/services/repository/type/event-source.js new file mode 100644 index 0000000000..2305756062 --- /dev/null +++ b/ui-v2/app/services/repository/type/event-source.js @@ -0,0 +1,92 @@ +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; + +import LazyProxyService from 'consul-ui/services/lazy-proxy'; + +import { cache as createCache, BlockingEventSource } from 'consul-ui/utils/dom/event-source'; + +const createProxy = function(repo, find, settings, cache, serialize = JSON.stringify) { + // proxied find*..(id, dc) + const throttle = get(this, 'wait').execute; + const client = get(this, 'client'); + return function() { + const key = `${repo.getModelName()}.${find}.${serialize([...arguments])}`; + const _args = arguments; + const newPromisedEventSource = cache; + return newPromisedEventSource( + function(configuration) { + // take a copy of the original arguments + // this means we don't have any configuration object on it + let args = [..._args]; + if (configuration.settings.enabled) { + // ...and only add our current cursor/configuration if we are blocking + args = args.concat([configuration]); + } + // save a callback so we can conditionally throttle + const cb = () => { + // original find... with configuration now added + return repo[find](...args) + .then(res => { + if (!configuration.settings.enabled) { + // blocking isn't enabled, immediately close + this.close(); + } + return res; + }) + .catch(function(e) { + // setup the aborted connection restarting + // this should happen here to avoid cache deletion + const status = get(e, 'errors.firstObject.status'); + if (status === '0') { + // Any '0' errors (abort) should possibly try again, depending upon the circumstances + // whenAvailable returns a Promise that resolves when the client is available + // again + return client.whenAvailable(e); + } + throw e; + }); + }; + // if we have a cursor (which means its at least the second call) + // and we have a throttle setting, wait for so many ms + if (typeof configuration.cursor !== 'undefined' && configuration.settings.throttle) { + return throttle(configuration.settings.throttle).then(cb); + } + return cb(); + }, + { + key: key, + type: BlockingEventSource, + settings: { + enabled: settings.blocking, + throttle: settings.throttle, + }, + } + ); + }; +}; +let cache = null; +export default LazyProxyService.extend({ + store: service('store'), + settings: service('settings'), + wait: service('timeout'), + client: service('client/http'), + init: function() { + this._super(...arguments); + if (cache === null) { + cache = createCache({}); + } + }, + willDestroy: function() { + cache = null; + }, + shouldProxy: function(content, method) { + return method.indexOf('find') === 0; + }, + execute: function(repo, find) { + return get(this, 'settings') + .findBySlug('client') + .then(settings => { + return createProxy.bind(this)(repo, find, settings, cache); + }); + }, +}); diff --git a/ui-v2/app/templates/components/hashicorp-consul.hbs b/ui-v2/app/templates/components/hashicorp-consul.hbs index 916d69343c..32161b51e6 100644 --- a/ui-v2/app/templates/components/hashicorp-consul.hbs +++ b/ui-v2/app/templates/components/hashicorp-consul.hbs @@ -44,11 +44,9 @@
  • Documentation
  • -{{#if false }}
  • Settings
  • -{{/if}} diff --git a/ui-v2/app/templates/settings.hbs b/ui-v2/app/templates/settings.hbs index 97390b6a6f..f38dfe069a 100644 --- a/ui-v2/app/templates/settings.hbs +++ b/ui-v2/app/templates/settings.hbs @@ -1,20 +1,5 @@ {{#hashicorp-consul id="wrapper" dcs=dcs dc=dc}} {{#app-view class="settings show"}} - {{#block-slot 'notification' as |status type|}} - {{#if (eq type 'update')}} - {{#if (eq status 'success') }} - Your settings were saved. - {{else}} - There was an error saving your settings. - {{/if}} - {{ else if (eq type 'delete')}} - {{#if (eq status 'success') }} - You settings have been reset. - {{else}} - There was an error resetting your settings. - {{/if}} - {{/if}} - {{/block-slot}} {{#block-slot 'header'}}

    Settings @@ -26,13 +11,13 @@

    - +
    + +
    -
    {{/block-slot}} {{/app-view}} diff --git a/ui-v2/app/utils/dom/event-source/blocking.js b/ui-v2/app/utils/dom/event-source/blocking.js new file mode 100644 index 0000000000..d5f8968f61 --- /dev/null +++ b/ui-v2/app/utils/dom/event-source/blocking.js @@ -0,0 +1,92 @@ +import { get } from '@ember/object'; +import { Promise } from 'rsvp'; + +// native EventSource retry is ~3s wait +export const create5xxBackoff = function(ms = 3000, P = Promise, wait = setTimeout) { + // This expects an ember-data like error + return function(err) { + const status = get(err, 'errors.firstObject.status'); + if (typeof status !== 'undefined') { + switch (true) { + // Any '5xx' (not 500) errors should back off and try again + case status.indexOf('5') === 0 && status.length === 3 && status !== '500': + return new P(function(resolve) { + wait(function() { + resolve(err); + }, ms); + }); + } + } + // any other errors should throw to be picked up by an error listener/catch + throw err; + }; +}; +const defaultCreateEvent = function(result, configuration) { + return { + type: 'message', + data: result, + }; +}; +/** + * Wraps an EventSource with functionality to add native EventSource-like functionality + * + * @param {Class} [CallableEventSource] - CallableEventSource Class + * @param {Function} [backoff] - Default backoff function for all instances, defaults to create5xxBackoff + */ +export default function(EventSource, backoff = create5xxBackoff()) { + /** + * An EventSource implementation to add native EventSource-like functionality with just callbacks (`cursor` and 5xx backoff) + * + * This includes: + * 1. 5xx backoff support (uses a 3 second reconnect like native implementations). You can add to this via `Promise.catch` + * 2. A `cursor` configuration value. Current `cursor` is taken from the `meta` property of the event (i.e. `event.data.meta.cursor`) + * 3. Event data can be customized by adding a `configuration.createEvent` + * + * @param {Function} [source] - Promise returning function that resolves your data + * @param {Object} [configuration] - Plain configuration object: + * `cursor` - Cursor position of the EventSource + * `createEvent` - A data filter, giving you the opportunity to filter or replace the event data, such as removing/replacing records + */ + return class extends EventSource { + constructor(source, configuration = {}) { + super(configuration => { + const { createEvent, ...superConfiguration } = configuration; + return source + .apply(this, [superConfiguration]) + .catch(backoff) + .then(result => { + if (!(result instanceof Error)) { + const _createEvent = + typeof createEvent === 'function' ? createEvent : defaultCreateEvent; + let event = _createEvent(result, configuration); + // allow custom types, but make a default of `message`, ideally this would check for CustomEvent + // but keep this flexible for the moment + if (!event.type) { + event = { + type: 'message', + data: event, + }; + } + // meta is also configurable by using createEvent + const meta = get(event.data || {}, 'meta'); + if (meta) { + // pick off the `cursor` from the meta and add it to configuration + configuration.cursor = meta.cursor; + } + this.currentEvent = event; + this.dispatchEvent(this.currentEvent); + this.previousEvent = this.currentEvent; + } + return result; + }); + }, configuration); + } + // if we are having these props, at least make getters + getCurrentEvent() { + return this.currentEvent; + } + getPreviousEvent() { + return this.previousEvent; + } + }; +} diff --git a/ui-v2/app/utils/dom/event-source/cache.js b/ui-v2/app/utils/dom/event-source/cache.js new file mode 100644 index 0000000000..4e7d9dee58 --- /dev/null +++ b/ui-v2/app/utils/dom/event-source/cache.js @@ -0,0 +1,31 @@ +export default function(source, DefaultEventSource, P = Promise) { + return function(sources) { + return function(cb, configuration) { + const key = configuration.key; + if (typeof sources[key] !== 'undefined' && configuration.settings.enabled) { + if (typeof sources[key].configuration === 'undefined') { + sources[key].configuration = {}; + } + sources[key].configuration.settings = configuration.settings; + return source(sources[key]); + } else { + const EventSource = configuration.type || DefaultEventSource; + const eventSource = (sources[key] = new EventSource(cb, configuration)); + return source(eventSource) + .catch(function(e) { + // any errors, delete from the cache for next time + delete sources[key]; + return P.reject(e); + }) + .then(function(eventSource) { + // make sure we cancel everything out if there is no cursor + if (typeof eventSource.configuration.cursor === 'undefined') { + eventSource.close(); + delete sources[key]; + } + return eventSource; + }); + } + }; + }; +} diff --git a/ui-v2/app/utils/dom/event-source/callable.js b/ui-v2/app/utils/dom/event-source/callable.js new file mode 100644 index 0000000000..cc266940ae --- /dev/null +++ b/ui-v2/app/utils/dom/event-source/callable.js @@ -0,0 +1,70 @@ +export const defaultRunner = function(target, configuration, isClosed) { + if (isClosed(target)) { + return; + } + // TODO Consider wrapping this is a promise for none thenable returns + return target.source + .bind(target)(configuration) + .then(function(res) { + return defaultRunner(target, configuration, isClosed); + }); +}; +const errorEvent = function(e) { + return new ErrorEvent('error', { + error: e, + message: e.message, + }); +}; +const isClosed = function(target) { + switch (target.readyState) { + case 2: // CLOSED + case 3: // CLOSING + return true; + } + return false; +}; +export default function( + EventTarget, + P = Promise, + run = defaultRunner, + createErrorEvent = errorEvent +) { + return class extends EventTarget { + constructor(source, configuration = {}) { + super(); + this.readyState = 2; + this.source = + typeof source !== 'function' + ? function(configuration) { + this.close(); + return P.resolve(); + } + : source; + this.readyState = 0; // connecting + P.resolve() + .then(() => { + this.readyState = 1; // open + // ...that the connection _was just_ opened + this.dispatchEvent({ type: 'open' }); + return run(this, configuration, isClosed); + }) + .catch(e => { + this.dispatchEvent(createErrorEvent(e)); + // close after the dispatch so we can tell if it was an error whilst closed or not + // but make sure its before the promise tick + this.readyState = 2; // CLOSE + }) + .then(() => { + // This only gets called when the promise chain completely finishes + // so only when its completely closed. + this.readyState = 2; // CLOSE + }); + } + close() { + // additional readyState 3 = CLOSING + if (this.readyState !== 2) { + this.readyState = 3; + } + } + }; +} diff --git a/ui-v2/app/utils/dom/event-source/index.js b/ui-v2/app/utils/dom/event-source/index.js new file mode 100644 index 0000000000..6c8de125f8 --- /dev/null +++ b/ui-v2/app/utils/dom/event-source/index.js @@ -0,0 +1,36 @@ +import ObjectProxy from '@ember/object/proxy'; +import ArrayProxy from '@ember/array/proxy'; +import { Promise } from 'rsvp'; + +import createListeners from 'consul-ui/utils/dom/create-listeners'; + +import EventTarget from 'consul-ui/utils/dom/event-target/rsvp'; + +import cacheFactory from 'consul-ui/utils/dom/event-source/cache'; +import proxyFactory from 'consul-ui/utils/dom/event-source/proxy'; +import firstResolverFactory from 'consul-ui/utils/dom/event-source/resolver'; + +import CallableEventSourceFactory from 'consul-ui/utils/dom/event-source/callable'; +import ReopenableEventSourceFactory from 'consul-ui/utils/dom/event-source/reopenable'; +import BlockingEventSourceFactory from 'consul-ui/utils/dom/event-source/blocking'; +import StorageEventSourceFactory from 'consul-ui/utils/dom/event-source/storage'; + +// All The EventSource-i +export const CallableEventSource = CallableEventSourceFactory(EventTarget, Promise); +export const ReopenableEventSource = ReopenableEventSourceFactory(CallableEventSource); +export const BlockingEventSource = BlockingEventSourceFactory(ReopenableEventSource); +export const StorageEventSource = StorageEventSourceFactory(EventTarget, Promise); + +// various utils +export const proxy = proxyFactory(ObjectProxy, ArrayProxy); +export const resolve = firstResolverFactory(Promise); + +export const source = function(source) { + // create API needed for conventional promise blocked, loading, Routes + // i.e. resolve/reject on first response + return resolve(source, createListeners()).then(function(data) { + // create API needed for conventional DD/computed and Controllers + return proxy(data, source, createListeners()); + }); +}; +export const cache = cacheFactory(source, BlockingEventSource, Promise); diff --git a/ui-v2/app/utils/dom/event-source/proxy.js b/ui-v2/app/utils/dom/event-source/proxy.js new file mode 100644 index 0000000000..c36de9aac1 --- /dev/null +++ b/ui-v2/app/utils/dom/event-source/proxy.js @@ -0,0 +1,50 @@ +import { get, set } from '@ember/object'; + +export default function(ObjProxy, ArrProxy) { + return function(data, source, listeners) { + let Proxy = ObjProxy; + if (typeof data !== 'string' && typeof get(data, 'length') !== 'undefined') { + data = data.filter(function(item) { + return !get(item, 'isDestroyed') && !get(item, 'isDeleted') && get(item, 'isLoaded'); + }); + } + if (typeof data !== 'string' && typeof get(data, 'length') !== 'undefined') { + Proxy = ArrProxy; + } + const proxy = Proxy.create({ + content: data, + init: function() { + this.listeners = listeners; + this.listeners.add(source, 'message', e => { + set(this, 'content', e.data); + }); + }, + configuration: source.configuration, + addEventListener: function(type, handler) { + // Force use of computed for messages + if (type !== 'message') { + this.listeners.add(source, type, handler); + } + }, + getCurrentEvent: function() { + return source.getCurrentEvent(...arguments); + }, + removeEventListener: function() { + return source.removeEventListener(...arguments); + }, + dispatchEvent: function() { + return source.dispatchEvent(...arguments); + }, + close: function() { + return source.close(...arguments); + }, + reopen: function() { + return source.reopen(...arguments); + }, + willDestroy: function() { + this.listeners.remove(); + }, + }); + return proxy; + }; +} diff --git a/ui-v2/app/utils/dom/event-source/reopenable.js b/ui-v2/app/utils/dom/event-source/reopenable.js new file mode 100644 index 0000000000..c1f362c5de --- /dev/null +++ b/ui-v2/app/utils/dom/event-source/reopenable.js @@ -0,0 +1,23 @@ +/** + * Wraps an EventSource so that you can `close` and `reopen` + * + * @param {Class} eventSource - EventSource class to extend from + */ +export default function(eventSource = EventSource) { + return class extends eventSource { + constructor(source, configuration) { + super(...arguments); + this.configuration = configuration; + } + reopen() { + switch (this.readyState) { + case 3: // CLOSING + this.readyState = 1; + break; + case 2: // CLOSED + eventSource.apply(this, [this.source, this.configuration]); + break; + } + } + }; +} diff --git a/ui-v2/app/utils/dom/event-source/resolver.js b/ui-v2/app/utils/dom/event-source/resolver.js new file mode 100644 index 0000000000..a5984295aa --- /dev/null +++ b/ui-v2/app/utils/dom/event-source/resolver.js @@ -0,0 +1,29 @@ +export default function(P = Promise) { + return function(source, listeners) { + let current; + if (typeof source.getCurrentEvent === 'function') { + current = source.getCurrentEvent(); + } + if (current != null) { + // immediately resolve if we have previous cached data + return P.resolve(current.data).then(function(cached) { + source.reopen(); + return cached; + }); + } + // if we have no previously cached data, listen for the first response + return new P(function(resolve, reject) { + // close, cleanup and reject if we get an error + listeners.add(source, 'error', function(e) { + listeners.remove(); + e.target.close(); + reject(e.error); + }); + // ...or cleanup and respond with the first lot of data + listeners.add(source, 'message', function(e) { + listeners.remove(); + resolve(e.data); + }); + }); + }; +} diff --git a/ui-v2/app/utils/dom/event-source/storage.js b/ui-v2/app/utils/dom/event-source/storage.js new file mode 100644 index 0000000000..b5cba3b618 --- /dev/null +++ b/ui-v2/app/utils/dom/event-source/storage.js @@ -0,0 +1,40 @@ +export default function(EventTarget, P = Promise) { + const handler = function(e) { + if (e.key === this.configuration.key) { + P.resolve(this.getCurrentEvent()).then(event => { + this.configuration.cursor++; + this.dispatchEvent(event); + }); + } + }; + return class extends EventTarget { + constructor(cb, configuration) { + super(...arguments); + this.source = cb; + this.handler = handler.bind(this); + this.configuration = configuration; + this.configuration.cursor = 1; + this.dispatcher = configuration.dispatcher; + this.reopen(); + } + dispatchEvent() { + if (this.readyState === 1) { + return super.dispatchEvent(...arguments); + } + } + close() { + this.dispatcher.removeEventListener('storage', this.handler); + this.readyState = 2; + } + reopen() { + this.dispatcher.addEventListener('storage', this.handler); + this.readyState = 1; + } + getCurrentEvent() { + return { + type: 'message', + data: this.source(this.configuration), + }; + } + }; +} diff --git a/ui-v2/app/utils/dom/event-target/event-target-shim/LICENSE b/ui-v2/app/utils/dom/event-target/event-target-shim/LICENSE new file mode 100644 index 0000000000..c39e6949ed --- /dev/null +++ b/ui-v2/app/utils/dom/event-target/event-target-shim/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Toru Nagashima + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/ui-v2/app/utils/dom/event-target/event-target-shim/event.js b/ui-v2/app/utils/dom/event-target/event-target-shim/event.js new file mode 100644 index 0000000000..13a46fd5a1 --- /dev/null +++ b/ui-v2/app/utils/dom/event-target/event-target-shim/event.js @@ -0,0 +1,470 @@ +/** + * @typedef {object} PrivateData + * @property {EventTarget} eventTarget The event target. + * @property {{type:string}} event The original event object. + * @property {number} eventPhase The current event phase. + * @property {EventTarget|null} currentTarget The current event target. + * @property {boolean} canceled The flag to prevent default. + * @property {boolean} stopped The flag to stop propagation. + * @property {boolean} immediateStopped The flag to stop propagation immediately. + * @property {Function|null} passiveListener The listener if the current listener is passive. Otherwise this is null. + * @property {number} timeStamp The unix time. + * @private + */ + +/** + * Private data for event wrappers. + * @type {WeakMap} + * @private + */ +const privateData = new WeakMap(); + +/** + * Cache for wrapper classes. + * @type {WeakMap} + * @private + */ +const wrappers = new WeakMap(); + +/** + * Get private data. + * @param {Event} event The event object to get private data. + * @returns {PrivateData} The private data of the event. + * @private + */ +function pd(event) { + const retv = privateData.get(event); + console.assert(retv != null, "'this' is expected an Event object, but got", event); + return retv; +} + +/** + * https://dom.spec.whatwg.org/#set-the-canceled-flag + * @param data {PrivateData} private data. + */ +function setCancelFlag(data) { + if (data.passiveListener != null) { + if (typeof console !== 'undefined' && typeof console.error === 'function') { + console.error( + 'Unable to preventDefault inside passive event listener invocation.', + data.passiveListener + ); + } + return; + } + if (!data.event.cancelable) { + return; + } + + data.canceled = true; + if (typeof data.event.preventDefault === 'function') { + data.event.preventDefault(); + } +} + +/** + * @see https://dom.spec.whatwg.org/#interface-event + * @private + */ +/** + * The event wrapper. + * @constructor + * @param {EventTarget} eventTarget The event target of this dispatching. + * @param {Event|{type:string}} event The original event to wrap. + */ +function Event(eventTarget, event) { + privateData.set(this, { + eventTarget, + event, + eventPhase: 2, + currentTarget: eventTarget, + canceled: false, + stopped: false, + immediateStopped: false, + passiveListener: null, + timeStamp: event.timeStamp || Date.now(), + }); + + // https://heycam.github.io/webidl/#Unforgeable + Object.defineProperty(this, 'isTrusted', { value: false, enumerable: true }); + + // Define accessors + const keys = Object.keys(event); + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (!(key in this)) { + Object.defineProperty(this, key, defineRedirectDescriptor(key)); + } + } +} + +// Should be enumerable, but class methods are not enumerable. +Event.prototype = { + /** + * The type of this event. + * @type {string} + */ + get type() { + return pd(this).event.type; + }, + + /** + * The target of this event. + * @type {EventTarget} + */ + get target() { + return pd(this).eventTarget; + }, + + /** + * The target of this event. + * @type {EventTarget} + */ + get currentTarget() { + return pd(this).currentTarget; + }, + + /** + * @returns {EventTarget[]} The composed path of this event. + */ + composedPath() { + const currentTarget = pd(this).currentTarget; + if (currentTarget == null) { + return []; + } + return [currentTarget]; + }, + + /** + * Constant of NONE. + * @type {number} + */ + get NONE() { + return 0; + }, + + /** + * Constant of CAPTURING_PHASE. + * @type {number} + */ + get CAPTURING_PHASE() { + return 1; + }, + + /** + * Constant of AT_TARGET. + * @type {number} + */ + get AT_TARGET() { + return 2; + }, + + /** + * Constant of BUBBLING_PHASE. + * @type {number} + */ + get BUBBLING_PHASE() { + return 3; + }, + + /** + * The target of this event. + * @type {number} + */ + get eventPhase() { + return pd(this).eventPhase; + }, + + /** + * Stop event bubbling. + * @returns {void} + */ + stopPropagation() { + const data = pd(this); + + data.stopped = true; + if (typeof data.event.stopPropagation === 'function') { + data.event.stopPropagation(); + } + }, + + /** + * Stop event bubbling. + * @returns {void} + */ + stopImmediatePropagation() { + const data = pd(this); + + data.stopped = true; + data.immediateStopped = true; + if (typeof data.event.stopImmediatePropagation === 'function') { + data.event.stopImmediatePropagation(); + } + }, + + /** + * The flag to be bubbling. + * @type {boolean} + */ + get bubbles() { + return Boolean(pd(this).event.bubbles); + }, + + /** + * The flag to be cancelable. + * @type {boolean} + */ + get cancelable() { + return Boolean(pd(this).event.cancelable); + }, + + /** + * Cancel this event. + * @returns {void} + */ + preventDefault() { + setCancelFlag(pd(this)); + }, + + /** + * The flag to indicate cancellation state. + * @type {boolean} + */ + get defaultPrevented() { + return pd(this).canceled; + }, + + /** + * The flag to be composed. + * @type {boolean} + */ + get composed() { + return Boolean(pd(this).event.composed); + }, + + /** + * The unix time of this event. + * @type {number} + */ + get timeStamp() { + return pd(this).timeStamp; + }, + + /** + * The target of this event. + * @type {EventTarget} + * @deprecated + */ + get srcElement() { + return pd(this).eventTarget; + }, + + /** + * The flag to stop event bubbling. + * @type {boolean} + * @deprecated + */ + get cancelBubble() { + return pd(this).stopped; + }, + set cancelBubble(value) { + if (!value) { + return; + } + const data = pd(this); + + data.stopped = true; + if (typeof data.event.cancelBubble === 'boolean') { + data.event.cancelBubble = true; + } + }, + + /** + * The flag to indicate cancellation state. + * @type {boolean} + * @deprecated + */ + get returnValue() { + return !pd(this).canceled; + }, + set returnValue(value) { + if (!value) { + setCancelFlag(pd(this)); + } + }, + + /** + * Initialize this event object. But do nothing under event dispatching. + * @param {string} type The event type. + * @param {boolean} [bubbles=false] The flag to be possible to bubble up. + * @param {boolean} [cancelable=false] The flag to be possible to cancel. + * @deprecated + */ + initEvent() { + // Do nothing. + }, +}; + +// `constructor` is not enumerable. +Object.defineProperty(Event.prototype, 'constructor', { + value: Event, + configurable: true, + writable: true, +}); + +// Ensure `event instanceof window.Event` is `true`. +if (typeof window !== 'undefined' && typeof window.Event !== 'undefined') { + Object.setPrototypeOf(Event.prototype, window.Event.prototype); + + // Make association for wrappers. + wrappers.set(window.Event.prototype, Event); +} + +/** + * Get the property descriptor to redirect a given property. + * @param {string} key Property name to define property descriptor. + * @returns {PropertyDescriptor} The property descriptor to redirect the property. + * @private + */ +function defineRedirectDescriptor(key) { + return { + get() { + return pd(this).event[key]; + }, + set(value) { + pd(this).event[key] = value; + }, + configurable: true, + enumerable: true, + }; +} + +/** + * Get the property descriptor to call a given method property. + * @param {string} key Property name to define property descriptor. + * @returns {PropertyDescriptor} The property descriptor to call the method property. + * @private + */ +function defineCallDescriptor(key) { + return { + value() { + const event = pd(this).event; + return event[key].apply(event, arguments); + }, + configurable: true, + enumerable: true, + }; +} + +/** + * Define new wrapper class. + * @param {Function} BaseEvent The base wrapper class. + * @param {Object} proto The prototype of the original event. + * @returns {Function} The defined wrapper class. + * @private + */ +function defineWrapper(BaseEvent, proto) { + const keys = Object.keys(proto); + if (keys.length === 0) { + return BaseEvent; + } + + /** CustomEvent */ + function CustomEvent(eventTarget, event) { + BaseEvent.call(this, eventTarget, event); + } + + CustomEvent.prototype = Object.create(BaseEvent.prototype, { + constructor: { value: CustomEvent, configurable: true, writable: true }, + }); + + // Define accessors. + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (!(key in BaseEvent.prototype)) { + const descriptor = Object.getOwnPropertyDescriptor(proto, key); + const isFunc = typeof descriptor.value === 'function'; + Object.defineProperty( + CustomEvent.prototype, + key, + isFunc ? defineCallDescriptor(key) : defineRedirectDescriptor(key) + ); + } + } + + return CustomEvent; +} + +/** + * Get the wrapper class of a given prototype. + * @param {Object} proto The prototype of the original event to get its wrapper. + * @returns {Function} The wrapper class. + * @private + */ +function getWrapper(proto) { + if (proto == null || proto === Object.prototype) { + return Event; + } + + let wrapper = wrappers.get(proto); + if (wrapper == null) { + wrapper = defineWrapper(getWrapper(Object.getPrototypeOf(proto)), proto); + wrappers.set(proto, wrapper); + } + return wrapper; +} + +/** + * Wrap a given event to management a dispatching. + * @param {EventTarget} eventTarget The event target of this dispatching. + * @param {Object} event The event to wrap. + * @returns {Event} The wrapper instance. + * @private + */ +export function wrapEvent(eventTarget, event) { + const Wrapper = getWrapper(Object.getPrototypeOf(event)); + return new Wrapper(eventTarget, event); +} + +/** + * Get the immediateStopped flag of a given event. + * @param {Event} event The event to get. + * @returns {boolean} The flag to stop propagation immediately. + * @private + */ +export function isStopped(event) { + return pd(event).immediateStopped; +} + +/** + * Set the current event phase of a given event. + * @param {Event} event The event to set current target. + * @param {number} eventPhase New event phase. + * @returns {void} + * @private + */ +export function setEventPhase(event, eventPhase) { + pd(event).eventPhase = eventPhase; +} + +/** + * Set the current target of a given event. + * @param {Event} event The event to set current target. + * @param {EventTarget|null} currentTarget New current target. + * @returns {void} + * @private + */ +export function setCurrentTarget(event, currentTarget) { + pd(event).currentTarget = currentTarget; +} + +/** + * Set a passive listener of a given event. + * @param {Event} event The event to set current target. + * @param {Function|null} passiveListener New passive listener. + * @returns {void} + * @private + */ +export function setPassiveListener(event, passiveListener) { + pd(event).passiveListener = passiveListener; +} diff --git a/ui-v2/app/utils/dom/event-target/rsvp.js b/ui-v2/app/utils/dom/event-target/rsvp.js new file mode 100644 index 0000000000..d1a108663e --- /dev/null +++ b/ui-v2/app/utils/dom/event-target/rsvp.js @@ -0,0 +1,63 @@ +// Simple RSVP.EventTarget wrapper to make it more like a standard EventTarget +import RSVP from 'rsvp'; +// See https://github.com/mysticatea/event-target-shim/blob/v4.0.2/src/event.mjs +// The MIT License (MIT) - Copyright (c) 2015 Toru Nagashima +import { setCurrentTarget, wrapEvent } from './event-target-shim/event'; + +const EventTarget = function() {}; +function callbacksFor(object) { + let callbacks = object._promiseCallbacks; + + if (!callbacks) { + callbacks = object._promiseCallbacks = {}; + } + + return callbacks; +} +EventTarget.prototype = Object.assign( + Object.create(Object.prototype, { + constructor: { + value: EventTarget, + configurable: true, + writable: true, + }, + }), + { + dispatchEvent: function(obj) { + // borrow just what I need from event-target-shim + // to make true events even ErrorEvents with targets + const wrappedEvent = wrapEvent(this, obj); + setCurrentTarget(wrappedEvent, null); + // RSVP trigger doesn't bind to `this` + // the rest is pretty much the contents of `trigger` + // but with a `.bind(this)` to make it compatible + // with standard EventTarget + // we use `let` and `callbacksFor` above, just to keep things the same as rsvp.js + const eventName = obj.type; + const options = wrappedEvent; + let allCallbacks = callbacksFor(this); + + let callbacks = allCallbacks[eventName]; + if (callbacks) { + // Don't cache the callbacks.length since it may grow + let callback; + for (let i = 0; i < callbacks.length; i++) { + callback = callbacks[i]; + callback.bind(this)(options); + } + } + }, + addEventListener: function(event, cb) { + this.on(event, cb); + }, + removeEventListener: function(event, cb) { + try { + this.off(event, cb); + } catch (e) { + // passthrough + } + }, + } +); +RSVP.EventTarget.mixin(EventTarget.prototype); +export default EventTarget; diff --git a/ui-v2/app/utils/form/builder.js b/ui-v2/app/utils/form/builder.js index 2047f0e355..101e238781 100644 --- a/ui-v2/app/utils/form/builder.js +++ b/ui-v2/app/utils/form/builder.js @@ -77,9 +77,13 @@ export default function(changeset = defaultChangeset, getFormNameProperty = pars } const data = this.getData(); // ember-data/changeset dance + // TODO: This works for ember-data RecordSets and Changesets but not for plain js Objects + // see settings const json = typeof data.toJSON === 'function' ? data.toJSON() : get(data, 'data').toJSON(); // if the form doesn't include a property then throw so it can be // caught outside, therefore the user can deal with things that aren't in the data + // TODO: possibly need to add support for deeper properties using `get` here + // for example `client.blocking` instead of just `blocking` if (!Object.keys(json).includes(prop)) { const error = new Error(`${prop} property doesn't exist`); error.target = target; diff --git a/ui-v2/tests/acceptance/dc/list-blocking.feature b/ui-v2/tests/acceptance/dc/list-blocking.feature new file mode 100644 index 0000000000..790d20dce0 --- /dev/null +++ b/ui-v2/tests/acceptance/dc/list-blocking.feature @@ -0,0 +1,34 @@ +@setupApplicationTest +Feature: dc / list-blocking + In order to see updates without refreshing the page + As a user + I want to see changes if I change consul externally + Background: + Given 1 datacenter model with the value "dc-1" + And settings from yaml + --- + consul:client: + blocking: 1 + throttle: 200 + --- + Scenario: + And 3 [Model] models + And a network latency of 100 + When I visit the [Page] page for yaml + --- + dc: dc-1 + --- + Then the url should be /dc-1/[Url] + And pause until I see 3 [Model] models + And an external edit results in 5 [Model] models + And pause until I see 5 [Model] models + And an external edit results in 1 [Model] model + And pause until I see 1 [Model] model + And an external edit results in 0 [Model] models + And pause until I see 0 [Model] models + Where: + -------------------------------------------- + | Page | Model | Url | + | services | service | services | + | nodes | node | nodes | + -------------------------------------------- diff --git a/ui-v2/tests/acceptance/page-navigation.feature b/ui-v2/tests/acceptance/page-navigation.feature index c90c4b5765..44dbe02611 100644 --- a/ui-v2/tests/acceptance/page-navigation.feature +++ b/ui-v2/tests/acceptance/page-navigation.feature @@ -44,7 +44,7 @@ Feature: Page Navigation | Item | Model | URL | Endpoint | Back | | service | services | /dc-1/services/service-0 | /v1/health/service/service-0?dc=dc-1 | /dc-1/services | | node | nodes | /dc-1/nodes/node-0 | /v1/session/node/node-0?dc=dc-1 | /dc-1/nodes | - | kv | kvs | /dc-1/kv/necessitatibus-0/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1 | /dc-1/kv | + | kv | kvs | /dc-1/kv/0-key-value/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1 | /dc-1/kv | # | acl | acls | /dc-1/acls/anonymous | /v1/acl/info/anonymous?dc=dc-1 | /dc-1/acls | | intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/internal/ui/services?dc=dc-1 | /dc-1/intentions | | token | tokens | /dc-1/acls/tokens/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/acl/policies?dc=dc-1 | /dc-1/acls/tokens | @@ -116,7 +116,7 @@ Feature: Page Navigation Where: -------------------------------------------------------------------------------------------------------- | Item | Model | URL | Back | - | kv | kvs | /dc-1/kv/necessitatibus-0/edit | /dc-1/kv | + | kv | kvs | /dc-1/kv/0-key-value/edit | /dc-1/kv | # | acl | acls | /dc-1/acls/anonymous | /dc-1/acls | | intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /dc-1/intentions | -------------------------------------------------------------------------------------------------------- diff --git a/ui-v2/tests/acceptance/steps/dc/list-blocking-steps.js b/ui-v2/tests/acceptance/steps/dc/list-blocking-steps.js new file mode 100644 index 0000000000..3c9a76f69f --- /dev/null +++ b/ui-v2/tests/acceptance/steps/dc/list-blocking-steps.js @@ -0,0 +1,10 @@ +import steps from '../steps'; + +// step definitions that are shared between features should be moved to the +// tests/acceptance/steps/steps.js file + +export default function(assert) { + return steps(assert).then('I should find a file', function() { + assert.ok(true, this.step); + }); +} diff --git a/ui-v2/tests/integration/utils/dom/event-source/callable-test.js b/ui-v2/tests/integration/utils/dom/event-source/callable-test.js new file mode 100644 index 0000000000..02605d6f66 --- /dev/null +++ b/ui-v2/tests/integration/utils/dom/event-source/callable-test.js @@ -0,0 +1,84 @@ +import domEventSourceCallable from 'consul-ui/utils/dom/event-source/callable'; +import EventTarget from 'consul-ui/utils/dom/event-target/rsvp'; +import { Promise } from 'rsvp'; + +import { module } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import test from 'ember-sinon-qunit/test-support/test'; + +module('Integration | Utility | dom/event-source/callable', function(hooks) { + setupTest(hooks); + test('it dispatches messages', function(assert) { + assert.expect(1); + const EventSource = domEventSourceCallable(EventTarget); + const listener = this.stub(); + const source = new EventSource( + function(configuration) { + return new Promise(resolve => { + setTimeout(() => { + this.dispatchEvent({ + type: 'message', + data: null, + }); + resolve(); + }, configuration.milliseconds); + }); + }, + { + milliseconds: 100, + } + ); + source.addEventListener('message', function() { + listener(); + }); + return new Promise(function(resolve) { + setTimeout(function() { + source.close(); + assert.equal(listener.callCount, 5); + resolve(); + }, 550); + }); + }); + test('it dispatches a single open event and closes when called with no callable', function(assert) { + assert.expect(4); + const EventSource = domEventSourceCallable(EventTarget); + const listener = this.stub(); + const source = new EventSource(); + source.addEventListener('open', function(e) { + assert.deepEqual(e.target, this); + assert.equal(e.target.readyState, 1); + listener(); + }); + return Promise.resolve().then(function() { + assert.ok(listener.calledOnce); + assert.equal(source.readyState, 2); + }); + }); + test('it dispatches a single open event, and calls the specified callable that can dispatch an event', function(assert) { + assert.expect(1); + const EventSource = domEventSourceCallable(EventTarget); + const listener = this.stub(); + const source = new EventSource(function() { + return new Promise(resolve => { + setTimeout(() => { + this.dispatchEvent({ + type: 'message', + data: {}, + }); + this.close(); + }, 190); + }); + }); + source.addEventListener('open', function() { + // open is called first + listener(); + }); + return new Promise(function(resolve) { + source.addEventListener('message', function() { + // message is called second + assert.ok(listener.calledOnce); + resolve(); + }); + }); + }); +}); diff --git a/ui-v2/tests/steps.js b/ui-v2/tests/steps.js index 859cfe830b..d0adc96f59 100644 --- a/ui-v2/tests/steps.js +++ b/ui-v2/tests/steps.js @@ -65,7 +65,10 @@ export default function(assert) { }, yadda) ) // doubles - .given(['$number $model model[s]?', '$number $model models'], function(number, model) { + .given(['an external edit results in $number $model model[s]?'], function(number, model) { + return create(number, model); + }) + .given(['$number $model model[s]?'], function(number, model) { return create(number, model); }) .given(['$number $model model[s]? with the value "$value"'], function(number, model, value) { @@ -77,7 +80,15 @@ export default function(assert) { return create(number, model, data); } ) - .given(["I'm using a legacy token"], function(number, model, data) { + .given(['settings from yaml\n$yaml'], function(data) { + return Object.keys(data).forEach(function(key) { + window.localStorage[key] = JSON.stringify(data[key]); + }); + }) + .given('a network latency of $number', function(number) { + api.server.setCookie('CONSUL_LATENCY', number); + }) + .given(["I'm using a legacy token"], function() { window.localStorage['consul:token'] = JSON.stringify({ AccessorID: null, SecretID: 'id' }); }) // TODO: Abstract this away from HTTP @@ -188,6 +199,26 @@ export default function(assert) { }); }) // assertions + .then('pause until I see $number $model model[s]?', function(num, model) { + return new Promise(function(resolve) { + let count = 0; + const interval = setInterval(function() { + if (++count >= 50) { + clearInterval(interval); + assert.ok(false); + resolve(); + } + const len = currentPage[`${pluralize(model)}`].filter(function(item) { + return item.isVisible; + }).length; + if (len === num) { + clearInterval(interval); + assert.equal(len, num, `Expected ${num} ${model}s, saw ${len}`); + resolve(); + } + }, 100); + }); + }) .then('a $method request is made to "$url" with the body from yaml\n$yaml', function( method, url, @@ -358,6 +389,9 @@ export default function(assert) { .then('I have settings like yaml\n$yaml', function(data) { // TODO: Inject this const settings = window.localStorage; + // TODO: this and the setup should probably use consul: + // as we are talking about 'settings' here not localStorage + // so the prefix should be hidden Object.keys(data).forEach(function(prop) { const actual = settings.getItem(prop); const expected = data[prop]; diff --git a/ui-v2/tests/unit/controllers/settings-test.js b/ui-v2/tests/unit/controllers/settings-test.js new file mode 100644 index 0000000000..236dfcdf2d --- /dev/null +++ b/ui-v2/tests/unit/controllers/settings-test.js @@ -0,0 +1,12 @@ +import { moduleFor, test } from 'ember-qunit'; + +moduleFor('controller:settings', 'Unit | Controller | settings', { + // Specify the other units that are required for this test. + needs: ['service:settings', 'service:dom'], +}); + +// Replace this with your real tests. +test('it exists', function(assert) { + let controller = this.subject(); + assert.ok(controller); +}); diff --git a/ui-v2/tests/unit/routes/settings-test.js b/ui-v2/tests/unit/routes/settings-test.js index 52f71ebcec..53583e8088 100644 --- a/ui-v2/tests/unit/routes/settings-test.js +++ b/ui-v2/tests/unit/routes/settings-test.js @@ -3,6 +3,7 @@ import { moduleFor, test } from 'ember-qunit'; moduleFor('route:settings', 'Unit | Route | settings', { // Specify the other units that are required for this test. needs: [ + 'service:client/http', 'service:repository/dc', 'service:settings', 'service:logger', diff --git a/ui-v2/tests/unit/utils/dom/event-source/blocking-test.js b/ui-v2/tests/unit/utils/dom/event-source/blocking-test.js new file mode 100644 index 0000000000..c405636a8e --- /dev/null +++ b/ui-v2/tests/unit/utils/dom/event-source/blocking-test.js @@ -0,0 +1,87 @@ +import domEventSourceBlocking, { + create5xxBackoff, +} from 'consul-ui/utils/dom/event-source/blocking'; +import { module } from 'qunit'; +import test from 'ember-sinon-qunit/test-support/test'; + +module('Unit | Utility | dom/event-source/blocking'); + +const createEventSource = function() { + return class { + constructor(cb) { + this.readyState = 1; + this.source = cb; + this.source.apply(this, arguments); + } + addEventListener() {} + removeEventListener() {} + dispatchEvent() {} + close() {} + }; +}; +const createPromise = function(resolve = function() {}) { + class PromiseMock { + constructor(cb = function() {}) { + cb(resolve); + } + then(cb) { + setTimeout(() => cb.bind(this)(), 0); + return this; + } + catch(cb) { + cb({ message: 'error' }); + return this; + } + } + PromiseMock.resolve = function() { + return new PromiseMock(); + }; + return PromiseMock; +}; +test('it creates an BlockingEventSource class implementing EventSource', function(assert) { + const EventSource = createEventSource(); + const BlockingEventSource = domEventSourceBlocking(EventSource, function() {}); + assert.ok(BlockingEventSource instanceof Function); + const source = new BlockingEventSource(function() { + return createPromise().resolve(); + }); + assert.ok(source instanceof EventSource); +}); +test("the 5xx backoff continues to throw when it's not a 5xx", function(assert) { + const backoff = create5xxBackoff(); + [ + undefined, + null, + new Error(), + { errors: [] }, + { errors: [{ status: '0' }] }, + { errors: [{ status: 501 }] }, + { errors: [{ status: '401' }] }, + { errors: [{ status: '500' }] }, + { errors: [{ status: '5' }] }, + { errors: [{ status: '50' }] }, + { errors: [{ status: '5000' }] }, + { errors: [{ status: '5050' }] }, + ].forEach(function(item) { + assert.throws(function() { + backoff(item); + }); + }); +}); +test('the 5xx backoff returns a resolve promise on a 5xx (apart from 500)', function(assert) { + [ + { errors: [{ status: '501' }] }, + { errors: [{ status: '503' }] }, + { errors: [{ status: '504' }] }, + { errors: [{ status: '524' }] }, + ].forEach(item => { + const timeout = this.stub().callsArg(0); + const resolve = this.stub().withArgs(item); + const Promise = createPromise(resolve); + const backoff = create5xxBackoff(undefined, Promise, timeout); + const promise = backoff(item); + assert.ok(promise instanceof Promise, 'a promise was returned'); + assert.ok(resolve.calledOnce, 'the promise was resolved with the correct arguments'); + assert.ok(timeout.calledOnce, 'timeout was called once'); + }); +}); diff --git a/ui-v2/tests/unit/utils/dom/event-source/cache-test.js b/ui-v2/tests/unit/utils/dom/event-source/cache-test.js new file mode 100644 index 0000000000..1d9a4df49a --- /dev/null +++ b/ui-v2/tests/unit/utils/dom/event-source/cache-test.js @@ -0,0 +1,145 @@ +import domEventSourceCache from 'consul-ui/utils/dom/event-source/cache'; +import { module } from 'qunit'; +import test from 'ember-sinon-qunit/test-support/test'; + +module('Unit | Utility | dom/event-source/cache'); + +const createEventSource = function() { + return class { + constructor(cb) { + this.source = cb; + this.source.apply(this, arguments); + } + addEventListener() {} + removeEventListener() {} + dispatchEvent() {} + close() {} + }; +}; +const createPromise = function( + resolve = result => result, + reject = (result = { message: 'error' }) => result +) { + class PromiseMock { + constructor(cb = function() {}) { + cb(resolve); + } + then(cb) { + setTimeout(() => cb.bind(this)(resolve()), 0); + return this; + } + catch(cb) { + setTimeout(() => cb.bind(this)(reject()), 0); + return this; + } + } + PromiseMock.resolve = function(result) { + return new PromiseMock(function(resolve) { + resolve(result); + }); + }; + PromiseMock.reject = function() { + return new PromiseMock(); + }; + return PromiseMock; +}; +test('it returns a function', function(assert) { + const EventSource = createEventSource(); + const Promise = createPromise(); + + const getCache = domEventSourceCache(function() {}, EventSource, Promise); + assert.ok(typeof getCache === 'function'); +}); +test('getCache returns a function', function(assert) { + const EventSource = createEventSource(); + const Promise = createPromise(); + + const getCache = domEventSourceCache(function() {}, EventSource, Promise); + const obj = {}; + const cache = getCache(obj); + assert.ok(typeof cache === 'function'); +}); +test('cache creates the default EventSource and keeps it open when there is a cursor', function(assert) { + const EventSource = createEventSource(); + const stub = { + configuration: { cursor: 1 }, + }; + const Promise = createPromise(function() { + return stub; + }); + const source = this.stub().returns(Promise.resolve()); + const cb = this.stub(); + const getCache = domEventSourceCache(source, EventSource, Promise); + const obj = {}; + const cache = getCache(obj); + const promisedEventSource = cache(cb, { + key: 'key', + settings: { + enabled: true, + }, + }); + assert.ok(source.calledOnce, 'promisifying source called once'); + assert.ok(promisedEventSource instanceof Promise, 'source returns a Promise'); + const retrievedEventSource = cache(cb, { + key: 'key', + settings: { + enabled: true, + }, + }); + assert.deepEqual(promisedEventSource, retrievedEventSource); + assert.ok(source.calledTwice, 'promisifying source called once'); + assert.ok(retrievedEventSource instanceof Promise, 'source returns a Promise'); +}); +test('cache creates the default EventSource and keeps it open when there is a cursor', function(assert) { + const EventSource = createEventSource(); + const stub = { + close: this.stub(), + configuration: { cursor: 1 }, + }; + const Promise = createPromise(function() { + return stub; + }); + const source = this.stub().returns(Promise.resolve()); + const cb = this.stub(); + const getCache = domEventSourceCache(source, EventSource, Promise); + const obj = {}; + const cache = getCache(obj); + const promisedEventSource = cache(cb, { + key: 0, + settings: { + enabled: true, + }, + }); + assert.ok(source.calledOnce, 'promisifying source called once'); + assert.ok(cb.calledOnce, 'callable event source callable called once'); + assert.ok(promisedEventSource instanceof Promise, 'source returns a Promise'); + // >> + return promisedEventSource.then(function() { + assert.notOk(stub.close.called, "close wasn't called"); + }); +}); +test("cache creates the default EventSource and closes it when there isn't a cursor", function(assert) { + const EventSource = createEventSource(); + const stub = { + close: this.stub(), + configuration: {}, + }; + const Promise = createPromise(function() { + return stub; + }); + const source = this.stub().returns(Promise.resolve()); + const cb = this.stub(); + const getCache = domEventSourceCache(source, EventSource, Promise); + const obj = {}; + const cache = getCache(obj); + const promisedEventSource = cache(cb, { + key: 0, + }); + assert.ok(source.calledOnce, 'promisifying source called once'); + assert.ok(cb.calledOnce, 'callable event source callable called once'); + assert.ok(promisedEventSource instanceof Promise, 'source returns a Promise'); + // >> + return promisedEventSource.then(function() { + assert.ok(stub.close.calledOnce, 'close was called'); + }); +}); diff --git a/ui-v2/tests/unit/utils/dom/event-source/callable-test.js b/ui-v2/tests/unit/utils/dom/event-source/callable-test.js new file mode 100644 index 0000000000..e43b342fb1 --- /dev/null +++ b/ui-v2/tests/unit/utils/dom/event-source/callable-test.js @@ -0,0 +1,65 @@ +import domEventSourceCallable, { defaultRunner } from 'consul-ui/utils/dom/event-source/callable'; +import { module } from 'qunit'; +import test from 'ember-sinon-qunit/test-support/test'; + +module('Unit | Utility | dom/event-source/callable'); + +const createEventTarget = function() { + return class { + addEventListener() {} + removeEventListener() {} + dispatchEvent() {} + }; +}; +const createPromise = function() { + class PromiseMock { + then(cb) { + cb(); + return this; + } + catch(cb) { + cb({ message: 'error' }); + return this; + } + } + PromiseMock.resolve = function() { + return new PromiseMock(); + }; + return PromiseMock; +}; +test('it creates an EventSource class implementing EventTarget', function(assert) { + const EventTarget = createEventTarget(); + const EventSource = domEventSourceCallable(EventTarget, createPromise()); + assert.ok(EventSource instanceof Function); + const source = new EventSource(); + assert.ok(source instanceof EventTarget); +}); +test('the default runner loops and can be closed', function(assert) { + assert.expect(12); // 10 not closed, 1 to close and the final call count + let count = 0; + const isClosed = function() { + count++; + assert.ok(true); + return count === 11; + }; + const configuration = {}; + const then = this.stub().callsArg(0); + const target = { + source: function(configuration) { + return { + then: then, + }; + }, + }; + defaultRunner(target, configuration, isClosed); + assert.ok(then.callCount == 10); +}); +test('it calls the defaultRunner', function(assert) { + const Promise = createPromise(); + const EventTarget = createEventTarget(); + const run = this.stub(); + const EventSource = domEventSourceCallable(EventTarget, Promise, run); + const source = new EventSource(); + assert.ok(run.calledOnce); + assert.equal(source.readyState, 2); +}); diff --git a/ui-v2/tests/unit/utils/dom/event-source/index-test.js b/ui-v2/tests/unit/utils/dom/event-source/index-test.js new file mode 100644 index 0000000000..0e99418ebe --- /dev/null +++ b/ui-v2/tests/unit/utils/dom/event-source/index-test.js @@ -0,0 +1,28 @@ +import { + source, + proxy, + cache, + resolve, + CallableEventSource, + ReopenableEventSource, + BlockingEventSource, + StorageEventSource, +} from 'consul-ui/utils/dom/event-source/index'; +import { module, test } from 'qunit'; + +module('Unit | Utility | dom/event source/index'); + +// Replace this with your real tests. +test('it works', function(assert) { + // All The EventSource + assert.ok(typeof CallableEventSource === 'function'); + assert.ok(typeof ReopenableEventSource === 'function'); + assert.ok(typeof BlockingEventSource === 'function'); + assert.ok(typeof StorageEventSource === 'function'); + + // Utils + assert.ok(typeof source === 'function'); + assert.ok(typeof proxy === 'function'); + assert.ok(typeof cache === 'function'); + assert.ok(typeof resolve === 'function'); +}); diff --git a/ui-v2/tests/unit/utils/dom/event-source/proxy-test.js b/ui-v2/tests/unit/utils/dom/event-source/proxy-test.js new file mode 100644 index 0000000000..75f136efaa --- /dev/null +++ b/ui-v2/tests/unit/utils/dom/event-source/proxy-test.js @@ -0,0 +1,10 @@ +import domEventSourceProxy from 'consul-ui/utils/dom/event-source/proxy'; +import { module, test } from 'qunit'; + +module('Unit | Utility | dom/event source/proxy'); + +// Replace this with your real tests. +test('it works', function(assert) { + let result = domEventSourceProxy(); + assert.ok(result); +}); diff --git a/ui-v2/tests/unit/utils/dom/event-source/reopenable-test.js b/ui-v2/tests/unit/utils/dom/event-source/reopenable-test.js new file mode 100644 index 0000000000..4936690140 --- /dev/null +++ b/ui-v2/tests/unit/utils/dom/event-source/reopenable-test.js @@ -0,0 +1,46 @@ +import domEventSourceReopenable from 'consul-ui/utils/dom/event-source/reopenable'; +import { module } from 'qunit'; +import test from 'ember-sinon-qunit/test-support/test'; + +module('Unit | Utility | dom/event-source/reopenable'); + +const createEventSource = function() { + return class { + constructor(cb) { + this.readyState = 1; + this.source = cb; + this.source.apply(this, arguments); + } + addEventListener() {} + removeEventListener() {} + dispatchEvent() {} + close() {} + }; +}; +test('it creates an Reopenable class implementing EventSource', function(assert) { + const EventSource = createEventSource(); + const ReopenableEventSource = domEventSourceReopenable(EventSource); + assert.ok(ReopenableEventSource instanceof Function); + const source = new ReopenableEventSource(function() {}); + assert.ok(source instanceof EventSource); +}); +test('it reopens the event source when reopen is called', function(assert) { + const callable = this.stub(); + const EventSource = createEventSource(); + const ReopenableEventSource = domEventSourceReopenable(EventSource); + const source = new ReopenableEventSource(callable); + assert.equal(source.readyState, 1); + // first automatic EventSource `open` + assert.ok(callable.calledOnce); + source.readyState = 3; + source.reopen(); + // still only called once as it hasn't completely closed yet + // therefore is just opened by resetting the readyState + assert.ok(callable.calledOnce); + assert.equal(source.readyState, 1); + // properly close the source + source.readyState = 2; + source.reopen(); + // this time it is reopened via a recall of the callable + assert.ok(callable.calledTwice); +}); diff --git a/ui-v2/tests/unit/utils/dom/event-source/resolver-test.js b/ui-v2/tests/unit/utils/dom/event-source/resolver-test.js new file mode 100644 index 0000000000..e123dbed9f --- /dev/null +++ b/ui-v2/tests/unit/utils/dom/event-source/resolver-test.js @@ -0,0 +1,10 @@ +import domEventSourceResolver from 'consul-ui/utils/dom/event-source/resolver'; +import { module, test } from 'qunit'; + +module('Unit | Utility | dom/event source/resolver'); + +// Replace this with your real tests. +test('it works', function(assert) { + let result = domEventSourceResolver(); + assert.ok(result); +}); diff --git a/ui-v2/tests/unit/utils/dom/event-source/storage-test.js b/ui-v2/tests/unit/utils/dom/event-source/storage-test.js new file mode 100644 index 0000000000..ccd83872a7 --- /dev/null +++ b/ui-v2/tests/unit/utils/dom/event-source/storage-test.js @@ -0,0 +1,10 @@ +import domEventSourceStorage from 'consul-ui/utils/dom/event-source/storage'; +import { module, test } from 'qunit'; + +module('Unit | Utility | dom/event source/storage'); + +// Replace this with your real tests. +test('it works', function(assert) { + let result = domEventSourceStorage(function EventTarget() {}); + assert.ok(result); +}); diff --git a/ui-v2/tests/unit/utils/dom/event-target/rsvp-test.js b/ui-v2/tests/unit/utils/dom/event-target/rsvp-test.js new file mode 100644 index 0000000000..7cea41640e --- /dev/null +++ b/ui-v2/tests/unit/utils/dom/event-target/rsvp-test.js @@ -0,0 +1,13 @@ +import domEventTargetRsvp from 'consul-ui/utils/dom/event-target/rsvp'; +import { module, test } from 'qunit'; + +module('Unit | Utility | dom/event-target/rsvp'); + +// Replace this with your real tests. +test('it has EventTarget methods', function(assert) { + const result = domEventTargetRsvp; + assert.equal(typeof result, 'function'); + ['addEventListener', 'removeEventListener', 'dispatchEvent'].forEach(function(item) { + assert.equal(typeof result.prototype[item], 'function'); + }); +}); diff --git a/ui-v2/yarn.lock b/ui-v2/yarn.lock index ce3cef4734..819d093918 100644 --- a/ui-v2/yarn.lock +++ b/ui-v2/yarn.lock @@ -543,6 +543,7 @@ "@gardenhq/component-factory@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@gardenhq/component-factory/-/component-factory-1.4.0.tgz#f5da8ddf2050fde9c69f4426d61fe55de043e78d" + integrity sha1-9dqN3yBQ/enGn0Qm1h/lXeBD540= dependencies: "@gardenhq/domino" "^1.0.0" "@gardenhq/tick-control" "^2.0.0" @@ -552,6 +553,7 @@ "@gardenhq/domino@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@gardenhq/domino/-/domino-1.0.0.tgz#832c493f3f05697b7df4ccce00c4cf620dc60923" + integrity sha1-gyxJPz8FaXt99MzOAMTPYg3GCSM= optionalDependencies: min-document "^2.19.0" unfetch "^2.1.2" @@ -560,6 +562,7 @@ "@gardenhq/o@^8.0.1": version "8.0.1" resolved "https://registry.yarnpkg.com/@gardenhq/o/-/o-8.0.1.tgz#d6772cec7e4295a951165284cf43fbd0a373b779" + integrity sha1-1ncs7H5ClalRFlKEz0P70KNzt3k= dependencies: "@gardenhq/component-factory" "^1.4.0" "@gardenhq/tick-control" "^2.0.0" @@ -577,10 +580,12 @@ "@gardenhq/tick-control@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@gardenhq/tick-control/-/tick-control-2.0.0.tgz#f84fe38ca7a09b7b2b52f42945c50429ba639897" + integrity sha1-+E/jjKegm3srUvQpRcUEKbpjmJc= "@gardenhq/willow@^6.2.0": version "6.2.0" resolved "https://registry.yarnpkg.com/@gardenhq/willow/-/willow-6.2.0.tgz#3e4bc220a89099732746ead3385cc097bfb70186" + integrity sha1-PkvCIKiQmXMnRurTOFzAl7+3AYY= "@glimmer/di@^0.2.0": version "0.2.1" @@ -593,8 +598,9 @@ "@glimmer/di" "^0.2.0" "@hashicorp/api-double@^1.3.0": - version "1.4.4" - resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.4.4.tgz#db5521230b0031bfc3dc3cc5b775f17413a4fe91" + version "1.4.5" + resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.4.5.tgz#839ba882fad76eb17fd2eb3a8899bf5dd5a162a8" + integrity sha512-X8xRtZGXu4JAlh/deaaPW15L8gJIqwNpVEM2OKLkQu1AWHXSh3NF8Vhd5U81061+Dha8Ohl8aEE7LZ8f1tPvzg== dependencies: "@gardenhq/o" "^8.0.1" "@gardenhq/tick-control" "^2.0.0" @@ -606,12 +612,14 @@ js-yaml "^3.10.0" "@hashicorp/consul-api-double@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.0.1.tgz#eaf2e3f230fbdd876c90b931fd4bb4d94aac10e2" + version "2.1.0" + resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.1.0.tgz#511e6a48842ad31133e2070f3b2307568539b10e" + integrity sha512-cyW7TiKQylrWzVUORT1e6m4SU8tQ1V5BYEKW2th7QwHP8OFazn/+om9hud/9X5YtjEuSPIQCmFIvhEVwZgLVpQ== "@hashicorp/ember-cli-api-double@^1.3.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@hashicorp/ember-cli-api-double/-/ember-cli-api-double-1.7.0.tgz#4fdab6152157dd82b999de030c593c87e0cdb8b7" + integrity sha512-ojPcUPyId+3hTbwAtBGYbP5TfCGVAH8Ky6kH+BzlisIO/8XKURo9BSYnFtmYWLgXQVLOIE3iuoia5kOjGS/w2A== dependencies: "@hashicorp/api-double" "^1.3.0" array-range "^1.0.1" @@ -769,6 +777,7 @@ "@xg-wang/whatwg-fetch@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@xg-wang/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#f7b222c012a238e7d6e89ed3d72a1e0edb58453d" + integrity sha512-ULtqA6L75RLzTNW68IiOja0XYv4Ebc3OGMzfia1xxSEMpD0mk/pMvkQX0vbCFyQmKc5xGp80Ms2WiSlXLh8hbA== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -995,6 +1004,7 @@ are-we-there-yet@~1.1.2: argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" @@ -1031,6 +1041,7 @@ array-find-index@^1.0.1: array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= array-map@~0.0.0: version "0.0.0" @@ -1039,6 +1050,7 @@ array-map@~0.0.0: array-range@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-range/-/array-range-1.0.1.tgz#f56e46591843611c6a56f77ef02eda7c50089bfc" + integrity sha1-9W5GWRhDYRxqVvd+8C7afFAIm/w= array-reduce@~0.0.0: version "0.0.0" @@ -1745,6 +1757,7 @@ babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: babel-standalone@^6.24.2: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-standalone/-/babel-standalone-6.26.0.tgz#15fb3d35f2c456695815ebf1ed96fe7f015b6886" + integrity sha1-Ffs9NfLEVmlYFevx7Zb+fwFbaIY= babel-template@^6.24.1, babel-template@^6.26.0: version "6.26.0" @@ -1903,6 +1916,7 @@ body-parser@1.18.2: body-parser@1.18.3, body-parser@^1.18.3: version "1.18.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= dependencies: bytes "3.0.0" content-type "~1.0.4" @@ -2640,6 +2654,7 @@ bytes@1: bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= cacache@^10.0.4: version "10.0.4" @@ -2895,6 +2910,7 @@ class-utils@^0.3.5: classtrophobic-es5@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/classtrophobic-es5/-/classtrophobic-es5-0.2.1.tgz#9bbfa62a9928abf26f385440032fb49da1cda88f" + integrity sha1-m7+mKpkoq/JvOFRAAy+0naHNqI8= clean-base-url@^1.0.0: version "1.0.0" @@ -3091,6 +3107,7 @@ commander@^2.6.0: commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== commander@~2.17.1: version "2.17.1" @@ -3206,10 +3223,12 @@ constants-browserify@^1.0.0, constants-browserify@~1.0.0: content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== continuable-cache@^0.3.1: version "0.3.1" @@ -3232,6 +3251,7 @@ convert-source-map@~1.1.0: cookie-parser@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5" + integrity sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU= dependencies: cookie "0.3.1" cookie-signature "1.0.6" @@ -3239,10 +3259,12 @@ cookie-parser@^1.4.3: cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= copy-concurrently@^1.0.0: version "1.0.5" @@ -3601,6 +3623,7 @@ des.js@^1.0.0: destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= detect-file@^0.1.0: version "0.1.0" @@ -3657,6 +3680,7 @@ dom-serializer@0: dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" + integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg= domain-browser@^1.1.1: version "1.2.0" @@ -3716,6 +3740,7 @@ editions@^1.1.1: ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.47: version "1.3.62" @@ -4701,6 +4726,7 @@ emojis-list@^2.0.0: encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= encoding@^0.1.11: version "0.1.12" @@ -4866,6 +4892,7 @@ es6-weak-map@^2.0.1: escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" @@ -4958,6 +4985,7 @@ espree@^3.5.4: esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esprima@~3.0.0: version "3.0.0" @@ -4994,6 +5022,7 @@ esutils@^2.0.2: etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= event-emitter@~0.3.5: version "0.3.5" @@ -5189,6 +5218,7 @@ express@^4.10.7, express@^4.12.3: express@^4.16.2: version "4.16.4" resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" + integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== dependencies: accepts "~1.3.5" array-flatten "1.1.1" @@ -5292,10 +5322,12 @@ eyes@0.1.x: fake-xml-http-request@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.0.0.tgz#41a92f0ca539477700cb1dafd2df251d55dac8ff" + integrity sha512-UjNnynb6eLAB0lyh2PlTEkjRJORnNsVF1hbzU+PQv89/cyBV9GDRCy7JAcLQgeCLYT+3kaumWWZKEJvbaK74eQ== faker@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f" + integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8= fast-deep-equal@^1.0.0: version "1.1.0" @@ -5372,7 +5404,8 @@ file-entry-cache@^2.0.0: file-saver@^1.3.3: version "1.3.8" - resolved "http://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8" + integrity sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg== filename-regex@^2.0.0: version "2.0.1" @@ -5410,7 +5443,8 @@ fill-range@^4.0.0: finalhandler@1.1.1: version "1.1.1" - resolved "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== dependencies: debug "2.6.9" encodeurl "~1.0.2" @@ -5552,6 +5586,7 @@ formatio@1.2.0: forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= fragment-cache@^0.2.1: version "0.2.1" @@ -5562,6 +5597,7 @@ fragment-cache@^0.2.1: fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= from2@^2.1.0: version "2.3.0" @@ -6200,7 +6236,8 @@ http-errors@1.6.2: http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: version "1.6.3" - resolved "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= dependencies: depd "~1.1.2" inherits "2.0.3" @@ -6260,6 +6297,7 @@ husky@^1.1.0: hyperhtml@^0.15.5: version "0.15.10" resolved "https://registry.yarnpkg.com/hyperhtml/-/hyperhtml-0.15.10.tgz#5e5f42393d4fc30cd803063fb88a5c9d97625e1c" + integrity sha512-D3dkc5nac47dzGXhLfGTearEoUXLk8ijSrj+5ngEH1Od+6EZ9Cwjspj/MWWx74DWpvCH+glO7M+B7WqCYSzkTg== iconv-lite@0.4.19: version "0.4.19" @@ -6268,6 +6306,7 @@ iconv-lite@0.4.19: iconv-lite@0.4.23: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== dependencies: safer-buffer ">= 2.1.2 < 3" @@ -6431,6 +6470,7 @@ invert-kv@^1.0.0: ipaddr.js@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" + integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= is-accessor-descriptor@^0.1.6: version "0.1.6" @@ -6631,6 +6671,7 @@ is-path-inside@^1.0.0: is-plain-obj@^1.1: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" @@ -6862,7 +6903,15 @@ js-yaml@0.3.x: version "0.3.7" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-0.3.7.tgz#d739d8ee86461e54b354d6a7d7d1f2ad9a167f62" -js-yaml@^3.10.0, js-yaml@^3.11.0, js-yaml@^3.12.0, js-yaml@^3.8.4: +js-yaml@^3.10.0, js-yaml@^3.11.0, js-yaml@^3.8.4: + version "3.12.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" + integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" dependencies: @@ -7701,7 +7750,8 @@ mdurl@^1.0.1: media-typer@0.3.0: version "0.3.0" - resolved "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= mem@^1.1.0: version "1.1.0" @@ -7740,10 +7790,12 @@ meow@^3.4.0, meow@^3.7.0: merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= merge-options@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-1.0.1.tgz#2a64b24457becd4e4dc608283247e94ce589aa32" + integrity sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg== dependencies: is-plain-obj "^1.1" @@ -7772,6 +7824,7 @@ merge@^1.1.3: methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7: version "2.3.11" @@ -7827,6 +7880,7 @@ mime-db@~1.36.0: mime-db@~1.37.0: version "1.37.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" + integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.7: version "2.1.20" @@ -7843,12 +7897,14 @@ mime-types@^2.1.18: mime-types@~2.1.18: version "2.1.21" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" + integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== dependencies: mime-db "~1.37.0" mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== mimic-fn@^1.0.0: version "1.2.0" @@ -7857,6 +7913,7 @@ mimic-fn@^1.0.0: min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" + integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU= dependencies: dom-walk "^0.1.0" @@ -7972,6 +8029,7 @@ morgan@^1.8.1: mousetrap@^1.6.1: version "1.6.2" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.2.tgz#caadd9cf886db0986fb2fee59a82f6bd37527587" + integrity sha512-jDjhi7wlHwdO6q6DS7YRmSHcuI+RVxadBkLt3KHrhd3C2b+w5pKefg3oj5beTcHZyVFA9Aksf+yEE1y5jxUjVA== mout@^1.0.0: version "1.1.0" @@ -7991,6 +8049,7 @@ move-concurrently@^1.0.1: ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= ms@^2.1.1: version "2.1.1" @@ -8051,7 +8110,8 @@ natural-compare@^1.4.0: ncp@^2.0.0: version "2.0.0" - resolved "http://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= needle@^2.2.1: version "2.2.4" @@ -8064,6 +8124,7 @@ needle@^2.2.1: negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= neo-async@^2.5.0: version "2.5.2" @@ -8409,6 +8470,7 @@ object.values@^1.0.4: on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= dependencies: ee-first "1.1.1" @@ -8607,6 +8669,7 @@ parseuri@0.0.5: parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= pascalcase@^0.1.1: version "0.1.1" @@ -8663,6 +8726,7 @@ path-posix@^1.0.0: path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= path-to-regexp@^1.7.0: version "1.7.0" @@ -8783,6 +8847,7 @@ preserve@^0.2.0: pretender@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/pretender/-/pretender-2.1.1.tgz#5085f0a1272c31d5b57c488386f69e6ca207cb35" + integrity sha512-IkidsJzaroAanw3I43tKCFm2xCpurkQr9aPXv5/jpN+LfCwDaeI8rngVWtQZTx4qqbhc5zJspnLHJ4N/25KvDQ== dependencies: "@xg-wang/whatwg-fetch" "^3.0.0" fake-xml-http-request "^2.0.0" @@ -8974,6 +9039,7 @@ randomfill@^1.0.3: range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= raw-body@2.3.2: version "2.3.2" @@ -8987,6 +9053,7 @@ raw-body@2.3.2: raw-body@2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== dependencies: bytes "3.0.0" http-errors "1.6.3" @@ -9115,6 +9182,7 @@ recast@^0.11.3: recursive-readdir-sync@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/recursive-readdir-sync/-/recursive-readdir-sync-1.0.6.tgz#1dbf6d32f3c5bb8d3cde97a6c588d547a9e13d56" + integrity sha1-Hb9tMvPFu4083pemxYjVR6nhPVY= redent@^1.0.0: version "1.0.0" @@ -9436,6 +9504,7 @@ rollup-plugin-commonjs@^9.1.0: rollup-plugin-memory@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/rollup-plugin-memory/-/rollup-plugin-memory-2.0.0.tgz#0a8ac6b57fa0e714f89a15c3ac82bc93f89c47c5" + integrity sha1-CorGtX+g5xT4mhXDrIK8k/icR8U= rollup-plugin-node-resolve@^3.3.0: version "3.4.0" @@ -9468,6 +9537,7 @@ rollup@^0.59.0: route-recognizer@^0.3.3: version "0.3.4" resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3" + integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g== rsvp@^3.0.14, rsvp@^3.0.16, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0, rsvp@^3.2.1, rsvp@^3.3.3, rsvp@^3.5.0: version "3.6.2" @@ -9550,6 +9620,7 @@ safe-regex@^1.1.0: "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== samsam@1.3.0, samsam@1.x, samsam@^1.1.3: version "1.3.0" @@ -9619,6 +9690,7 @@ semver@~5.3.0: send@0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== dependencies: debug "2.6.9" depd "~1.1.2" @@ -9641,6 +9713,7 @@ serialize-javascript@^1.4.0: serve-static@1.13.2: version "1.13.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" + integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" @@ -9684,6 +9757,7 @@ setprototypeof@1.0.3: setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: version "2.4.11" @@ -9894,6 +9968,7 @@ source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4: source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" @@ -9960,6 +10035,7 @@ sprintf-js@^1.0.3: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= sri-toolbox@^0.2.0: version "0.2.0" @@ -10012,6 +10088,7 @@ static-extend@^0.1.1: statuses@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== stdout-stream@^1.4.0: version "1.4.1" @@ -10608,6 +10685,7 @@ underscore@~1.6.0: unfetch@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-2.1.2.tgz#684fee4d8acdb135bdb26c0364c642fc326ca95b" + integrity sha1-aE/uTYrNsTW9smwDZMZC/DJsqVs= unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" @@ -10662,6 +10740,7 @@ universalify@^0.1.0: unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= unquote@~1.1.1: version "1.1.1" @@ -10758,6 +10837,7 @@ util@^0.10.3: utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= uuid@^3.0.0, uuid@^3.1.0, uuid@^3.3.2: version "3.3.2" @@ -10779,6 +10859,7 @@ validate-npm-package-name@^3.0.0: vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= verror@1.10.0: version "1.10.0" @@ -11004,6 +11085,7 @@ xdg-basedir@^3.0.0: xhr2@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f" + integrity sha1-f4dliEdxbbUCYyOBL4GMras4el8= xmldom@^0.1.19: version "0.1.27"