From 65ef2969c7baa40bdd7d1641d8cbaf5bf7c6d9e7 Mon Sep 17 00:00:00 2001
From: John Cowen
Date: Tue, 6 Nov 2018 09:10:20 +0000
Subject: [PATCH] ui: Async Search (#4859)
This does several things to make improving the search experience easier
moving forwards:
1. Separate searching off from filtering. 'Searching' can be thought of
as specifically 'text searching' whilst filtering is more of a
boolean/flag search.
2. Decouple the actual searching functionality to almost pure,
isolated / unit testable units and unit test. (I still import embers get
which, once I upgrade to 3.5, I shouldn't need)
3. Searching rules are now configurable from the outside, i.e. not
wrapped in Controllers or Components.
4. General searching itself now can use an asynchronous approach based on
events. This prepares for future possibilities of handing off the
searching to a web worker or elsewhere, which should aid in large scale
searching and prepares the way for other searching methods.
5. Adds the possibility of have multiple searches in one
template/route/page.
Additionally, this adds a WithSearching mixin to aid linking the
searching to ember in an ember-like way in a single place. Plus a
WithListeners mixin to aid with cleaning up of event listeners on
Controller/Component destruction.
Post-initial work I slightly changed the API of create listeners:
Returning the handler from a `remover` means you can re-add it again if you
want to, this avoids having to save a reference to the handler elsewhere
to do the same.
The `remove` method itself now returns an array of handlers, again you
might want to use these again or something, and its also more useful
then just returning an empty array.
The more I look at this the more I doubt that you'll ever use `remove`
to remove individual handlers, you may aswell just use the `remover`
returned from add. I've added some comments to reflect this, but they'll
likely be removed once I'm absolutely sure of this.
I also added some comments for WithSearching to explain possible further
work re: moving `searchParams` so it can be `hung` off the
controller object
---
ui-v2/app/components/changeable-set.js | 19 ++
ui-v2/app/components/freetext-filter.js | 12 +-
.../app/controllers/dc/acls/policies/index.js | 26 +-
ui-v2/app/controllers/dc/acls/tokens/index.js | 32 +--
ui-v2/app/controllers/dc/intentions/index.js | 26 +-
ui-v2/app/controllers/dc/kv/index.js | 22 +-
ui-v2/app/controllers/dc/nodes/index.js | 24 +-
ui-v2/app/controllers/dc/nodes/show.js | 35 ++-
ui-v2/app/controllers/dc/services/index.js | 25 +-
ui-v2/app/controllers/dc/services/show.js | 29 ++-
ui-v2/app/initializers/search.js | 37 +++
ui-v2/app/mixins/with-listeners.js | 27 ++
ui-v2/app/mixins/with-searching.js | 32 +++
ui-v2/app/search/filters/intention.js | 15 ++
ui-v2/app/search/filters/kv.js | 10 +
ui-v2/app/search/filters/node.js | 11 +
ui-v2/app/search/filters/node/service.js | 21 ++
ui-v2/app/search/filters/policy.js | 14 ++
ui-v2/app/search/filters/service.js | 14 ++
ui-v2/app/search/filters/service/node.js | 14 ++
ui-v2/app/search/filters/token.js | 21 ++
ui-v2/app/services/dom.js | 2 +
ui-v2/app/services/search.js | 2 +
.../templates/components/catalog-filter.hbs | 2 +-
.../templates/components/changeable-set.hbs | 6 +
.../templates/components/intention-filter.hbs | 2 +-
.../app/templates/dc/acls/policies/index.hbs | 107 ++++----
ui-v2/app/templates/dc/acls/tokens/index.hbs | 230 +++++++++---------
ui-v2/app/templates/dc/intentions/index.hbs | 127 +++++-----
ui-v2/app/templates/dc/kv/index.hbs | 77 +++---
ui-v2/app/templates/dc/nodes/-services.hbs | 77 +++---
ui-v2/app/templates/dc/nodes/index.hbs | 62 +++--
ui-v2/app/templates/dc/services/index.hbs | 98 ++++----
ui-v2/app/templates/dc/services/show.hbs | 60 +++--
ui-v2/app/utils/dom/create-listeners.js | 32 +++
ui-v2/app/utils/search/filterable.js | 38 +++
.../components/changeable-set-test.js | 33 +++
.../dc/acls/policies/create-test.js | 2 +-
.../dc/acls/policies/index-test.js | 2 +-
.../controllers/dc/acls/tokens/index-test.js | 2 +-
.../controllers/dc/intentions/index-test.js | 2 +-
.../unit/controllers/dc/kv/folder-test.js | 2 +-
.../unit/controllers/dc/kv/index-test.js | 2 +-
.../unit/controllers/dc/nodes/index-test.js | 2 +-
.../unit/controllers/dc/nodes/show-test.js | 2 +-
.../controllers/dc/services/index-test.js | 2 +-
.../unit/controllers/dc/services/show-test.js | 2 +-
.../tests/unit/mixins/with-listeners-test.js | 21 ++
.../tests/unit/mixins/with-searching-test.js | 21 ++
.../unit/search/filters/intention-test.js | 72 ++++++
ui-v2/tests/unit/search/filters/kv-test.js | 36 +++
ui-v2/tests/unit/search/filters/node-test.js | 30 +++
.../unit/search/filters/node/service-test.js | 94 +++++++
.../tests/unit/search/filters/policy-test.js | 36 +++
.../tests/unit/search/filters/service-test.js | 59 +++++
.../unit/search/filters/service/node-test.js | 56 +++++
ui-v2/tests/unit/search/filters/token-test.js | 86 +++++++
ui-v2/tests/unit/services/search-test.js | 12 +
.../unit/utils/dom/create-listeners-test.js | 79 ++++++
.../unit/utils/search/filterable-test.js | 10 +
60 files changed, 1543 insertions(+), 510 deletions(-)
create mode 100644 ui-v2/app/components/changeable-set.js
create mode 100644 ui-v2/app/initializers/search.js
create mode 100644 ui-v2/app/mixins/with-listeners.js
create mode 100644 ui-v2/app/mixins/with-searching.js
create mode 100644 ui-v2/app/search/filters/intention.js
create mode 100644 ui-v2/app/search/filters/kv.js
create mode 100644 ui-v2/app/search/filters/node.js
create mode 100644 ui-v2/app/search/filters/node/service.js
create mode 100644 ui-v2/app/search/filters/policy.js
create mode 100644 ui-v2/app/search/filters/service.js
create mode 100644 ui-v2/app/search/filters/service/node.js
create mode 100644 ui-v2/app/search/filters/token.js
create mode 100644 ui-v2/app/services/search.js
create mode 100644 ui-v2/app/templates/components/changeable-set.hbs
create mode 100644 ui-v2/app/utils/dom/create-listeners.js
create mode 100644 ui-v2/app/utils/search/filterable.js
create mode 100644 ui-v2/tests/integration/components/changeable-set-test.js
create mode 100644 ui-v2/tests/unit/mixins/with-listeners-test.js
create mode 100644 ui-v2/tests/unit/mixins/with-searching-test.js
create mode 100644 ui-v2/tests/unit/search/filters/intention-test.js
create mode 100644 ui-v2/tests/unit/search/filters/kv-test.js
create mode 100644 ui-v2/tests/unit/search/filters/node-test.js
create mode 100644 ui-v2/tests/unit/search/filters/node/service-test.js
create mode 100644 ui-v2/tests/unit/search/filters/policy-test.js
create mode 100644 ui-v2/tests/unit/search/filters/service-test.js
create mode 100644 ui-v2/tests/unit/search/filters/service/node-test.js
create mode 100644 ui-v2/tests/unit/search/filters/token-test.js
create mode 100644 ui-v2/tests/unit/services/search-test.js
create mode 100644 ui-v2/tests/unit/utils/dom/create-listeners-test.js
create mode 100644 ui-v2/tests/unit/utils/search/filterable-test.js
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 @@
{{!}}
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 @@
{{!}}
\ 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) }}
{{/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')}}
-
-{{#if (policy/is-management item)}}
-
- View
-
-{{else}}
+ {{#changeable-set dispatcher=searchable}}
+ {{#block-slot 'set' as |filtered|}}
+ {{#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')}}
+
+ {{#if (policy/is-management item)}}
+
+ View
+
+ {{else}}
-
- Edit
-
-
- Delete
-
-{{/if}}
-
- {{/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}}
-{{else}}
-
- There are no Policies.
-
-{{/if}}
+
+ Edit
+
+
+ Delete
+
+ {{/if}}
+
+ {{/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) }}
-
+ {{#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) }}
+
+{{/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')}}
-
- {{#if false}}
-
- {{#copy-button-feedback title="Copy AccessorID to the clipboard" copy=item.AccessorID name="AccessorID"}}Copy AccessorID{{/copy-button-feedback}}
-
- {{/if}}
-
- Edit
-
+ {{#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')}}
+
+ {{#if false}}
+
+ {{#copy-button-feedback title="Copy AccessorID to the clipboard" copy=item.AccessorID name="AccessorID"}}Copy AccessorID{{/copy-button-feedback}}
+
+ {{/if}}
+
+ Edit
+
{{#if (not (token/is-legacy item))}}
-
- Duplicate
-
+
+ Duplicate
+
{{/if}}
{{#if (eq item.AccessorID token.AccessorID) }}
-
- Stop using
-
+
+ Stop using
+
{{else}}
-
-
- Use
-
+
+ Use
+
{{/if}}
{{#unless (or (token/is-anonymous item) (eq item.AccessorID token.AccessorID)) }}
-
- Delete
-
+
+ Delete
+
{{/unless}}
-
- {{/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}}
-
-
- {{#if (eq name 'delete')}}
- Confirm Delete
- {{else if (eq name 'logout')}}
- Confirm Logout
- {{ else if (eq name 'use')}}
- Confirm Use
- {{/if}}
-
- Cancel
- {{/block-slot}}
- {{/confirmation-dialog}}
- {{/block-slot}}
+
+ {{/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}}
+
+
+ {{#if (eq name 'delete')}}
+ Confirm Delete
+ {{else if (eq name 'logout')}}
+ Confirm Logout
+ {{ else if (eq name 'use')}}
+ Confirm Use
+ {{/if}}
+
+ Cancel
+ {{/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) }}
{{/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) }}
-
-
+
+
{{/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 uses, consider that
+ data: data,
+ },
+ });
+ // not returned
+ return data;
+ });
+ return this;
+ },
+ });
+ };
+}
diff --git a/ui-v2/tests/integration/components/changeable-set-test.js b/ui-v2/tests/integration/components/changeable-set-test.js
new file mode 100644
index 0000000000..e495809a8c
--- /dev/null
+++ b/ui-v2/tests/integration/components/changeable-set-test.js
@@ -0,0 +1,33 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('changeable-set', 'Integration | Component | changeable set', {
+ integration: true,
+});
+
+test('it renders', function(assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.on('myAction', function(val) { ... });
+
+ this.render(hbs`{{changeable-set}}`);
+
+ assert.equal(
+ this.$()
+ .text()
+ .trim(),
+ ''
+ );
+
+ // Template block usage:
+ this.render(hbs`
+ {{#changeable-set}}
+ {{/changeable-set}}
+ `);
+
+ assert.equal(
+ this.$()
+ .text()
+ .trim(),
+ ''
+ );
+});
diff --git a/ui-v2/tests/unit/controllers/dc/acls/policies/create-test.js b/ui-v2/tests/unit/controllers/dc/acls/policies/create-test.js
index 12f561f5e9..5df3439d87 100644
--- a/ui-v2/tests/unit/controllers/dc/acls/policies/create-test.js
+++ b/ui-v2/tests/unit/controllers/dc/acls/policies/create-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/acls/policies/create', 'Unit | Controller | dc/acls/policies/create', {
// Specify the other units that are required for this test.
- needs: ['service:dom', 'service:form'],
+ needs: ['service:form', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/controllers/dc/acls/policies/index-test.js b/ui-v2/tests/unit/controllers/dc/acls/policies/index-test.js
index fc5e3a97b3..5978228dcb 100644
--- a/ui-v2/tests/unit/controllers/dc/acls/policies/index-test.js
+++ b/ui-v2/tests/unit/controllers/dc/acls/policies/index-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/acls/policies/index', 'Unit | Controller | dc/acls/policies/index', {
// Specify the other units that are required for this test.
- // needs: ['controller:foo']
+ needs: ['service:search', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/controllers/dc/acls/tokens/index-test.js b/ui-v2/tests/unit/controllers/dc/acls/tokens/index-test.js
index 9eb384d4b1..8cd6b95d79 100644
--- a/ui-v2/tests/unit/controllers/dc/acls/tokens/index-test.js
+++ b/ui-v2/tests/unit/controllers/dc/acls/tokens/index-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/acls/tokens/index', 'Unit | Controller | dc/acls/tokens/index', {
// Specify the other units that are required for this test.
- // needs: ['controller:foo']
+ needs: ['service:search', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/controllers/dc/intentions/index-test.js b/ui-v2/tests/unit/controllers/dc/intentions/index-test.js
index d17e006b2d..56dff2ae1c 100644
--- a/ui-v2/tests/unit/controllers/dc/intentions/index-test.js
+++ b/ui-v2/tests/unit/controllers/dc/intentions/index-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/intentions/index', 'Unit | Controller | dc/intentions/index', {
// Specify the other units that are required for this test.
- // needs: ['controller:foo']
+ needs: ['service:search', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/controllers/dc/kv/folder-test.js b/ui-v2/tests/unit/controllers/dc/kv/folder-test.js
index c9edb0e623..34f4ed9a19 100644
--- a/ui-v2/tests/unit/controllers/dc/kv/folder-test.js
+++ b/ui-v2/tests/unit/controllers/dc/kv/folder-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/kv/folder', 'Unit | Controller | dc/kv/folder', {
// Specify the other units that are required for this test.
- // needs: ['controller:foo']
+ needs: ['service:search', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/controllers/dc/kv/index-test.js b/ui-v2/tests/unit/controllers/dc/kv/index-test.js
index af2dfbad18..fb811912b6 100644
--- a/ui-v2/tests/unit/controllers/dc/kv/index-test.js
+++ b/ui-v2/tests/unit/controllers/dc/kv/index-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/kv/index', 'Unit | Controller | dc/kv/index', {
// Specify the other units that are required for this test.
- // needs: ['controller:foo']
+ needs: ['service:search', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/controllers/dc/nodes/index-test.js b/ui-v2/tests/unit/controllers/dc/nodes/index-test.js
index a24ec182f2..c5a1251af6 100644
--- a/ui-v2/tests/unit/controllers/dc/nodes/index-test.js
+++ b/ui-v2/tests/unit/controllers/dc/nodes/index-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/nodes/index', 'Unit | Controller | dc/nodes/index', {
// Specify the other units that are required for this test.
- // needs: ['controller:foo']
+ needs: ['service:search', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/controllers/dc/nodes/show-test.js b/ui-v2/tests/unit/controllers/dc/nodes/show-test.js
index 12974449f7..a1dd0c5618 100644
--- a/ui-v2/tests/unit/controllers/dc/nodes/show-test.js
+++ b/ui-v2/tests/unit/controllers/dc/nodes/show-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/nodes/show', 'Unit | Controller | dc/nodes/show', {
// Specify the other units that are required for this test.
- // needs: ['controller:foo']
+ needs: ['service:search', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/controllers/dc/services/index-test.js b/ui-v2/tests/unit/controllers/dc/services/index-test.js
index c9e423e1d1..a949d17085 100644
--- a/ui-v2/tests/unit/controllers/dc/services/index-test.js
+++ b/ui-v2/tests/unit/controllers/dc/services/index-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/services/index', 'Unit | Controller | dc/services/index', {
// Specify the other units that are required for this test.
- // needs: ['controller:foo']
+ needs: ['service:search', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/controllers/dc/services/show-test.js b/ui-v2/tests/unit/controllers/dc/services/show-test.js
index 6e90df4d1c..4b4a50ef72 100644
--- a/ui-v2/tests/unit/controllers/dc/services/show-test.js
+++ b/ui-v2/tests/unit/controllers/dc/services/show-test.js
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('controller:dc/services/show', 'Unit | Controller | dc/services/show', {
// Specify the other units that are required for this test.
- // needs: ['controller:foo']
+ needs: ['service:search', 'service:dom'],
});
// Replace this with your real tests.
diff --git a/ui-v2/tests/unit/mixins/with-listeners-test.js b/ui-v2/tests/unit/mixins/with-listeners-test.js
new file mode 100644
index 0000000000..830437ba44
--- /dev/null
+++ b/ui-v2/tests/unit/mixins/with-listeners-test.js
@@ -0,0 +1,21 @@
+import { moduleFor } from 'ember-qunit';
+import test from 'ember-sinon-qunit/test-support/test';
+import { getOwner } from '@ember/application';
+import Controller from '@ember/controller';
+import Mixin from 'consul-ui/mixins/with-listeners';
+
+moduleFor('mixin:with-listeners', 'Unit | Mixin | with listeners', {
+ // Specify the other units that are required for this test.
+ needs: ['service:dom'],
+ subject: function() {
+ const MixedIn = Controller.extend(Mixin);
+ this.register('test-container:with-listeners-object', MixedIn);
+ return getOwner(this).lookup('test-container:with-listeners-object');
+ },
+});
+
+// Replace this with your real tests.
+test('it works', function(assert) {
+ const subject = this.subject();
+ assert.ok(subject);
+});
diff --git a/ui-v2/tests/unit/mixins/with-searching-test.js b/ui-v2/tests/unit/mixins/with-searching-test.js
new file mode 100644
index 0000000000..e3c550b219
--- /dev/null
+++ b/ui-v2/tests/unit/mixins/with-searching-test.js
@@ -0,0 +1,21 @@
+import { moduleFor } from 'ember-qunit';
+import test from 'ember-sinon-qunit/test-support/test';
+import { getOwner } from '@ember/application';
+import Controller from '@ember/controller';
+import Mixin from 'consul-ui/mixins/with-searching';
+
+moduleFor('mixin:with-searching', 'Unit | Mixin | with searching', {
+ // Specify the other units that are required for this test.
+ needs: ['service:search', 'service:dom'],
+ subject: function() {
+ const MixedIn = Controller.extend(Mixin);
+ this.register('test-container:with-searching-object', MixedIn);
+ return getOwner(this).lookup('test-container:with-searching-object');
+ },
+});
+
+// Replace this with your real tests.
+test('it works', function(assert) {
+ const subject = this.subject();
+ assert.ok(subject);
+});
diff --git a/ui-v2/tests/unit/search/filters/intention-test.js b/ui-v2/tests/unit/search/filters/intention-test.js
new file mode 100644
index 0000000000..3f39853226
--- /dev/null
+++ b/ui-v2/tests/unit/search/filters/intention-test.js
@@ -0,0 +1,72 @@
+import getFilter from 'consul-ui/search/filters/intention';
+import { module, test } from 'qunit';
+
+module('Unit | Search | Filter | intention');
+
+const filter = getFilter(cb => cb);
+test('items are found by properties', function(assert) {
+ [
+ {
+ SourceName: 'Hit',
+ DestinationName: 'destination',
+ },
+ {
+ SourceName: 'source',
+ DestinationName: 'hiT',
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(actual);
+ });
+});
+test('items are not found', function(assert) {
+ [
+ {
+ SourceName: 'source',
+ DestinationName: 'destination',
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: '*',
+ });
+ assert.ok(!actual);
+ });
+});
+test('items are found by *', function(assert) {
+ [
+ {
+ SourceName: '*',
+ DestinationName: 'destination',
+ },
+ {
+ SourceName: 'source',
+ DestinationName: '*',
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: '*',
+ });
+ assert.ok(actual);
+ });
+});
+test("* items are found by searching anything in 'All Services (*)'", function(assert) {
+ [
+ {
+ SourceName: '*',
+ DestinationName: 'destination',
+ },
+ {
+ SourceName: 'source',
+ DestinationName: '*',
+ },
+ ].forEach(function(item) {
+ ['All Services (*)', 'SerVices', '(*)', '*', 'vIces', 'lL Ser'].forEach(function(term) {
+ const actual = filter(item, {
+ s: term,
+ });
+ assert.ok(actual);
+ });
+ });
+});
diff --git a/ui-v2/tests/unit/search/filters/kv-test.js b/ui-v2/tests/unit/search/filters/kv-test.js
new file mode 100644
index 0000000000..e278752e14
--- /dev/null
+++ b/ui-v2/tests/unit/search/filters/kv-test.js
@@ -0,0 +1,36 @@
+import getFilter from 'consul-ui/search/filters/kv';
+import { module, test } from 'qunit';
+
+module('Unit | Search | Filter | kv');
+
+const filter = getFilter(cb => cb);
+test('items are found by properties', function(assert) {
+ [
+ {
+ Key: 'HIT-here',
+ },
+ {
+ Key: 'folder-HIT/',
+ },
+ {
+ Key: 'really/long/path/HIT-here',
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(actual);
+ });
+});
+test('items are not found', function(assert) {
+ [
+ {
+ Key: 'key',
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
diff --git a/ui-v2/tests/unit/search/filters/node-test.js b/ui-v2/tests/unit/search/filters/node-test.js
new file mode 100644
index 0000000000..aa53f12f6e
--- /dev/null
+++ b/ui-v2/tests/unit/search/filters/node-test.js
@@ -0,0 +1,30 @@
+import getFilter from 'consul-ui/search/filters/node';
+import { module, test } from 'qunit';
+
+module('Unit | Search | Filter | node');
+
+const filter = getFilter(cb => cb);
+test('items are found by properties', function(assert) {
+ [
+ {
+ Node: 'node-HIT',
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(actual);
+ });
+});
+test('items are not found', function(assert) {
+ [
+ {
+ Node: 'name',
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
diff --git a/ui-v2/tests/unit/search/filters/node/service-test.js b/ui-v2/tests/unit/search/filters/node/service-test.js
new file mode 100644
index 0000000000..54dcc5b459
--- /dev/null
+++ b/ui-v2/tests/unit/search/filters/node/service-test.js
@@ -0,0 +1,94 @@
+import getFilter from 'consul-ui/search/filters/node/service';
+import { module, test } from 'qunit';
+
+module('Unit | Search | Filter | node/service');
+
+const filter = getFilter(cb => cb);
+test('items are found by properties', function(assert) {
+ [
+ {
+ Service: 'service-HIT',
+ ID: 'id',
+ Port: 8500,
+ Tags: [],
+ },
+ {
+ Service: 'service',
+ ID: 'id-HiT',
+ Port: 8500,
+ Tags: [],
+ },
+ {
+ Service: 'service',
+ ID: 'id',
+ Port: 8500,
+ Tags: ['tag', 'tag-withHiT'],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(actual);
+ });
+});
+test('items are found by port (non-string)', function(assert) {
+ [
+ {
+ Service: 'service',
+ ID: 'id',
+ Port: 8500,
+ Tags: ['tag', 'tag'],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: '8500',
+ });
+ assert.ok(actual);
+ });
+});
+test('items are not found', function(assert) {
+ [
+ {
+ Service: 'service',
+ ID: 'id',
+ Port: 8500,
+ },
+ {
+ Service: 'service',
+ ID: 'id',
+ Port: 8500,
+ Tags: ['one', 'two'],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
+test('tags can be empty', function(assert) {
+ [
+ {
+ Service: 'service',
+ ID: 'id',
+ Port: 8500,
+ },
+ {
+ Service: 'service',
+ ID: 'id',
+ Port: 8500,
+ Tags: null,
+ },
+ {
+ Service: 'service',
+ ID: 'id',
+ Port: 8500,
+ Tags: [],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
diff --git a/ui-v2/tests/unit/search/filters/policy-test.js b/ui-v2/tests/unit/search/filters/policy-test.js
new file mode 100644
index 0000000000..92d8f6dfe0
--- /dev/null
+++ b/ui-v2/tests/unit/search/filters/policy-test.js
@@ -0,0 +1,36 @@
+import getFilter from 'consul-ui/search/filters/policy';
+import { module, test } from 'qunit';
+
+module('Unit | Search | Filter | policy');
+
+const filter = getFilter(cb => cb);
+test('items are found by properties', function(assert) {
+ [
+ {
+ Name: 'name-HIT',
+ Description: 'description',
+ },
+ {
+ Name: 'name',
+ Description: 'desc-HIT-ription',
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(actual);
+ });
+});
+test('items are not found', function(assert) {
+ [
+ {
+ Name: 'name',
+ Description: 'description',
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
diff --git a/ui-v2/tests/unit/search/filters/service-test.js b/ui-v2/tests/unit/search/filters/service-test.js
new file mode 100644
index 0000000000..58364c4b50
--- /dev/null
+++ b/ui-v2/tests/unit/search/filters/service-test.js
@@ -0,0 +1,59 @@
+import getFilter from 'consul-ui/search/filters/service';
+import { module, test } from 'qunit';
+
+module('Unit | Search | Filter | service');
+
+const filter = getFilter(cb => cb);
+test('items are found by properties', function(assert) {
+ [
+ {
+ Name: 'name-HIT',
+ Tags: [],
+ },
+ {
+ Name: 'name',
+ Tags: ['tag', 'tag-withHiT'],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(actual);
+ });
+});
+test('items are not found', function(assert) {
+ [
+ {
+ Name: 'name',
+ },
+ {
+ Name: 'name',
+ Tags: ['one', 'two'],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
+test('tags can be empty', function(assert) {
+ [
+ {
+ Name: 'name',
+ },
+ {
+ Name: 'name',
+ Tags: null,
+ },
+ {
+ Name: 'name',
+ Tags: [],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
diff --git a/ui-v2/tests/unit/search/filters/service/node-test.js b/ui-v2/tests/unit/search/filters/service/node-test.js
new file mode 100644
index 0000000000..818de9e2c3
--- /dev/null
+++ b/ui-v2/tests/unit/search/filters/service/node-test.js
@@ -0,0 +1,56 @@
+import getFilter from 'consul-ui/search/filters/service/node';
+import { module, test } from 'qunit';
+
+module('Unit | Search | Filter | service/node');
+
+const filter = getFilter(cb => cb);
+test('items are found by properties', function(assert) {
+ [
+ {
+ Service: {
+ ID: 'hit',
+ },
+ Node: {
+ Node: 'node',
+ },
+ },
+ {
+ Service: {
+ ID: 'id',
+ },
+ Node: {
+ Node: 'nodeHiT',
+ },
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(actual);
+ });
+});
+test('items are not found', function(assert) {
+ [
+ {
+ Service: {
+ ID: 'ID',
+ },
+ Node: {
+ Node: 'node',
+ },
+ },
+ {
+ Service: {
+ ID: 'id',
+ },
+ Node: {
+ Node: 'node',
+ },
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
diff --git a/ui-v2/tests/unit/search/filters/token-test.js b/ui-v2/tests/unit/search/filters/token-test.js
new file mode 100644
index 0000000000..c01e1b4fb5
--- /dev/null
+++ b/ui-v2/tests/unit/search/filters/token-test.js
@@ -0,0 +1,86 @@
+import getFilter from 'consul-ui/search/filters/token';
+import { module, test } from 'qunit';
+
+module('Unit | Search | Filter | token');
+
+const filter = getFilter(cb => cb);
+test('items are found by properties', function(assert) {
+ [
+ {
+ AccessorID: 'HIT-id',
+ Name: 'name',
+ Description: 'description',
+ Policies: [],
+ },
+ {
+ AccessorID: 'id',
+ Name: 'name-HIT',
+ Description: 'description',
+ Policies: [],
+ },
+ {
+ AccessorID: 'id',
+ Name: 'name',
+ Description: 'desc-HIT-ription',
+ Policies: [],
+ },
+ {
+ AccessorID: 'id',
+ Name: 'name',
+ Description: 'description',
+ Policies: [{ Name: 'policy' }, { Name: 'policy-HIT' }],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(actual);
+ });
+});
+test('items are not found', function(assert) {
+ [
+ {
+ AccessorID: 'id',
+ Name: 'name',
+ Description: 'description',
+ Policies: [],
+ },
+ {
+ AccessorID: 'id',
+ Name: 'name',
+ Description: 'description',
+ Policies: [{ Name: 'policy' }, { Name: 'policy-second' }],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
+test('policies can be empty', function(assert) {
+ [
+ {
+ AccessorID: 'id',
+ Name: 'name',
+ Description: 'description',
+ },
+ {
+ AccessorID: 'id',
+ Name: 'name',
+ Description: 'description',
+ Policies: null,
+ },
+ {
+ AccessorID: 'id',
+ Name: 'name',
+ Description: 'description',
+ Policies: [],
+ },
+ ].forEach(function(item) {
+ const actual = filter(item, {
+ s: 'hit',
+ });
+ assert.ok(!actual);
+ });
+});
diff --git a/ui-v2/tests/unit/services/search-test.js b/ui-v2/tests/unit/services/search-test.js
new file mode 100644
index 0000000000..d499f7a472
--- /dev/null
+++ b/ui-v2/tests/unit/services/search-test.js
@@ -0,0 +1,12 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('service:search', 'Unit | Service | search', {
+ // Specify the other units that are required for this test.
+ // needs: ['service:foo']
+});
+
+// Replace this with your real tests.
+test('it exists', function(assert) {
+ let service = this.subject();
+ assert.ok(service);
+});
diff --git a/ui-v2/tests/unit/utils/dom/create-listeners-test.js b/ui-v2/tests/unit/utils/dom/create-listeners-test.js
new file mode 100644
index 0000000000..ae735467ba
--- /dev/null
+++ b/ui-v2/tests/unit/utils/dom/create-listeners-test.js
@@ -0,0 +1,79 @@
+import createListeners from 'consul-ui/utils/dom/create-listeners';
+import { module } from 'ember-qunit';
+import test from 'ember-sinon-qunit/test-support/test';
+
+module('Unit | Utility | dom/create listeners');
+
+test('it has add and remove methods', function(assert) {
+ const listeners = createListeners();
+ assert.ok(typeof listeners.add === 'function');
+ assert.ok(typeof listeners.remove === 'function');
+});
+test('add returns an remove function', function(assert) {
+ const listeners = createListeners();
+ const remove = listeners.add({
+ addEventListener: function() {},
+ });
+ assert.ok(typeof remove === 'function');
+});
+test('remove returns an array of removed handlers (the return of a saved remove)', function(assert) {
+ // just use true here to prove that it's what gets returned
+ const expected = true;
+ const handlers = [
+ function() {
+ return expected;
+ },
+ ];
+ const listeners = createListeners(handlers);
+ const actual = listeners.remove();
+ assert.deepEqual(actual, [expected]);
+ // handlers should now be empty
+ assert.equal(handlers.length, 0);
+});
+test('remove calls the remove functions', function(assert) {
+ const expected = this.stub();
+ const arr = [expected];
+ const listeners = createListeners(arr);
+ listeners.remove();
+ assert.ok(expected.calledOnce);
+ assert.equal(arr.length, 0);
+});
+test('listeners are added on add', function(assert) {
+ const listeners = createListeners();
+ const stub = this.stub();
+ const target = {
+ addEventListener: stub,
+ };
+ const name = 'test';
+ const handler = function(e) {};
+ listeners.add(target, name, handler);
+ assert.ok(stub.calledOnce);
+ assert.ok(stub.calledWith(name, handler));
+});
+test('listeners are removed on remove', function(assert) {
+ const listeners = createListeners();
+ const stub = this.stub();
+ const target = {
+ addEventListener: function() {},
+ removeEventListener: stub,
+ };
+ const name = 'test';
+ const handler = function(e) {};
+ const remove = listeners.add(target, name, handler);
+ remove();
+ assert.ok(stub.calledOnce);
+ assert.ok(stub.calledWith(name, handler));
+});
+test('remove returns the original handler', function(assert) {
+ const listeners = createListeners();
+ const target = {
+ addEventListener: function() {},
+ removeEventListener: function() {},
+ };
+ const name = 'test';
+ const expected = this.stub();
+ const remove = listeners.add(target, name, expected);
+ const actual = remove();
+ actual();
+ assert.ok(expected.calledOnce);
+});
diff --git a/ui-v2/tests/unit/utils/search/filterable-test.js b/ui-v2/tests/unit/utils/search/filterable-test.js
new file mode 100644
index 0000000000..1d11342015
--- /dev/null
+++ b/ui-v2/tests/unit/utils/search/filterable-test.js
@@ -0,0 +1,10 @@
+import searchFilterable from 'consul-ui/utils/search/filterable';
+import { module, test } from 'qunit';
+
+module('Unit | Utility | search/filterable');
+
+// Replace this with your real tests.
+test('it works', function(assert) {
+ let result = searchFilterable();
+ assert.ok(result);
+});