diff --git a/ui-v2/app/components/changeable-set.js b/ui-v2/app/components/changeable-set.js new file mode 100644 index 0000000000..f9d88d91c4 --- /dev/null +++ b/ui-v2/app/components/changeable-set.js @@ -0,0 +1,19 @@ +import Component from '@ember/component'; +import { get, set } from '@ember/object'; +import SlotsMixin from 'ember-block-slots'; +import WithListeners from 'consul-ui/mixins/with-listeners'; + +export default Component.extend(WithListeners, SlotsMixin, { + tagName: '', + didReceiveAttrs: function() { + this._super(...arguments); + this.removeListeners(); + const dispatcher = get(this, 'dispatcher'); + if (dispatcher) { + this.listen(dispatcher, 'change', e => { + set(this, 'items', e.target.data); + }); + set(this, 'items', get(dispatcher, 'data')); + } + }, +}); diff --git a/ui-v2/app/components/freetext-filter.js b/ui-v2/app/components/freetext-filter.js index b5fc2d39ef..d776119ecf 100644 --- a/ui-v2/app/components/freetext-filter.js +++ b/ui-v2/app/components/freetext-filter.js @@ -1,7 +1,15 @@ import Component from '@ember/component'; - +import { get } from '@ember/object'; export default Component.extend({ tagName: 'fieldset', classNames: ['freetext-filter'], - onchange: function(){} + onchange: function(e) { + let searchable = get(this, 'searchable'); + if (!Array.isArray(searchable)) { + searchable = [searchable]; + } + searchable.forEach(function(item) { + item.search(e.target.value); + }); + }, }); diff --git a/ui-v2/app/controllers/dc/acls/policies/index.js b/ui-v2/app/controllers/dc/acls/policies/index.js index 534a82cda2..ea6142dbd7 100644 --- a/ui-v2/app/controllers/dc/acls/policies/index.js +++ b/ui-v2/app/controllers/dc/acls/policies/index.js @@ -1,23 +1,23 @@ import Controller from '@ember/controller'; -import { get } from '@ember/object'; -import WithFiltering from 'consul-ui/mixins/with-filtering'; -export default Controller.extend(WithFiltering, { +import { get, computed } from '@ember/object'; +import WithSearching from 'consul-ui/mixins/with-searching'; +export default Controller.extend(WithSearching, { queryParams: { s: { as: 'filter', replace: true, }, }, - filter: function(item, { s = '', type = '' }) { - const sLower = s.toLowerCase(); - return ( - get(item, 'Name') - .toLowerCase() - .indexOf(sLower) !== -1 || - get(item, 'Description') - .toLowerCase() - .indexOf(sLower) !== -1 - ); + init: function() { + this.searchParams = { + policy: 's', + }; + this._super(...arguments); }, + searchable: computed('items', function() { + return get(this, 'searchables.policy') + .add(get(this, 'items')) + .search(get(this, this.searchParams.policy)); + }), actions: {}, }); diff --git a/ui-v2/app/controllers/dc/acls/tokens/index.js b/ui-v2/app/controllers/dc/acls/tokens/index.js index 729664c72c..4c65831667 100644 --- a/ui-v2/app/controllers/dc/acls/tokens/index.js +++ b/ui-v2/app/controllers/dc/acls/tokens/index.js @@ -1,30 +1,24 @@ import Controller from '@ember/controller'; -import { get } from '@ember/object'; -import WithFiltering from 'consul-ui/mixins/with-filtering'; -export default Controller.extend(WithFiltering, { +import { computed, get } from '@ember/object'; +import WithSearching from 'consul-ui/mixins/with-searching'; +export default Controller.extend(WithSearching, { queryParams: { s: { as: 'filter', replace: true, }, }, - filter: function(item, { s = '', type = '' }) { - const sLower = s.toLowerCase(); - return ( - get(item, 'AccessorID') - .toLowerCase() - .indexOf(sLower) !== -1 || - get(item, 'Name') - .toLowerCase() - .indexOf(sLower) !== -1 || - get(item, 'Description') - .toLowerCase() - .indexOf(sLower) !== -1 || - (get(item, 'Policies') || []).some(function(item) { - return item.Name.toLowerCase().indexOf(sLower) !== -1; - }) - ); + init: function() { + this.searchParams = { + token: 's', + }; + this._super(...arguments); }, + searchable: computed('items', function() { + return get(this, 'searchables.token') + .add(get(this, 'items')) + .search(get(this, this.searchParams.token)); + }), actions: { sendClone: function(item) { this.send('clone', item); diff --git a/ui-v2/app/controllers/dc/intentions/index.js b/ui-v2/app/controllers/dc/intentions/index.js index 0ff73e7cdf..88c0b8e72d 100644 --- a/ui-v2/app/controllers/dc/intentions/index.js +++ b/ui-v2/app/controllers/dc/intentions/index.js @@ -1,6 +1,7 @@ import Controller from '@ember/controller'; import { computed, get } from '@ember/object'; import WithFiltering from 'consul-ui/mixins/with-filtering'; +import WithSearching from 'consul-ui/mixins/with-searching'; import ucfirst from 'consul-ui/utils/ucfirst'; // TODO: DRY out in acls at least const createCounter = function(prop) { @@ -9,7 +10,7 @@ const createCounter = function(prop) { }; }; const countAction = createCounter('Action'); -export default Controller.extend(WithFiltering, { +export default Controller.extend(WithSearching, WithFiltering, { queryParams: { action: { as: 'action', @@ -19,6 +20,17 @@ export default Controller.extend(WithFiltering, { replace: true, }, }, + init: function() { + this.searchParams = { + intention: 's', + }; + this._super(...arguments); + }, + searchable: computed('filtered', function() { + return get(this, 'searchables.intention') + .add(get(this, 'filtered')) + .search(get(this, this.searchParams.intention)); + }), actionFilters: computed('items', function() { const items = get(this, 'items'); return ['', 'allow', 'deny'].map(function(item) { @@ -32,16 +44,6 @@ export default Controller.extend(WithFiltering, { }); }), filter: function(item, { s = '', action = '' }) { - const source = get(item, 'SourceName').toLowerCase(); - const destination = get(item, 'DestinationName').toLowerCase(); - const sLower = s.toLowerCase(); - const allLabel = 'All Services (*)'.toLowerCase(); - return ( - (source.indexOf(sLower) !== -1 || - destination.indexOf(sLower) !== -1 || - (source === '*' && allLabel.indexOf(sLower) !== -1) || - (destination === '*' && allLabel.indexOf(sLower) !== -1)) && - (action === '' || get(item, 'Action') === action) - ); + return action === '' || get(item, 'Action') === action; }, }); diff --git a/ui-v2/app/controllers/dc/kv/index.js b/ui-v2/app/controllers/dc/kv/index.js index 20a2399058..ac19fabfd5 100644 --- a/ui-v2/app/controllers/dc/kv/index.js +++ b/ui-v2/app/controllers/dc/kv/index.js @@ -1,18 +1,22 @@ import Controller from '@ember/controller'; -import { get } from '@ember/object'; -import WithFiltering from 'consul-ui/mixins/with-filtering'; -import rightTrim from 'consul-ui/utils/right-trim'; -export default Controller.extend(WithFiltering, { +import { get, computed } from '@ember/object'; +import WithSearching from 'consul-ui/mixins/with-searching'; +export default Controller.extend(WithSearching, { queryParams: { s: { as: 'filter', replace: true, }, }, - filter: function(item, { s = '' }) { - const key = rightTrim(get(item, 'Key'), '/') - .split('/') - .pop(); - return key.toLowerCase().indexOf(s.toLowerCase()) !== -1; + init: function() { + this.searchParams = { + kv: 's', + }; + this._super(...arguments); }, + searchable: computed('items', function() { + return get(this, 'searchables.kv') + .add(get(this, 'items')) + .search(get(this, this.searchParams.kv)); + }), }); diff --git a/ui-v2/app/controllers/dc/nodes/index.js b/ui-v2/app/controllers/dc/nodes/index.js index 2202b73da1..cbbd7b90d9 100644 --- a/ui-v2/app/controllers/dc/nodes/index.js +++ b/ui-v2/app/controllers/dc/nodes/index.js @@ -1,12 +1,26 @@ import Controller from '@ember/controller'; import { computed } from '@ember/object'; 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(WithHealthFiltering, { +export default Controller.extend(WithSearching, WithHealthFiltering, { init: function() { + this.searchParams = { + healthyNode: 's', + unhealthyNode: 's', + }; this._super(...arguments); - this.columns = [25, 25, 25, 25]; }, + searchableHealthy: computed('healthy', function() { + return get(this, 'searchables.healthyNode') + .add(get(this, 'healthy')) + .search(get(this, this.searchParams.healthyNode)); + }), + searchableUnhealthy: computed('unhealthy', function() { + return get(this, 'searchables.unhealthyNode') + .add(get(this, 'unhealthy')) + .search(get(this, this.searchParams.unhealthyNode)); + }), unhealthy: computed('filtered', function() { return get(this, 'filtered').filter(function(item) { return get(item, 'isUnhealthy'); @@ -18,10 +32,6 @@ export default Controller.extend(WithHealthFiltering, { }); }), filter: function(item, { s = '', status = '' }) { - return ( - get(item, 'Node') - .toLowerCase() - .indexOf(s.toLowerCase()) !== -1 && item.hasStatus(status) - ); + return item.hasStatus(status); }, }); diff --git a/ui-v2/app/controllers/dc/nodes/show.js b/ui-v2/app/controllers/dc/nodes/show.js index 1eb34e6bec..8c900df163 100644 --- a/ui-v2/app/controllers/dc/nodes/show.js +++ b/ui-v2/app/controllers/dc/nodes/show.js @@ -1,18 +1,29 @@ import Controller from '@ember/controller'; -import { get, set } from '@ember/object'; +import { get, set, computed } from '@ember/object'; import { getOwner } from '@ember/application'; -import WithFiltering from 'consul-ui/mixins/with-filtering'; +import WithSearching from 'consul-ui/mixins/with-searching'; import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; import getComponentFactory from 'consul-ui/utils/get-component-factory'; const $$ = qsaFactory(); -export default Controller.extend(WithFiltering, { +export default Controller.extend(WithSearching, { queryParams: { s: { as: 'filter', replace: true, }, }, + init: function() { + this.searchParams = { + nodeservice: 's', + }; + this._super(...arguments); + }, + searchable: computed('items', function() { + return get(this, 'searchables.nodeservice') + .add(get(this, 'items')) + .search(get(this, this.searchParams.nodeservice)); + }), setProperties: function() { this._super(...arguments); // the default selected tab depends on whether you have any healthchecks or not @@ -22,24 +33,6 @@ export default Controller.extend(WithFiltering, { // need this variable set(this, 'selectedTab', get(this.item, 'Checks.length') > 0 ? 'health-checks' : 'services'); }, - filter: function(item, { s = '' }) { - const term = s.toLowerCase(); - return ( - get(item, 'Service') - .toLowerCase() - .indexOf(term) !== -1 || - get(item, 'ID') - .toLowerCase() - .indexOf(term) !== -1 || - (get(item, 'Tags') || []).some(function(item) { - return item.toLowerCase().indexOf(term) !== -1; - }) || - get(item, 'Port') - .toString() - .toLowerCase() - .indexOf(term) !== -1 - ); - }, actions: { change: function(e) { set(this, 'selectedTab', e.target.value); diff --git a/ui-v2/app/controllers/dc/services/index.js b/ui-v2/app/controllers/dc/services/index.js index a0cc0bd399..1a81a10c01 100644 --- a/ui-v2/app/controllers/dc/services/index.js +++ b/ui-v2/app/controllers/dc/services/index.js @@ -2,6 +2,7 @@ import Controller from '@ember/controller'; import { get, computed } from '@ember/object'; import { htmlSafe } from '@ember/string'; import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering'; +import WithSearching from 'consul-ui/mixins/with-searching'; const max = function(arr, prop) { return arr.reduce(function(prev, item) { return Math.max(prev, get(item, prop)); @@ -24,18 +25,20 @@ const width = function(num) { const widthDeclaration = function(num) { return htmlSafe(`width: ${num}px`); }; -export default Controller.extend(WithHealthFiltering, { +export default Controller.extend(WithSearching, WithHealthFiltering, { + init: function() { + this.searchParams = { + service: 's', + }; + this._super(...arguments); + }, + searchable: computed('filtered', function() { + return get(this, 'searchables.service') + .add(get(this, 'filtered')) + .search(get(this, this.searchParams.service)); + }), filter: function(item, { s = '', status = '' }) { - const term = s.toLowerCase(); - return ( - (get(item, 'Name') - .toLowerCase() - .indexOf(term) !== -1 || - (get(item, 'Tags') || []).some(function(item) { - return item.toLowerCase().indexOf(term) !== -1; - })) && - item.hasStatus(status) - ); + return item.hasStatus(status); }, maxWidth: computed('{maxPassing,maxWarning,maxCritical}', function() { const PADDING = 32 * 3 + 13; diff --git a/ui-v2/app/controllers/dc/services/show.js b/ui-v2/app/controllers/dc/services/show.js index 5a08b008ae..fd3153661f 100644 --- a/ui-v2/app/controllers/dc/services/show.js +++ b/ui-v2/app/controllers/dc/services/show.js @@ -4,10 +4,25 @@ import { computed } from '@ember/object'; import sumOfUnhealthy from 'consul-ui/utils/sumOfUnhealthy'; import hasStatus from 'consul-ui/utils/hasStatus'; import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering'; -export default Controller.extend(WithHealthFiltering, { +import WithSearching from 'consul-ui/mixins/with-searching'; +export default Controller.extend(WithSearching, WithHealthFiltering, { init: function() { + this.searchParams = { + healthyServiceNode: 's', + unhealthyServiceNode: 's', + }; this._super(...arguments); }, + searchableHealthy: computed('healthy', function() { + return get(this, 'searchables.healthyServiceNode') + .add(get(this, 'healthy')) + .search(get(this, this.searchParams.healthyServiceNode)); + }), + searchableUnhealthy: computed('unhealthy', function() { + return get(this, 'searchables.unhealthyServiceNode') + .add(get(this, 'unhealthy')) + .search(get(this, this.searchParams.unhealthyServiceNode)); + }), unhealthy: computed('filtered', function() { return get(this, 'filtered').filter(function(item) { return sumOfUnhealthy(item.Checks) > 0; @@ -19,16 +34,6 @@ export default Controller.extend(WithHealthFiltering, { }); }), filter: function(item, { s = '', status = '' }) { - const term = s.toLowerCase(); - - return ( - get(item, 'Node.Node') - .toLowerCase() - .indexOf(term) !== -1 || - (get(item, 'Service.ID') - .toLowerCase() - .indexOf(term) !== -1 && - hasStatus(get(item, 'Checks'), status)) - ); + return hasStatus(get(item, 'Checks'), status); }, }); diff --git a/ui-v2/app/initializers/search.js b/ui-v2/app/initializers/search.js new file mode 100644 index 0000000000..455c9ad17b --- /dev/null +++ b/ui-v2/app/initializers/search.js @@ -0,0 +1,37 @@ +import intention from 'consul-ui/search/filters/intention'; +import token from 'consul-ui/search/filters/token'; +import policy from 'consul-ui/search/filters/policy'; +import kv from 'consul-ui/search/filters/kv'; +import node from 'consul-ui/search/filters/node'; +// service instance +import nodeService from 'consul-ui/search/filters/node/service'; +import serviceNode from 'consul-ui/search/filters/service/node'; +import service from 'consul-ui/search/filters/service'; + +import filterableFactory from 'consul-ui/utils/search/filterable'; +const filterable = filterableFactory(); +export function initialize(application) { + // Service-less injection using private properties at a per-project level + const Builder = application.resolveRegistration('service:search'); + const searchables = { + intention: intention(filterable), + token: token(filterable), + policy: policy(filterable), + kv: kv(filterable), + healthyNode: node(filterable), + unhealthyNode: node(filterable), + healthyServiceNode: serviceNode(filterable), + unhealthyServiceNode: serviceNode(filterable), + nodeservice: nodeService(filterable), + service: service(filterable), + }; + Builder.reopen({ + searchable: function(name) { + return searchables[name]; + }, + }); +} + +export default { + initialize, +}; diff --git a/ui-v2/app/mixins/with-listeners.js b/ui-v2/app/mixins/with-listeners.js new file mode 100644 index 0000000000..a1ef188b80 --- /dev/null +++ b/ui-v2/app/mixins/with-listeners.js @@ -0,0 +1,27 @@ +import Component from '@ember/component'; +import Mixin from '@ember/object/mixin'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; + +export default Mixin.create({ + dom: service('dom'), + init: function() { + this._super(...arguments); + this._listeners = get(this, 'dom').listeners(); + let method = 'willDestroy'; + if (this instanceof Component) { + method = 'willDestroyElement'; + } + const destroy = this[method]; + this[method] = function() { + destroy(...arguments); + this.removeListeners(); + }; + }, + listen: function(target, event, handler) { + return this._listeners.add(...arguments); + }, + removeListeners: function() { + return this._listeners.remove(...arguments); + }, +}); diff --git a/ui-v2/app/mixins/with-searching.js b/ui-v2/app/mixins/with-searching.js new file mode 100644 index 0000000000..2a18a75601 --- /dev/null +++ b/ui-v2/app/mixins/with-searching.js @@ -0,0 +1,32 @@ +import Mixin from '@ember/object/mixin'; +import { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; +import WithListeners from 'consul-ui/mixins/with-listeners'; +/** + * WithSearching mostly depends on a `searchParams` object which must be set + * inside the `init` function. The naming and usage of this is modelled on + * `queryParams` but in contrast cannot _yet_ be 'hung' of the Controller + * object, it MUST be set in the `init` method. + * Reasons: As well as producing a eslint error, it can also be 'shared' amongst + * child Classes of the component. It is not clear _yet_ whether mixing this in + * avoids this and is something to be looked at in future to slightly improve DX + * Please also see: + * https://emberjs.com/api/ember/2.12/classes/Ember.Object/properties?anchor=mergedProperties + * + */ +export default Mixin.create(WithListeners, { + builder: service('search'), + init: function() { + this._super(...arguments); + const params = this.searchParams || {}; + this.searchables = {}; + Object.keys(params).forEach(type => { + const key = params[type]; + this.searchables[type] = get(this, 'builder').searchable(type); + this.listen(this.searchables[type], 'change', e => { + const value = e.target.value; + set(this, key, value === '' ? null : value); + }); + }); + }, +}); diff --git a/ui-v2/app/search/filters/intention.js b/ui-v2/app/search/filters/intention.js new file mode 100644 index 0000000000..cf71a6d86c --- /dev/null +++ b/ui-v2/app/search/filters/intention.js @@ -0,0 +1,15 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const source = get(item, 'SourceName').toLowerCase(); + const destination = get(item, 'DestinationName').toLowerCase(); + const sLower = s.toLowerCase(); + const allLabel = 'All Services (*)'.toLowerCase(); + return ( + source.indexOf(sLower) !== -1 || + destination.indexOf(sLower) !== -1 || + (source === '*' && allLabel.indexOf(sLower) !== -1) || + (destination === '*' && allLabel.indexOf(sLower) !== -1) + ); + }); +} diff --git a/ui-v2/app/search/filters/kv.js b/ui-v2/app/search/filters/kv.js new file mode 100644 index 0000000000..ffef18f3c9 --- /dev/null +++ b/ui-v2/app/search/filters/kv.js @@ -0,0 +1,10 @@ +import { get } from '@ember/object'; +import rightTrim from 'consul-ui/utils/right-trim'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const key = rightTrim(get(item, 'Key'), '/') + .split('/') + .pop(); + return key.toLowerCase().indexOf(s.toLowerCase()) !== -1; + }); +} diff --git a/ui-v2/app/search/filters/node.js b/ui-v2/app/search/filters/node.js new file mode 100644 index 0000000000..6ac9c302f6 --- /dev/null +++ b/ui-v2/app/search/filters/node.js @@ -0,0 +1,11 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'Node') + .toLowerCase() + .indexOf(sLower) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/node/service.js b/ui-v2/app/search/filters/node/service.js new file mode 100644 index 0000000000..255c238eaf --- /dev/null +++ b/ui-v2/app/search/filters/node/service.js @@ -0,0 +1,21 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const term = s.toLowerCase(); + return ( + get(item, 'Service') + .toLowerCase() + .indexOf(term) !== -1 || + get(item, 'ID') + .toLowerCase() + .indexOf(term) !== -1 || + (get(item, 'Tags') || []).some(function(item) { + return item.toLowerCase().indexOf(term) !== -1; + }) || + get(item, 'Port') + .toString() + .toLowerCase() + .indexOf(term) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/policy.js b/ui-v2/app/search/filters/policy.js new file mode 100644 index 0000000000..3144ac6e26 --- /dev/null +++ b/ui-v2/app/search/filters/policy.js @@ -0,0 +1,14 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'Name') + .toLowerCase() + .indexOf(sLower) !== -1 || + get(item, 'Description') + .toLowerCase() + .indexOf(sLower) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/service.js b/ui-v2/app/search/filters/service.js new file mode 100644 index 0000000000..c1fcc0daaf --- /dev/null +++ b/ui-v2/app/search/filters/service.js @@ -0,0 +1,14 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const term = s.toLowerCase(); + return ( + get(item, 'Name') + .toLowerCase() + .indexOf(term) !== -1 || + (get(item, 'Tags') || []).some(function(item) { + return item.toLowerCase().indexOf(term) !== -1; + }) + ); + }); +} diff --git a/ui-v2/app/search/filters/service/node.js b/ui-v2/app/search/filters/service/node.js new file mode 100644 index 0000000000..5f12724218 --- /dev/null +++ b/ui-v2/app/search/filters/service/node.js @@ -0,0 +1,14 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const term = s.toLowerCase(); + return ( + get(item, 'Node.Node') + .toLowerCase() + .indexOf(term) !== -1 || + get(item, 'Service.ID') + .toLowerCase() + .indexOf(term) !== -1 + ); + }); +} diff --git a/ui-v2/app/search/filters/token.js b/ui-v2/app/search/filters/token.js new file mode 100644 index 0000000000..f8f5f1c3ef --- /dev/null +++ b/ui-v2/app/search/filters/token.js @@ -0,0 +1,21 @@ +import { get } from '@ember/object'; +export default function(filterable) { + return filterable(function(item, { s = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'AccessorID') + .toLowerCase() + .indexOf(sLower) !== -1 || + // TODO: Check if Name can go, it was just for legacy + get(item, 'Name') + .toLowerCase() + .indexOf(sLower) !== -1 || + get(item, 'Description') + .toLowerCase() + .indexOf(sLower) !== -1 || + (get(item, 'Policies') || []).some(function(item) { + return item.Name.toLowerCase().indexOf(sLower) !== -1; + }) + ); + }); +} diff --git a/ui-v2/app/services/dom.js b/ui-v2/app/services/dom.js index 4c59831da5..6b84f21035 100644 --- a/ui-v2/app/services/dom.js +++ b/ui-v2/app/services/dom.js @@ -6,6 +6,7 @@ import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; // TODO: Move to utils/dom import getComponentFactory from 'consul-ui/utils/get-component-factory'; import normalizeEvent from 'consul-ui/utils/dom/normalize-event'; +import createListeners from 'consul-ui/utils/dom/create-listeners'; // ember-eslint doesn't like you using a single $ so use double // use $_ for components @@ -20,6 +21,7 @@ export default Service.extend({ normalizeEvent: function() { return normalizeEvent(...arguments); }, + listeners: createListeners, root: function() { return get(this, 'doc').documentElement; }, diff --git a/ui-v2/app/services/search.js b/ui-v2/app/services/search.js new file mode 100644 index 0000000000..5a1c491cc1 --- /dev/null +++ b/ui-v2/app/services/search.js @@ -0,0 +1,2 @@ +import Service from '@ember/service'; +export default Service.extend({}); diff --git a/ui-v2/app/templates/components/catalog-filter.hbs b/ui-v2/app/templates/components/catalog-filter.hbs index d69ecbad0a..9d95acc258 100644 --- a/ui-v2/app/templates/components/catalog-filter.hbs +++ b/ui-v2/app/templates/components/catalog-filter.hbs @@ -1,4 +1,4 @@ {{!
}} - {{freetext-filter value=search placeholder="Search by name" onchange=(action onchange)}} + {{freetext-filter searchable=searchable value=search placeholder="Search by name"}} {{radio-group name="status" value=status items=filters onchange=(action onchange)}} {{!
}} diff --git a/ui-v2/app/templates/components/changeable-set.hbs b/ui-v2/app/templates/components/changeable-set.hbs new file mode 100644 index 0000000000..c074009d5f --- /dev/null +++ b/ui-v2/app/templates/components/changeable-set.hbs @@ -0,0 +1,6 @@ +{{yield}} +{{#if (gt items.length 0)}} + {{#yield-slot 'set' (block-params items)}}{{yield}}{{/yield-slot}} +{{else}} + {{#yield-slot 'empty'}}{{yield}}{{/yield-slot}} +{{/if}} \ No newline at end of file diff --git a/ui-v2/app/templates/components/intention-filter.hbs b/ui-v2/app/templates/components/intention-filter.hbs index 39b815e44e..cb7c999171 100644 --- a/ui-v2/app/templates/components/intention-filter.hbs +++ b/ui-v2/app/templates/components/intention-filter.hbs @@ -1,4 +1,4 @@ {{!
}} - {{freetext-filter onchange=(action onchange) value=search placeholder="Search by Source or Destination"}} + {{freetext-filter searchable=searchable value=search placeholder="Search by Source or Destination"}} {{radio-group name="action" value=action items=filters onchange=(action onchange)}} {{!
}} \ No newline at end of file diff --git a/ui-v2/app/templates/dc/acls/policies/index.hbs b/ui-v2/app/templates/dc/acls/policies/index.hbs index 5d1e4d8718..0f5aac4824 100644 --- a/ui-v2/app/templates/dc/acls/policies/index.hbs +++ b/ui-v2/app/templates/dc/acls/policies/index.hbs @@ -22,60 +22,63 @@ {{#block-slot 'content'}} {{#if (gt items.length 0) }}
- {{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search"}} + {{freetext-filter searchable=searchable value=s placeholder="Search"}}
{{/if}} -{{#if (gt filtered.length 0)}} - {{#tabular-collection - items=(sort-by 'CreateIndex:desc' 'Name:asc' filtered) as |item index| - }} - {{#block-slot 'header'}} - Name - Datacenters - Description - {{/block-slot}} - {{#block-slot 'row' }} - - {{item.Name}} - - - {{join ', ' (policy/datacenters item)}} - - - {{item.Description}} - - {{/block-slot}} - {{#block-slot 'actions' as |index change checked|}} - {{#confirmation-dialog confirming=false index=index message="Are you sure you want to delete this Policy?"}} - {{#block-slot 'action' as |confirm|}} - {{#action-group index=index onchange=(action change) checked=(if (eq checked index) 'checked')}} - + {{/action-group}} + {{/block-slot}} + {{#block-slot 'dialog' as |execute cancel message name|}} + {{delete-confirmation message=message execute=execute cancel=cancel}} + {{/block-slot}} + {{/confirmation-dialog}} + {{/block-slot}} + {{/tabular-collection}} + {{/block-slot}} + {{#block-slot 'empty'}} +

+ There are no Policies. +

+ {{/block-slot}} + {{/changeable-set}} {{/block-slot}} {{/app-view}} \ No newline at end of file diff --git a/ui-v2/app/templates/dc/acls/tokens/index.hbs b/ui-v2/app/templates/dc/acls/tokens/index.hbs index 4dad5c7e92..16eb04b9f8 100644 --- a/ui-v2/app/templates/dc/acls/tokens/index.hbs +++ b/ui-v2/app/templates/dc/acls/tokens/index.hbs @@ -1,131 +1,133 @@ {{#app-view class=(concat 'token ' (if (and isEnabled (not isAuthorized)) 'edit' 'list')) loading=isLoading authorized=isAuthorized enabled=isEnabled}} - {{#block-slot 'notification' as |status type subject|}} - {{partial 'dc/acls/tokens/notifications'}} - {{/block-slot}} - {{#block-slot 'header'}} -

- Access Controls -

- {{#if isAuthorized }} - {{partial 'dc/acls/nav'}} - {{/if}} - {{/block-slot}} - {{#block-slot 'disabled'}} - {{partial 'dc/acls/disabled'}} - {{/block-slot}} - {{#block-slot 'authorization'}} - {{partial 'dc/acls/authorization'}} - {{/block-slot}} - {{#block-slot 'actions'}} - Create - {{/block-slot}} - {{#block-slot 'content'}} -{{#if (gt items.length 0) }} -
- {{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search"}} -
+ {{#block-slot 'notification' as |status type subject|}} + {{partial 'dc/acls/tokens/notifications'}} + {{/block-slot}} + {{#block-slot 'header'}} +

+ Access Controls +

+{{#if isAuthorized }} + {{partial 'dc/acls/nav'}} {{/if}} - {{#if (token/is-legacy items)}} + {{/block-slot}} + {{#block-slot 'disabled'}} + {{partial 'dc/acls/disabled'}} + {{/block-slot}} + {{#block-slot 'authorization'}} + {{partial 'dc/acls/authorization'}} + {{/block-slot}} + {{#block-slot 'actions'}} + Create + {{/block-slot}} + {{#block-slot 'content'}} +{{#if (gt items.length 0) }} +
+ {{freetext-filter searchable=searchable value=s placeholder="Search"}} +
+{{/if}} +{{#if (token/is-legacy items)}}

Update. We have upgraded our ACL System to allow the creation of reusable policies that can be applied to tokens. Read more about the changes and how to upgrade legacy tokens in our documentation.

- {{/if}} -{{#if (gt filtered.length 0)}} +{{/if}} + {{#changeable-set dispatcher=searchable}} + {{#block-slot 'set' as |filtered|}} {{#tabular-collection items=(sort-by 'CreateTime:desc' filtered) as |item index| }} - {{#block-slot 'header'}} - Accessor ID - Scope - Description - Policies -   - {{/block-slot}} - {{#block-slot 'row'}} - - {{truncate item.AccessorID 8 false}} - - - {{if item.Local 'local' 'global' }} - - - {{default item.Description item.Name}} - - - {{#if (token/is-legacy item) }} - Legacy tokens have embedded policies. - {{ else }} - {{#each item.Policies as |item|}} - {{item.Name}} - {{/each}} - {{/if}} - - {{#if (eq item.AccessorID token.AccessorID)}} - Your token - {{/if}} - {{/block-slot}} - {{#block-slot 'actions' as |index change checked|}} - {{#confirmation-dialog confirming=false index=index message="Are you sure you want to delete this Token?"}} - {{#block-slot 'action' as |confirm|}} - {{#action-group index=index onchange=(action change) checked=(if (eq checked index) 'checked')}} - + {{/action-group}} + {{/block-slot}} + {{#block-slot 'dialog' as |execute cancel message name|}} +

+ {{#if (eq name 'delete')}} + {{message}} + {{else if (eq name 'logout')}} + Are you sure you want to stop using this ACL token? This will log you out. + {{else if (eq name 'use')}} + Are you sure you want to use this ACL token? + {{/if}} +

+ + + {{/block-slot}} + {{/confirmation-dialog}} + {{/block-slot}} {{/tabular-collection}} -{{else}} + {{/block-slot}} + {{#block-slot 'empty'}}

- There are no Tokens. + There are no Tokens.

-{{/if}} - {{/block-slot}} + {{/block-slot}} + {{/changeable-set}} + {{/block-slot}} {{/app-view}} diff --git a/ui-v2/app/templates/dc/intentions/index.hbs b/ui-v2/app/templates/dc/intentions/index.hbs index bd509d85a8..ed103d92d2 100644 --- a/ui-v2/app/templates/dc/intentions/index.hbs +++ b/ui-v2/app/templates/dc/intentions/index.hbs @@ -13,70 +13,73 @@ {{/block-slot}} {{#block-slot 'toolbar'}} {{#if (gt items.length 0) }} - {{intention-filter filters=actionFilters search=filters.s type=filters.action onchange=(action 'filter')}} + {{intention-filter searchable=searchable filters=actionFilters search=filters.s type=filters.action onchange=(action 'filter')}} {{/if}} {{/block-slot}} {{#block-slot 'content'}} -{{#if (gt filtered.length 0) }} - {{#tabular-collection - route='dc.intentions.edit' - key='SourceName' - items=filtered as |item index| - }} - {{#block-slot 'header'}} - Source -   - Destination - Precedence - {{/block-slot}} - {{#block-slot 'row'}} - - - {{#if (eq item.SourceName '*') }} - All Services (*) - {{else}} - {{item.SourceName}} - {{/if}} - - - - {{item.Action}} - - - {{#if (eq item.DestinationName '*') }} - All Services (*) - {{else}} - {{item.DestinationName}} - {{/if}} - - - {{item.Precedence}} - - {{/block-slot}} - {{#block-slot 'actions' as |index change checked|}} - {{#confirmation-dialog confirming=false index=index message='Are you sure you want to delete this intention?'}} - {{#block-slot 'action' as |confirm|}} - {{#action-group index=index onchange=(action change) checked=(if (eq checked index) 'checked')}} - - {{/action-group}} - {{/block-slot}} - {{#block-slot 'dialog' as |execute cancel message|}} - {{delete-confirmation message=message execute=execute cancel=cancel}} - {{/block-slot}} - {{/confirmation-dialog}} - {{/block-slot}} - {{/tabular-collection}} -{{else}} -

- There are no intentions. -

-{{/if}} + {{#changeable-set dispatcher=searchable}} + {{#block-slot 'set' as |filtered|}} + {{#tabular-collection + route='dc.intentions.edit' + key='SourceName' + items=filtered as |item index| + }} + {{#block-slot 'header'}} + Source +   + Destination + Precedence + {{/block-slot}} + {{#block-slot 'row'}} + + + {{#if (eq item.SourceName '*') }} + All Services (*) + {{else}} + {{item.SourceName}} + {{/if}} + + + + {{item.Action}} + + + {{#if (eq item.DestinationName '*') }} + All Services (*) + {{else}} + {{item.DestinationName}} + {{/if}} + + + {{item.Precedence}} + + {{/block-slot}} + {{#block-slot 'actions' as |index change checked|}} + {{#confirmation-dialog confirming=false index=index message='Are you sure you want to delete this intention?'}} + {{#block-slot 'action' as |confirm|}} + {{#action-group index=index onchange=(action change) checked=(if (eq checked index) 'checked')}} + + {{/action-group}} + {{/block-slot}} + {{#block-slot 'dialog' as |execute cancel message|}} + {{delete-confirmation message=message execute=execute cancel=cancel}} + {{/block-slot}} + {{/confirmation-dialog}} + {{/block-slot}} + {{/tabular-collection}} + {{/block-slot}} + {{#block-slot 'empty'}} +

+ There are no intentions. +

+ {{/block-slot}} + {{/changeable-set}} {{/block-slot}} {{/app-view}} \ No newline at end of file diff --git a/ui-v2/app/templates/dc/kv/index.hbs b/ui-v2/app/templates/dc/kv/index.hbs index ecb6ac2ef7..a9d10cd824 100644 --- a/ui-v2/app/templates/dc/kv/index.hbs +++ b/ui-v2/app/templates/dc/kv/index.hbs @@ -25,7 +25,7 @@ {{#block-slot 'toolbar'}} {{#if (gt items.length 0) }}
- {{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search by name"}} + {{freetext-filter searchable=searchable value=s placeholder="Search by name"}}
{{/if}} {{/block-slot}} @@ -37,40 +37,45 @@ {{/if}} {{/block-slot}} {{#block-slot 'content'}} -{{#if (gt filtered.length 0)}} - {{#tabular-collection - items=(sort-by 'isFolder:desc' 'Key:asc' filtered) as |item index| - }} - {{#block-slot 'header'}} - Name - {{/block-slot}} - {{#block-slot 'row'}} - - {{right-trim (left-trim item.Key parent.Key) '/'}} - - {{/block-slot}} - {{#block-slot 'actions' as |index change checked|}} - {{#confirmation-dialog confirming=false index=index message='Are you sure you want to delete this key?'}} - {{#block-slot 'action' as |confirm|}} - {{#action-group index=index onchange=(action change) checked=(if (eq checked index) 'checked')}} - - {{/action-group}} - {{/block-slot}} - {{#block-slot 'dialog' as |execute cancel message|}} - {{delete-confirmation message=message execute=execute cancel=cancel}} - {{/block-slot}} - {{/confirmation-dialog}} - {{/block-slot}} - {{/tabular-collection}} -{{else}} -

There are no Key / Value pairs.

-{{/if}} + {{#changeable-set dispatcher=searchable}} + {{#block-slot 'set' as |filtered|}} + {{#tabular-collection + items=(sort-by 'isFolder:desc' 'Key:asc' filtered) as |item index| + }} + {{#block-slot 'header'}} + Name + {{/block-slot}} + {{#block-slot 'row'}} + + {{right-trim (left-trim item.Key parent.Key) '/'}} + + {{/block-slot}} + {{#block-slot 'actions' as |index change checked|}} + {{#confirmation-dialog confirming=false index=index message='Are you sure you want to delete this key?'}} + {{#block-slot 'action' as |confirm|}} + {{#action-group index=index onchange=(action change) checked=(if (eq checked index) 'checked')}} + + {{/action-group}} + {{/block-slot}} + {{#block-slot 'dialog' as |execute cancel message|}} + {{delete-confirmation message=message execute=execute cancel=cancel}} + {{/block-slot}} + {{/confirmation-dialog}} + {{/block-slot}} + {{/tabular-collection}} + {{/block-slot}} + {{#block-slot 'empty'}} +

+ There are no Key / Value pairs. +

+ {{/block-slot}} + {{/changeable-set}} {{/block-slot}} {{/app-view}} \ No newline at end of file diff --git a/ui-v2/app/templates/dc/nodes/-services.hbs b/ui-v2/app/templates/dc/nodes/-services.hbs index cf809393bd..856e51b172 100644 --- a/ui-v2/app/templates/dc/nodes/-services.hbs +++ b/ui-v2/app/templates/dc/nodes/-services.hbs @@ -1,40 +1,43 @@ {{#if (gt items.length 0) }} - -
- {{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search by name/port"}} -
+ +
+ {{freetext-filter searchable=searchable value=s placeholder="Search by name/port"}} +
{{/if}} -{{#if (gt filtered.length 0)}} - {{#tabular-collection - data-test-services - items=filtered as |item index| - }} - {{#block-slot 'header'}} - Service - Port - Tags - {{/block-slot}} - {{#block-slot 'row'}} - - - - {{item.Service}}{{#if (not-eq item.ID item.Service) }}({{item.ID}}){{/if}} - - - - {{item.Port}} - - - {{#if (gt item.Tags.length 0)}} - {{#each item.Tags as |item|}} - {{item}} - {{/each}} - {{/if}} - - {{/block-slot}} - {{/tabular-collection}} -{{else}} -

+ {{#changeable-set dispatcher=searchable}} + {{#block-slot 'set' as |filtered|}} + {{#tabular-collection + data-test-services + items=filtered as |item index| + }} + {{#block-slot 'header'}} + Service + Port + Tags + {{/block-slot}} + {{#block-slot 'row'}} + + + + {{item.Service}}{{#if (not-eq item.ID item.Service) }}({{item.ID}}){{/if}} + + + + {{item.Port}} + + + {{#if (gt item.Tags.length 0)}} + {{#each item.Tags as |item|}} + {{item}} + {{/each}} + {{/if}} + + {{/block-slot}} + {{/tabular-collection}} + {{/block-slot}} + {{#block-slot 'empty'}} +

There are no services. -

-{{/if}} +

+ {{/block-slot}} + {{/changeable-set}} diff --git a/ui-v2/app/templates/dc/nodes/index.hbs b/ui-v2/app/templates/dc/nodes/index.hbs index b49368aef4..0d9ca57ed4 100644 --- a/ui-v2/app/templates/dc/nodes/index.hbs +++ b/ui-v2/app/templates/dc/nodes/index.hbs @@ -7,7 +7,7 @@ {{/block-slot}} {{#block-slot 'toolbar'}} {{#if (gt items.length 0) }} - {{catalog-filter filters=healthFilters search=filters.s status=filters.status onchange=(action 'filter')}} + {{catalog-filter searchable=(array searchableHealthy searchableUnhealthy) filters=healthFilters search=s status=filters.status onchange=(action 'filter')}} {{/if}} {{/block-slot}} {{#block-slot 'content'}} @@ -16,33 +16,51 @@

Unhealthy Nodes

{{! think about 2 differing views here }} -
    - {{#each unhealthy as |item|}} - {{healthchecked-resource - tagName='li' - data-test-node=item.Node - href=(href-to 'dc.nodes.show' item.Node) - name=item.Node - address=item.Address - checks=item.Checks - }} - {{/each}} -
+
{{/if}} {{#if (gt healthy.length 0) }}

Healthy Nodes

- {{#list-collection cellHeight=92 items=healthy as |item index|}} - {{healthchecked-resource - data-test-node=item.Node - href=(href-to 'dc.nodes.show' item.Node) - name=item.Node - address=item.Address - status=item.Checks.[0].Status - }} - {{/list-collection}} + {{#changeable-set dispatcher=searchableHealthy}} + {{#block-slot 'set' as |healthy|}} + {{#list-collection cellHeight=92 items=healthy as |item index|}} + {{healthchecked-resource + data-test-node=item.Node + href=(href-to 'dc.nodes.show' item.Node) + name=item.Node + address=item.Address + status=item.Checks.[0].Status + }} + {{/list-collection}} + {{/block-slot}} + {{#block-slot 'empty'}} +

+ There are no healthy nodes for that search. +

+ {{/block-slot}} + {{/changeable-set}}
{{/if}} {{#if (and (eq healthy.length 0) (eq unhealthy.length 0)) }} diff --git a/ui-v2/app/templates/dc/services/index.hbs b/ui-v2/app/templates/dc/services/index.hbs index f3029d046f..aee20a9718 100644 --- a/ui-v2/app/templates/dc/services/index.hbs +++ b/ui-v2/app/templates/dc/services/index.hbs @@ -11,57 +11,59 @@ {{/block-slot}} {{#block-slot 'toolbar'}} {{#if (gt items.length 0) }} - {{catalog-filter filters=healthFilters search=filters.s status=filters.status onchange=(action 'filter')}} + {{catalog-filter searchable=searchable filters=healthFilters search=filters.s status=filters.status onchange=(action 'filter')}} {{/if}} {{/block-slot}} {{#block-slot 'content'}} -{{#if (gt filtered.length 0) }} - {{#tabular-collection - route='dc.services.show' - key='Name' - items=filtered as |item index| - }} - {{#block-slot 'header'}} - Service - Health ChecksThe number of health checks for the service on all nodes - Tags - {{/block-slot}} - {{#block-slot 'row'}} - - - - - {{item.Name}} - - - -{{#if (and (lt item.ChecksPassing 1) (lt item.ChecksWarning 1) (lt item.ChecksCritical 1) )}} - 0 -{{else}} -
-
Healthchecks Passing
-
{{format-number item.ChecksPassing}}
-
Healthchecks Warning
-
{{format-number item.ChecksWarning}}
-
Healthchecks Critical
-
{{format-number item.ChecksCritical}}
-
-{{/if}} - - - {{#if (gt item.Tags.length 0)}} - {{#each item.Tags as |item|}} - {{item}} - {{/each}} - {{/if}} - - {{/block-slot}} - {{/tabular-collection}} -{{else}} -

- There are no services. -

-{{/if}} + {{#changeable-set dispatcher=searchable}} + {{#block-slot 'set' as |filtered|}} + {{#tabular-collection + route='dc.services.show' + key='Name' + items=filtered as |item index| + }} + {{#block-slot 'header'}} + Service + Health ChecksThe number of health checks for the service on all nodes + Tags + {{/block-slot}} + {{#block-slot 'row'}} + + + + {{item.Name}} + + + + {{#if (and (lt item.ChecksPassing 1) (lt item.ChecksWarning 1) (lt item.ChecksCritical 1) )}} + 0 + {{else}} +
+
Healthchecks Passing
+
{{format-number item.ChecksPassing}}
+
Healthchecks Warning
+
{{format-number item.ChecksWarning}}
+
Healthchecks Critical
+
{{format-number item.ChecksCritical}}
+
+ {{/if}} + + + {{#if (gt item.Tags.length 0)}} + {{#each item.Tags as |item|}} + {{item}} + {{/each}} + {{/if}} + + {{/block-slot}} + {{/tabular-collection}} + {{/block-slot}} + {{#block-slot 'empty'}} +

+ There are no services. +

+ {{/block-slot}} + {{/changeable-set}} {{/block-slot}} {{/app-view}} diff --git a/ui-v2/app/templates/dc/services/show.hbs b/ui-v2/app/templates/dc/services/show.hbs index 130c18ce80..5a75f0055f 100644 --- a/ui-v2/app/templates/dc/services/show.hbs +++ b/ui-v2/app/templates/dc/services/show.hbs @@ -18,7 +18,7 @@ {{/block-slot}} {{#block-slot 'toolbar'}} {{#if (gt items.length 0) }} - {{catalog-filter filters=healthFilters search=filters.s status=filters.status onchange=(action 'filter')}} + {{catalog-filter searchable=(array searchableHealthy searchableUnhealthy) filters=healthFilters search=s status=filters.status onchange=(action 'filter')}} {{/if}} {{/block-slot}} {{#block-slot 'content'}} @@ -37,17 +37,26 @@

Unhealthy Nodes

@@ -55,17 +64,26 @@ {{#if (gt healthy.length 0) }}

Healthy Nodes

- {{#list-collection cellHeight=113 items=healthy as |item index|}} - {{healthchecked-resource - href=(href-to 'dc.nodes.show' item.Node.Node) - data-test-node=item.Node.Node - name=item.Node.Node - service=item.Service.ID - address=(concat (default item.Service.Address item.Node.Address) ':' item.Service.Port) - checks=item.Checks - status=item.Checks.[0].Status - }} - {{/list-collection}} + {{#changeable-set dispatcher=searchableHealthy}} + {{#block-slot 'set' as |healthy|}} + {{#list-collection cellHeight=113 items=healthy as |item index|}} + {{healthchecked-resource + href=(href-to 'dc.nodes.show' item.Node.Node) + data-test-node=item.Node.Node + name=item.Node.Node + service=item.Service.ID + address=(concat (default item.Service.Address item.Node.Address) ':' item.Service.Port) + checks=item.Checks + status=item.Checks.[0].Status + }} + {{/list-collection}} + {{/block-slot}} + {{#block-slot 'empty'}} +

+ There are no healthy nodes for that search. +

+ {{/block-slot}} + {{/changeable-set}}
{{/if}} {{/block-slot}} diff --git a/ui-v2/app/utils/dom/create-listeners.js b/ui-v2/app/utils/dom/create-listeners.js new file mode 100644 index 0000000000..623046198f --- /dev/null +++ b/ui-v2/app/utils/dom/create-listeners.js @@ -0,0 +1,32 @@ +export default function(listeners = []) { + const add = function(target, event, handler) { + let addEventListener = 'addEventListener'; + let removeEventListener = 'removeEventListener'; + if (typeof target[addEventListener] === 'undefined') { + addEventListener = 'on'; + removeEventListener = 'off'; + } + target[addEventListener](event, handler); + const remove = function() { + target[removeEventListener](event, handler); + return handler; + }; + listeners.push(remove); + return remove; + }; + // TODO: Allow passing of a 'listener remove' in here + // call it, find in the array and remove + // Post-thoughts, pretty sure this is covered now by returning the remove + // function above, use-case for wanting to use this method to remove individual + // listeners is probably pretty limited, this method itself could be easily implemented + // from the outside also, but I suppose its handy to keep here + const remove = function() { + const handlers = listeners.map(item => item()); + listeners.splice(0, listeners.length); + return handlers; + }; + return { + add: add, + remove: remove, + }; +} diff --git a/ui-v2/app/utils/search/filterable.js b/ui-v2/app/utils/search/filterable.js new file mode 100644 index 0000000000..f14d0139ed --- /dev/null +++ b/ui-v2/app/utils/search/filterable.js @@ -0,0 +1,38 @@ +import RSVP, { Promise } from 'rsvp'; +export default function(EventTarget = RSVP.EventTarget, P = Promise) { + // TODO: Class-ify + return function(filter) { + return EventTarget.mixin({ + value: '', + add: function(data) { + this.data = data; + return this; + }, + search: function(term = '') { + this.value = term === null ? '' : term.trim(); + // specifically no return here we return `this` instead + // right now filtering is sync but we introduce an async + // flow now for later on + P.resolve( + this.value !== '' + ? this.data.filter(item => { + return filter(item, { s: term }); + }) + : this.data + ).then(data => { + // TODO: For the moment, lets just fake a target + this.trigger('change', { + target: { + value: this.value, + // TODO: selectedOptions is what