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"