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
This commit is contained in:
parent
9c77f2c52a
commit
74390f2d24
|
@ -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'));
|
||||
}
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
import Service from '@ember/service';
|
||||
export default Service.extend({});
|
|
@ -1,4 +1,4 @@
|
|||
{{!<form>}}
|
||||
{{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)}}
|
||||
{{!</form>}}
|
||||
|
|
|
@ -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}}
|
|
@ -1,4 +1,4 @@
|
|||
{{!<form>}}
|
||||
{{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)}}
|
||||
{{!</form>}}
|
|
@ -22,60 +22,63 @@
|
|||
{{#block-slot 'content'}}
|
||||
{{#if (gt items.length 0) }}
|
||||
<form class="filter-bar">
|
||||
{{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search"}}
|
||||
{{freetext-filter searchable=searchable value=s placeholder="Search"}}
|
||||
</form>
|
||||
{{/if}}
|
||||
{{#if (gt filtered.length 0)}}
|
||||
{{#tabular-collection
|
||||
items=(sort-by 'CreateIndex:desc' 'Name:asc' filtered) as |item index|
|
||||
}}
|
||||
{{#block-slot 'header'}}
|
||||
<th>Name</th>
|
||||
<th>Datacenters</th>
|
||||
<th>Description</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row' }}
|
||||
<td data-test-policy="{{item.Name}}">
|
||||
<a href={{href-to 'dc.acls.policies.edit' item.ID}} class={{if (policy/is-management item) 'is-management'}}>{{item.Name}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{join ', ' (policy/datacenters item)}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.Description}}
|
||||
</td>
|
||||
{{/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')}}
|
||||
<ul>
|
||||
{{#if (policy/is-management item)}}
|
||||
<li>
|
||||
<a data-test-edit href={{href-to 'dc.acls.policies.edit' item.ID}}>View</a>
|
||||
</li>
|
||||
{{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'}}
|
||||
<th>Name</th>
|
||||
<th>Datacenters</th>
|
||||
<th>Description</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row' }}
|
||||
<td data-test-policy="{{item.Name}}">
|
||||
<a href={{href-to 'dc.acls.policies.edit' item.ID}} class={{if (policy/is-management item) 'is-management'}}>{{item.Name}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{join ', ' (policy/datacenters item)}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.Description}}
|
||||
</td>
|
||||
{{/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')}}
|
||||
<ul>
|
||||
{{#if (policy/is-management item)}}
|
||||
<li>
|
||||
<a data-test-edit href={{href-to 'dc.acls.policies.edit' item.ID}}>View</a>
|
||||
</li>
|
||||
{{else}}
|
||||
|
||||
<li>
|
||||
<a data-test-edit href={{href-to 'dc.acls.policies.edit' item.ID}}>Edit</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-delete onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{/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}}
|
||||
<p>
|
||||
There are no Policies.
|
||||
</p>
|
||||
{{/if}}
|
||||
<li>
|
||||
<a data-test-edit href={{href-to 'dc.acls.policies.edit' item.ID}}>Edit</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-delete onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{/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'}}
|
||||
<p>
|
||||
There are no Policies.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
{{/block-slot}}
|
||||
{{/app-view}}
|
|
@ -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'}}
|
||||
<h1>
|
||||
Access Controls
|
||||
</h1>
|
||||
{{#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'}}
|
||||
<a data-test-create href="{{href-to 'dc.acls.tokens.create'}}" class="type-create">Create</a>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'content'}}
|
||||
{{#if (gt items.length 0) }}
|
||||
<form class="filter-bar">
|
||||
{{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search"}}
|
||||
</form>
|
||||
{{#block-slot 'notification' as |status type subject|}}
|
||||
{{partial 'dc/acls/tokens/notifications'}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'header'}}
|
||||
<h1>
|
||||
Access Controls
|
||||
</h1>
|
||||
{{#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'}}
|
||||
<a data-test-create href="{{href-to 'dc.acls.tokens.create'}}" class="type-create">Create</a>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'content'}}
|
||||
{{#if (gt items.length 0) }}
|
||||
<form class="filter-bar">
|
||||
{{freetext-filter searchable=searchable value=s placeholder="Search"}}
|
||||
</form>
|
||||
{{/if}}
|
||||
{{#if (token/is-legacy items)}}
|
||||
<p data-test-notification-update class="notice info"><strong>Update.</strong> 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 <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl-migrate-tokens.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p>
|
||||
{{/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'}}
|
||||
<th>Accessor ID</th>
|
||||
<th>Scope</th>
|
||||
<th>Description</th>
|
||||
<th>Policies</th>
|
||||
<th> </th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td data-test-token="{{item.AccessorID}}" class={{if (eq item.AccessorID token.AccessorID) 'me' }}>
|
||||
<a href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>{{truncate item.AccessorID 8 false}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{if item.Local 'local' 'global' }}
|
||||
</td>
|
||||
<td>
|
||||
{{default item.Description item.Name}}
|
||||
</td>
|
||||
<td colspan={{if (not-eq item.AccessorID token.AccessorID) '2' }}>
|
||||
{{#if (token/is-legacy item) }}
|
||||
Legacy tokens have embedded policies.
|
||||
{{ else }}
|
||||
{{#each item.Policies as |item|}}
|
||||
<strong class={{if (policy/is-management item) 'policy-management' }}>{{item.Name}}</strong>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{#if (eq item.AccessorID token.AccessorID)}}
|
||||
<td>Your token</td>
|
||||
{{/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')}}
|
||||
<ul>
|
||||
{{#if false}}
|
||||
<li>
|
||||
{{#copy-button-feedback title="Copy AccessorID to the clipboard" copy=item.AccessorID name="AccessorID"}}Copy AccessorID{{/copy-button-feedback}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
<a data-test-edit href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>Edit</a>
|
||||
</li>
|
||||
{{#block-slot 'header'}}
|
||||
<th>Accessor ID</th>
|
||||
<th>Scope</th>
|
||||
<th>Description</th>
|
||||
<th>Policies</th>
|
||||
<th> </th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td data-test-token="{{item.AccessorID}}" class={{if (eq item.AccessorID token.AccessorID) 'me' }}>
|
||||
<a href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>{{truncate item.AccessorID 8 false}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{if item.Local 'local' 'global' }}
|
||||
</td>
|
||||
<td>
|
||||
{{default item.Description item.Name}}
|
||||
</td>
|
||||
<td colspan={{if (not-eq item.AccessorID token.AccessorID) '2' }}>
|
||||
{{#if (token/is-legacy item) }}
|
||||
Legacy tokens have embedded policies.
|
||||
{{ else }}
|
||||
{{#each item.Policies as |item|}}
|
||||
<strong class={{if (policy/is-management item) 'policy-management' }}>{{item.Name}}</strong>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{#if (eq item.AccessorID token.AccessorID)}}
|
||||
<td>Your token</td>
|
||||
{{/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')}}
|
||||
<ul>
|
||||
{{#if false}}
|
||||
<li>
|
||||
{{#copy-button-feedback title="Copy AccessorID to the clipboard" copy=item.AccessorID name="AccessorID"}}Copy AccessorID{{/copy-button-feedback}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
<a data-test-edit href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>Edit</a>
|
||||
</li>
|
||||
{{#if (not (token/is-legacy item))}}
|
||||
<li>
|
||||
<a data-test-clone onclick={{action 'sendClone' item}}>Duplicate</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-clone onclick={{action 'sendClone' item}}>Duplicate</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (eq item.AccessorID token.AccessorID) }}
|
||||
<li>
|
||||
<a data-test-logout onclick={{queue (action confirm 'logout' item) (action change)}}>Stop using</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-logout onclick={{queue (action confirm 'logout' item) (action change)}}>Stop using</a>
|
||||
</li>
|
||||
{{else}}
|
||||
|
||||
<li>
|
||||
<a data-test-use onclick={{queue (action confirm 'use' item) (action change)}}>Use</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-use onclick={{queue (action confirm 'use' item) (action change)}}>Use</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#unless (or (token/is-anonymous item) (eq item.AccessorID token.AccessorID)) }}
|
||||
<li>
|
||||
<a data-test-delete onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-delete onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
</li>
|
||||
{{/unless}}
|
||||
</ul>
|
||||
{{/action-group}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'dialog' as |execute cancel message name|}}
|
||||
<p>
|
||||
{{#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}}
|
||||
</p>
|
||||
<button type="button" class="type-delete" {{action execute}}>
|
||||
{{#if (eq name 'delete')}}
|
||||
Confirm Delete
|
||||
{{else if (eq name 'logout')}}
|
||||
Confirm Logout
|
||||
{{ else if (eq name 'use')}}
|
||||
Confirm Use
|
||||
{{/if}}
|
||||
</button>
|
||||
<button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
|
||||
{{/block-slot}}
|
||||
{{/confirmation-dialog}}
|
||||
{{/block-slot}}
|
||||
</ul>
|
||||
{{/action-group}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'dialog' as |execute cancel message name|}}
|
||||
<p>
|
||||
{{#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}}
|
||||
</p>
|
||||
<button type="button" class="type-delete" {{action execute}}>
|
||||
{{#if (eq name 'delete')}}
|
||||
Confirm Delete
|
||||
{{else if (eq name 'logout')}}
|
||||
Confirm Logout
|
||||
{{ else if (eq name 'use')}}
|
||||
Confirm Use
|
||||
{{/if}}
|
||||
</button>
|
||||
<button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
|
||||
{{/block-slot}}
|
||||
{{/confirmation-dialog}}
|
||||
{{/block-slot}}
|
||||
{{/tabular-collection}}
|
||||
{{else}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no Tokens.
|
||||
There are no Tokens.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/block-slot}}
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
{{/block-slot}}
|
||||
{{/app-view}}
|
||||
|
|
|
@ -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'}}
|
||||
<th>Source</th>
|
||||
<th> </th>
|
||||
<th>Destination</th>
|
||||
<th>Precedence</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td class="source" data-test-intention="{{item.ID}}">
|
||||
<a href={{href-to 'dc.intentions.edit' item.ID}} data-test-intention-source="{{item.SourceName}}">
|
||||
{{#if (eq item.SourceName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
{{item.SourceName}}
|
||||
{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td class="intent-{{item.Action}}" data-test-intention-action="{{item.Action}}">
|
||||
<strong>{{item.Action}}</strong>
|
||||
</td>
|
||||
<td class="destination" data-test-intention-destination="{{item.DestinationName}}">
|
||||
{{#if (eq item.DestinationName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
{{item.DestinationName}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="precedence">
|
||||
{{item.Precedence}}
|
||||
</td>
|
||||
{{/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')}}
|
||||
<ul>
|
||||
<li>
|
||||
<a href={{href-to 'dc.intentions.edit' item.ID}}>Edit</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-delete onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{/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}}
|
||||
<p>
|
||||
There are no intentions.
|
||||
</p>
|
||||
{{/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'}}
|
||||
<th>Source</th>
|
||||
<th> </th>
|
||||
<th>Destination</th>
|
||||
<th>Precedence</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td class="source" data-test-intention="{{item.ID}}">
|
||||
<a href={{href-to 'dc.intentions.edit' item.ID}} data-test-intention-source="{{item.SourceName}}">
|
||||
{{#if (eq item.SourceName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
{{item.SourceName}}
|
||||
{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td class="intent-{{item.Action}}" data-test-intention-action="{{item.Action}}">
|
||||
<strong>{{item.Action}}</strong>
|
||||
</td>
|
||||
<td class="destination" data-test-intention-destination="{{item.DestinationName}}">
|
||||
{{#if (eq item.DestinationName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
{{item.DestinationName}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="precedence">
|
||||
{{item.Precedence}}
|
||||
</td>
|
||||
{{/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')}}
|
||||
<ul>
|
||||
<li>
|
||||
<a href={{href-to 'dc.intentions.edit' item.ID}}>Edit</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-delete onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{/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'}}
|
||||
<p>
|
||||
There are no intentions.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
{{/block-slot}}
|
||||
{{/app-view}}
|
|
@ -25,7 +25,7 @@
|
|||
{{#block-slot 'toolbar'}}
|
||||
{{#if (gt items.length 0) }}
|
||||
<form class="filter-bar">
|
||||
{{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search by name"}}
|
||||
{{freetext-filter searchable=searchable value=s placeholder="Search by name"}}
|
||||
</form>
|
||||
{{/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'}}
|
||||
<th>Name</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td data-test-kv="{{item.Key}}" class={{if item.isFolder 'folder' 'file' }}>
|
||||
<a href={{href-to (if item.isFolder 'dc.kv.folder' 'dc.kv.edit') item.Key }}>{{right-trim (left-trim item.Key parent.Key) '/'}}</a>
|
||||
</td>
|
||||
{{/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')}}
|
||||
<ul>
|
||||
<li>
|
||||
<a data-test-edit href={{href-to (if item.isFolder 'dc.kv.folder' 'dc.kv.edit') item.Key}}>{{if item.isFolder 'View' 'Edit'}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-delete onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{/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}}
|
||||
<p>There are no Key / Value pairs.</p>
|
||||
{{/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'}}
|
||||
<th>Name</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td data-test-kv="{{item.Key}}" class={{if item.isFolder 'folder' 'file' }}>
|
||||
<a href={{href-to (if item.isFolder 'dc.kv.folder' 'dc.kv.edit') item.Key}}>{{right-trim (left-trim item.Key parent.Key) '/'}}</a>
|
||||
</td>
|
||||
{{/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')}}
|
||||
<ul>
|
||||
<li>
|
||||
<a data-test-edit href={{href-to (if item.isFolder 'dc.kv.folder' 'dc.kv.edit') item.Key}}>{{if item.isFolder 'View' 'Edit'}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-test-delete onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{/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'}}
|
||||
<p>
|
||||
There are no Key / Value pairs.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
{{/block-slot}}
|
||||
{{/app-view}}
|
|
@ -1,40 +1,43 @@
|
|||
{{#if (gt items.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<form class="filter-bar">
|
||||
{{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search by name/port"}}
|
||||
</form>
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<form class="filter-bar">
|
||||
{{freetext-filter searchable=searchable value=s placeholder="Search by name/port"}}
|
||||
</form>
|
||||
{{/if}}
|
||||
{{#if (gt filtered.length 0)}}
|
||||
{{#tabular-collection
|
||||
data-test-services
|
||||
items=filtered as |item index|
|
||||
}}
|
||||
{{#block-slot 'header'}}
|
||||
<th>Service</th>
|
||||
<th>Port</th>
|
||||
<th>Tags</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td data-test-service-name="{{item.Service}}">
|
||||
<a href={{href-to 'dc.services.show' item.Service}}>
|
||||
<span data-test-external-source="{{service/external-source item}}" style={{{ concat 'background-image: ' (css-var (concat '--' (service/external-source item) '-color-svg') 'none')}}}></span>
|
||||
{{item.Service}}{{#if (not-eq item.ID item.Service) }}<em data-test-service-id="{{item.ID}}">({{item.ID}})</em>{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-service-port="{{item.Port}}" class="port">
|
||||
{{item.Port}}
|
||||
</td>
|
||||
<td data-test-service-tags class="tags">
|
||||
{{#if (gt item.Tags.length 0)}}
|
||||
{{#each item.Tags as |item|}}
|
||||
<span>{{item}}</span>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/block-slot}}
|
||||
{{/tabular-collection}}
|
||||
{{else}}
|
||||
<p>
|
||||
{{#changeable-set dispatcher=searchable}}
|
||||
{{#block-slot 'set' as |filtered|}}
|
||||
{{#tabular-collection
|
||||
data-test-services
|
||||
items=filtered as |item index|
|
||||
}}
|
||||
{{#block-slot 'header'}}
|
||||
<th>Service</th>
|
||||
<th>Port</th>
|
||||
<th>Tags</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td data-test-service-name="{{item.Service}}">
|
||||
<a href={{href-to 'dc.services.show' item.Service}}>
|
||||
<span data-test-external-source="{{service/external-source item}}" style={{{ concat 'background-image: ' (css-var (concat '--' (service/external-source item) '-color-svg') 'none')}}}></span>
|
||||
{{item.Service}}{{#if (not-eq item.ID item.Service) }}<em data-test-service-id="{{item.ID}}">({{item.ID}})</em>{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td data-test-service-port="{{item.Port}}" class="port">
|
||||
{{item.Port}}
|
||||
</td>
|
||||
<td data-test-service-tags class="tags">
|
||||
{{#if (gt item.Tags.length 0)}}
|
||||
{{#each item.Tags as |item|}}
|
||||
<span>{{item}}</span>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/block-slot}}
|
||||
{{/tabular-collection}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no services.
|
||||
</p>
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
|
|
|
@ -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 @@
|
|||
<h2>Unhealthy Nodes</h2>
|
||||
<div>
|
||||
{{! think about 2 differing views here }}
|
||||
<ol>
|
||||
{{#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}}
|
||||
</ol>
|
||||
<ul>
|
||||
{{#changeable-set dispatcher=searchableUnhealthy}}
|
||||
{{#block-slot 'set' as |unhealthy|}}
|
||||
{{#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}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no unhealthy nodes for that search.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (gt healthy.length 0) }}
|
||||
<div class="healthy">
|
||||
<h2>Healthy Nodes</h2>
|
||||
{{#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'}}
|
||||
<p>
|
||||
There are no healthy nodes for that search.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (and (eq healthy.length 0) (eq unhealthy.length 0)) }}
|
||||
|
|
|
@ -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'}}
|
||||
<th style={{remainingWidth}}>Service</th>
|
||||
<th style={{totalWidth}}>Health Checks<span><em>The number of health checks for the service on all nodes</em></span></th>
|
||||
<th style={{remainingWidth}}>Tags</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
|
||||
<td data-test-service="{{item.Name}}" style={{remainingWidth}}>
|
||||
<a href={{href-to 'dc.services.show' item.Name}}>
|
||||
<span data-test-external-source="{{service/external-source item}}" style={{{ concat 'background-image: ' (css-var (concat '--' (service/external-source item) '-color-svg') 'none')}}}></span>
|
||||
{{item.Name}}
|
||||
</a>
|
||||
</td>
|
||||
<td style={{totalWidth}}>
|
||||
{{#if (and (lt item.ChecksPassing 1) (lt item.ChecksWarning 1) (lt item.ChecksCritical 1) )}}
|
||||
<span title="No Healthchecks" class="zero">0</span>
|
||||
{{else}}
|
||||
<dl>
|
||||
<dt title="Passing" class="passing{{if (lt item.ChecksPassing 1) ' zero'}}">Healthchecks Passing</dt>
|
||||
<dd title="Passing" class={{if (lt item.ChecksPassing 1) 'zero'}} style={{passingWidth}}>{{format-number item.ChecksPassing}}</dd>
|
||||
<dt title="Warning" class="warning{{if (lt item.ChecksWarning 1) ' zero'}}">Healthchecks Warning</dt>
|
||||
<dd title="Warning" class={{if (lt item.ChecksWarning 1) 'zero'}} style={{warningWidth}}>{{format-number item.ChecksWarning}}</dd>
|
||||
<dt title="Critical" class="critical{{if (lt item.ChecksCritical 1) ' zero'}}">Healthchecks Critical</dt>
|
||||
<dd title="Critical" class={{if (lt item.ChecksCritical 1) 'zero'}} style={{criticalWidth}}>{{format-number item.ChecksCritical}}</dd>
|
||||
</dl>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="tags" style={{remainingWidth}}>
|
||||
{{#if (gt item.Tags.length 0)}}
|
||||
{{#each item.Tags as |item|}}
|
||||
<span>{{item}}</span>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/block-slot}}
|
||||
{{/tabular-collection}}
|
||||
{{else}}
|
||||
<p>
|
||||
There are no services.
|
||||
</p>
|
||||
{{/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'}}
|
||||
<th style={{remainingWidth}}>Service</th>
|
||||
<th style={{totalWidth}}>Health Checks<span><em>The number of health checks for the service on all nodes</em></span></th>
|
||||
<th style={{remainingWidth}}>Tags</th>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td data-test-service="{{item.Name}}" style={{remainingWidth}}>
|
||||
<a href={{href-to 'dc.services.show' item.Name}}>
|
||||
<span data-test-external-source="{{service/external-source item}}" style={{{ concat 'background-image: ' (css-var (concat '--' (service/external-source item) '-color-svg') 'none')}}}></span>
|
||||
{{item.Name}}
|
||||
</a>
|
||||
</td>
|
||||
<td style={{totalWidth}}>
|
||||
{{#if (and (lt item.ChecksPassing 1) (lt item.ChecksWarning 1) (lt item.ChecksCritical 1) )}}
|
||||
<span title="No Healthchecks" class="zero">0</span>
|
||||
{{else}}
|
||||
<dl>
|
||||
<dt title="Passing" class="passing{{if (lt item.ChecksPassing 1) ' zero'}}">Healthchecks Passing</dt>
|
||||
<dd title="Passing" class={{if (lt item.ChecksPassing 1) 'zero'}} style={{passingWidth}}>{{format-number item.ChecksPassing}}</dd>
|
||||
<dt title="Warning" class="warning{{if (lt item.ChecksWarning 1) ' zero'}}">Healthchecks Warning</dt>
|
||||
<dd title="Warning" class={{if (lt item.ChecksWarning 1) 'zero'}} style={{warningWidth}}>{{format-number item.ChecksWarning}}</dd>
|
||||
<dt title="Critical" class="critical{{if (lt item.ChecksCritical 1) ' zero'}}">Healthchecks Critical</dt>
|
||||
<dd title="Critical" class={{if (lt item.ChecksCritical 1) 'zero'}} style={{criticalWidth}}>{{format-number item.ChecksCritical}}</dd>
|
||||
</dl>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="tags" style={{remainingWidth}}>
|
||||
{{#if (gt item.Tags.length 0)}}
|
||||
{{#each item.Tags as |item|}}
|
||||
<span>{{item}}</span>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/block-slot}}
|
||||
{{/tabular-collection}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no services.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
{{/block-slot}}
|
||||
{{/app-view}}
|
||||
|
||||
|
|
|
@ -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 @@
|
|||
<h2>Unhealthy Nodes</h2>
|
||||
<div>
|
||||
<ul>
|
||||
{{#changeable-set dispatcher=searchableUnhealthy}}
|
||||
{{#block-slot 'set' as |unhealthy|}}
|
||||
{{#each unhealthy as |item|}}
|
||||
{{healthchecked-resource
|
||||
tagName='li'
|
||||
data-test-node=item.Node.Node
|
||||
href=(href-to 'dc.nodes.show' 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
|
||||
}}
|
||||
{{healthchecked-resource
|
||||
tagName='li'
|
||||
data-test-node=item.Node.Node
|
||||
href=(href-to 'dc.nodes.show' 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
|
||||
}}
|
||||
{{/each}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'empty'}}
|
||||
<p>
|
||||
There are no unhealthy nodes for that search.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,17 +64,26 @@
|
|||
{{#if (gt healthy.length 0) }}
|
||||
<div data-test-healthy class="healthy">
|
||||
<h2>Healthy Nodes</h2>
|
||||
{{#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'}}
|
||||
<p>
|
||||
There are no healthy nodes for that search.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
{{/changeable-set}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/block-slot}}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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 <select> uses, consider that
|
||||
data: data,
|
||||
},
|
||||
});
|
||||
// not returned
|
||||
return data;
|
||||
});
|
||||
return this;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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(),
|
||||
''
|
||||
);
|
||||
});
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
});
|
Loading…
Reference in New Issue