Skip to content

Commit

Permalink
UI: Add EventSource ready for implementing blocking queries (#5070)
Browse files Browse the repository at this point in the history
- 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)
  • Loading branch information
johncowen authored and John Cowen committed Feb 21, 2019
1 parent 9c157e2 commit 2f72484
Show file tree
Hide file tree
Showing 45 changed files with 1,878 additions and 56 deletions.
1 change: 1 addition & 0 deletions ui-v2/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
app/utils/dom/event-target/event-target-shim/event.js
3 changes: 3 additions & 0 deletions ui-v2/app/adapters/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down
3 changes: 2 additions & 1 deletion ui-v2/app/controllers/dc/nodes/index.js
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
15 changes: 8 additions & 7 deletions ui-v2/app/controllers/dc/services/index.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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',
Expand All @@ -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')));
Expand Down
29 changes: 29 additions & 0 deletions ui-v2/app/controllers/settings.js
Original file line number Diff line number Diff line change
@@ -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;
}
},
},
});
61 changes: 61 additions & 0 deletions ui-v2/app/instance-initializers/event-source.js
Original file line number Diff line number Diff line change
@@ -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,
};
16 changes: 16 additions & 0 deletions ui-v2/app/mixins/with-event-source.js
Original file line number Diff line number Diff line change
@@ -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);
},
});
2 changes: 1 addition & 1 deletion ui-v2/app/mixins/with-filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion ui-v2/app/mixins/with-health-filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions ui-v2/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
},
Expand Down
16 changes: 10 additions & 6 deletions ui-v2/app/routes/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
},
},
});
3 changes: 3 additions & 0 deletions ui-v2/app/services/client/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions ui-v2/app/services/lazy-proxy.js
Original file line number Diff line number Diff line change
@@ -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);
};
}
}
}
},
});
92 changes: 92 additions & 0 deletions ui-v2/app/services/repository/type/event-source.js
Original file line number Diff line number Diff line change
@@ -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);
});
},
});
2 changes: 0 additions & 2 deletions ui-v2/app/templates/components/hashicorp-consul.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,9 @@
<li data-test-main-nav-docs>
<a href="{{ env 'CONSUL_DOCUMENTATION_URL'}}/index.html" rel="help noopener noreferrer" target="_blank">Documentation</a>
</li>
{{#if false }}
<li data-test-main-nav-settings class={{if (is-href 'settings') 'is-active'}}>
<a href={{href-to 'settings'}}>Settings</a>
</li>
{{/if}}
</ul>
</nav>
</div>
Expand Down
Loading

0 comments on commit 2f72484

Please sign in to comment.