ui: Remove old style 'filterable' searching (#9356)

* Switch upstream-instances to use new style of searchable

* Add search action to DataCollection plus basic README

* Use DataCollection for PowerSelect searching in child-selectors

* Remove old style filterable search for role/policies and instances

* Remove old helpers/components related to search/sort/filter
This commit is contained in:
John Cowen 2020-12-09 19:12:17 +00:00 committed by GitHub
parent 52ca0f2a2b
commit 7f4cd240b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 280 additions and 716 deletions

View File

@ -1,7 +0,0 @@
{{yield}}
<YieldSlot @name="content" @params={{block-params items}}>{{yield}}</YieldSlot>
{{#if (gt items.length 0)}}
<YieldSlot @name="set" @params={{block-params items}}>{{yield}}</YieldSlot>
{{else}}
<YieldSlot @name="empty">{{yield}}</YieldSlot>
{{/if}}

View File

@ -1,28 +0,0 @@
import Component from '@ember/component';
import { get, set } from '@ember/object';
import { inject as service } from '@ember/service';
import Slotted from 'block-slots';
export default Component.extend(Slotted, {
tagName: '',
dom: service('dom'),
init: function() {
this._super(...arguments);
this._listeners = this.dom.listeners();
},
willDestroyElement: function() {
this._listeners.remove();
this._super(...arguments);
},
didReceiveAttrs: function() {
this._super(...arguments);
if (this.items !== this.dispatcher.data) {
this._listeners.remove();
this._listeners.add(this.dispatcher, {
change: e => set(this, 'items', e.target.data),
});
set(this, 'items', get(this.dispatcher, 'data'));
}
this.dispatcher.search(this.terms);
},
});

View File

@ -1,4 +1,5 @@
<div <div
class="child-selector {{type}}-child-selector"
...attributes ...attributes
> >
{{yield}} {{yield}}
@ -11,18 +12,28 @@
@onchange={{action (mut allOptions) value="data"}} @onchange={{action (mut allOptions) value="data"}}
/> />
{{/if}} {{/if}}
<DataCollection
@type={{type}}
@sort='Name:asc'
@filters={{hash
searchproperties=(array 'Name')
}}
@items={{options}}
as |collection|>
<PowerSelect <PowerSelect
@searchEnabled={{true}} @searchEnabled={{true}}
@search={{action "search"}} @search={{action collection.search}}
@options={{options}} @options={{options}}
@loadingMessage="Loading..." @loadingMessage="Loading..."
@searchMessage="No possible options" @searchMessage="No possible options"
@searchPlaceholder={{placeholder}} @searchPlaceholder={{placeholder}}
@onOpen={{action (mut isOpen) true}} @onOpen={{action (mut isOpen) true}}
@onClose={{action (mut isOpen) false}} @onClose={{action (mut isOpen) false}}
@onChange={{action "change" "items[]" items}} as |item|> @onChange={{action "change" "items[]" items}}
as |item|>
<YieldSlot @name="option" @params={{block-params item}}>{{yield}}</YieldSlot> <YieldSlot @name="option" @params={{block-params item}}>{{yield}}</YieldSlot>
</PowerSelect> </PowerSelect>
</DataCollection>
</label> </label>
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<YieldSlot @name="set">{{yield}}</YieldSlot> <YieldSlot @name="set">{{yield}}</YieldSlot>

View File

@ -15,8 +15,6 @@ export default Component.extend(Slotted, {
type: '', type: '',
dom: service('dom'), dom: service('dom'),
search: service('search'),
sort: service('sort'),
formContainer: service('form'), formContainer: service('form'),
item: alias('form.data'), item: alias('form.data'),
@ -26,7 +24,6 @@ export default Component.extend(Slotted, {
init: function() { init: function() {
this._super(...arguments); this._super(...arguments);
this._listeners = this.dom.listeners(); this._listeners = this.dom.listeners();
this.searchable = this.search.searchable(this.type);
this.form = this.formContainer.form(this.type); this.form = this.formContainer.form(this.type);
this.form.clear({ Datacenter: this.dc, Namespace: this.nspace }); this.form.clear({ Datacenter: this.dc, Namespace: this.nspace });
}, },
@ -34,14 +31,10 @@ export default Component.extend(Slotted, {
this._super(...arguments); this._super(...arguments);
this._listeners.remove(); this._listeners.remove();
}, },
sortedOptions: sort('allOptions.[]', 'comparator'), options: computed('selectedOptions.[]', 'allOptions.[]', function() {
comparator: computed(function() {
return this.sort.comparator(this.type)();
}),
options: computed('selectedOptions.[]', 'sortedOptions.[]', function() {
// It's not massively important here that we are defaulting `items` and // It's not massively important here that we are defaulting `items` and
// losing reference as its just to figure out the diff // losing reference as its just to figure out the diff
let options = this.sortedOptions || []; let options = this.allOptions || [];
const items = this.selectedOptions || []; const items = this.selectedOptions || [];
if (get(items, 'length') > 0) { if (get(items, 'length') > 0) {
// filter out any items from the available options that have already been // filter out any items from the available options that have already been
@ -49,7 +42,6 @@ export default Component.extend(Slotted, {
// TODO: find a proper ember-data diff // TODO: find a proper ember-data diff
options = options.filter(item => !items.findBy('ID', get(item, 'ID'))); options = options.filter(item => !items.findBy('ID', get(item, 'ID')));
} }
this.searchable.add(options);
return options; return options;
}), }),
save: task(function*(item, items, success = function() {}) { save: task(function*(item, items, success = function() {}) {
@ -72,19 +64,6 @@ export default Component.extend(Slotted, {
} }
}), }),
actions: { actions: {
search: function(term) {
// TODO: make sure we can either search before things are loaded
// or wait until we are loaded, guess power select take care of that
return new Promise(resolve => {
const remove = this._listeners.add(this.searchable, {
change: e => {
remove();
resolve(e.target.data);
},
});
this.searchable.search(term);
});
},
reset: function() { reset: function() {
this.form.clear({ Datacenter: this.dc, Namespace: this.nspace }); this.form.clear({ Datacenter: this.dc, Namespace: this.nspace });
}, },

View File

@ -2,65 +2,61 @@
class="consul-upstream-instance-list" class="consul-upstream-instance-list"
...attributes ...attributes
> >
{{#if (gt this.items.length 0)}} <ul>
<ul> {{#each @items as |item|}}
{{#each this.items as |item|}} <li>
<li> <div class="header">
<div class="header"> <p>
<p> {{item.DestinationName}}
{{item.DestinationName}} </p>
</p> </div>
</div> <div class="detail">
<div class="detail"> {{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (env 'CONSUL_NSPACES_ENABLED')}} {{#if (not-eq item.DestinationType 'prepared_query')}}
{{#if (not-eq item.DestinationType 'prepared_query')}} <dl class="nspace">
<dl class="nspace"> <dt>
<dt> <Tooltip>
<Tooltip> Namespace
Namespace </Tooltip>
</Tooltip> </dt>
</dt> <dd>
<dd> {{or item.DestinationNamespace 'default'}}
{{or item.DestinationNamespace 'default'}} </dd>
</dd> </dl>
</dl>
{{/if}}
{{/if}} {{/if}}
{{#if (and (not-eq item.Datacenter @dc) (not-eq item.Datacenter ""))}} {{/if}}
<dl class="datacenter"> {{#if (and (not-eq item.Datacenter @dc) (not-eq item.Datacenter ""))}}
<dt> <dl class="datacenter">
<Tooltip> <dt>
Datacenter <Tooltip>
</Tooltip> Datacenter
</dt> </Tooltip>
<dd> </dt>
{{item.Datacenter}} <dd>
</dd> {{item.Datacenter}}
</dl> </dd>
{{/if}} </dl>
{{#if (gt item.LocalBindPort 0)}} {{/if}}
{{#if (gt item.LocalBindPort 0)}}
{{#let (concat (or item.LocalBindAddress '127.0.0.1') ':' item.LocalBindPort) as |combinedAddress|}} {{#let (concat (or item.LocalBindAddress '127.0.0.1') ':' item.LocalBindPort) as |combinedAddress|}}
<dl class="local-bind-address"> <dl class="local-bind-address">
<dt> <dt>
<span> <span>
Address Address
</span> </span>
</dt> </dt>
<dd> <dd>
<CopyButton <CopyButton
@value={{combinedAddress}} @value={{combinedAddress}}
@name="Address" @name="Address"
/> />
{{combinedAddress}} {{combinedAddress}}
</dd> </dd>
</dl> </dl>
{{/let}} {{/let}}
{{/if}} {{/if}}
</div> </div>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
{{else}}
{{yield api to="empty"}}
{{/if}}
</div> </div>

View File

@ -1,24 +0,0 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { sort } from '@ember/object/computed';
export default class ConsulUpstreamInstanceList extends Component {
@service('sort') sort;
@service('search') search;
@sort('searched', 'comparator') sorted;
get items() {
return this.sorted;
}
get searched() {
if (typeof this.args.search === 'undefined') {
return this.args.items;
}
const predicate = this.search.predicate('upstream-instance');
return this.args.items.filter(predicate(this.args.search));
}
get comparator() {
return this.sort.comparator('upstream-instance')(this.args.sort);
}
}

View File

@ -2,11 +2,33 @@
class="consul-upstream-instance-search-bar filter-bar" class="consul-upstream-instance-search-bar filter-bar"
...attributes ...attributes
> >
<FreetextFilter <div class="search">
@onsearch={{action @onsearch}} <FreetextFilter
@value={{@search}} @onsearch={{action @onsearch}}
@placeholder="Search" @value={{@search}}
/> @placeholder="Search"
>
<PopoverSelect
class="type-search-properties"
@position="right"
@onchange={{action @onfilter.searchproperty}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Search across
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each @searchproperties as |prop|}}
<Option @value={{prop}} @selected={{contains prop @filter.searchproperties}}>{{prop}}</Option>
{{/each}}
{{/let}}
</BlockSlot>
</PopoverSelect>
</FreetextFilter>
</div>
<div class="sort"> <div class="sort">
{{#let (or @sort 'DestinationName:asc') as |sort|}} {{#let (or @sort 'DestinationName:asc') as |sort|}}
<PopoverSelect <PopoverSelect

View File

@ -0,0 +1,47 @@
## DataSource
```handlebars
<DataCollection
@search={{''}}
@sort={{''}}
@filter={{hash
searchproperties=(array)
}}
@items={{array}}
as |collection|>
{{collection.items.length}}
<collection.Collection>
Has Results
</collection.Collection>
<collection.Empty>
Is Empty
</collection.Empty>
</DataCollection>
```
### Arguments
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `Array` | [] | The collection of data to search/sort/filter |
| `search` | `String` | '' | A search term to use for searching |
| `sort` | `String` | '' | A sort term to use for sorting on ('Name:asc') |
| `filter` | `Object` | | An object whose properties are the properties/values to filter on |
### Yields
The DataCollection yields an object containing the following:
| Property | Type | Description |
| --- | --- | --- |
| `items` | `Array` | The resulting collection of data after searching/sorting/filtering |
| `search` | `Function` | An action used to perform a search - takes a single string argument that should be the search term |
| `Collection` | `Component` | A slot-like component that only renders when the items in the collection is greater than 0 |
| `Empty` | `Component` | A slot-like component that only renders when the items in the collection is 0 |
### See
- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)
---

View File

@ -1,4 +1,6 @@
{{did-update (action (fn (set this 'term') '') @search)}}
{{yield (hash {{yield (hash
search=(action this.search)
items=this.items items=this.items
Collection=(if (gt this.items.length 0) (component 'anonymous') '') Collection=(if (gt this.items.length 0) (component 'anonymous') '')
Empty=(if (eq this.items.length 0) (component 'anonymous') '') Empty=(if (eq this.items.length 0) (component 'anonymous') '')

View File

@ -1,18 +1,32 @@
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { computed } from '@ember/object'; import { computed, get, action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { sort } from '@ember/object/computed'; import { sort } from '@ember/object/computed';
import { defineProperty } from '@ember/object'; import { defineProperty } from '@ember/object';
export default class DataCollectionComponent extends Component { export default class DataCollectionComponent extends Component {
@service('filter') filter; @service('filter') filter;
@service('sort') sort; @service('sort') sort;
@service('search') search; @service('search') searchService;
@tracked term = '';
get type() { get type() {
return this.args.type; return this.args.type;
} }
@computed('term', 'args.search')
get searchTerm() {
return this.term || this.args.search || '';
}
@action
search(term) {
this.term = term;
return this.items;
}
@computed('args.items', 'args.items.content') @computed('args.items', 'args.items.content')
get content() { get content() {
// TODO: Temporary little hack to ensure we detect DataSource proxy // TODO: Temporary little hack to ensure we detect DataSource proxy
@ -37,17 +51,17 @@ export default class DataCollectionComponent extends Component {
return this.sorted; return this.sorted;
} }
@computed('type', 'filtered', 'args.filters.searchproperties', 'args.search') @computed('type', 'filtered', 'args.filters.searchproperties', 'searchTerm')
get searched() { get searched() {
if (typeof this.args.search === 'undefined') { if (typeof this.searchTerm === '') {
return this.filtered; return this.filtered;
} }
const predicate = this.search.predicate(this.type); const predicate = this.searchService.predicate(this.type);
const options = {}; const options = {};
if (typeof this.args.filters.searchproperties !== 'undefined') { if (typeof get(this, 'args.filters.searchproperties') !== 'undefined') {
options.properties = this.args.filters.searchproperties; options.properties = this.args.filters.searchproperties;
} }
return this.filtered.filter(predicate(this.args.search, options)); return this.filtered.filter(predicate(this.searchTerm, options));
} }
@computed('type', 'content', 'args.filters') @computed('type', 'content', 'args.filters')

View File

@ -1,9 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default Helper.extend({
sort: service('sort'),
compute([type, key], hash) {
return this.sort.comparator(type)(key);
},
});

View File

@ -1,9 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default Helper.extend({
search: service('search'),
compute([type, items], hash) {
return this.search.searchable(type).add(items);
},
});

View File

@ -1,46 +0,0 @@
import { helper } from '@ember/component/helper';
import { slugify } from 'consul-ui/helpers/slugify';
export const selectableKeyValues = function(params = [], hash = {}) {
let selected;
const items = params.map(function(item, i) {
let key, value;
switch (typeof item) {
case 'string':
key = slugify([item]);
value = item;
break;
default:
if (item.length > 1) {
key = item[0];
value = item[1];
} else {
key = slugify([item[0]]);
value = item[0];
}
break;
}
const kv = {
key: key,
value: value,
};
switch (typeof hash.selected) {
case 'string':
if (hash.selected === item[0]) {
selected = kv;
}
break;
case 'number':
if (hash.selected === i) {
selected = kv;
}
break;
}
return kv;
});
return {
items: items,
selected: typeof selected === 'undefined' ? items[0] : selected,
};
};
export default helper(selectableKeyValues);

View File

@ -7,6 +7,10 @@ export default class UpstreamsRoute extends Route {
as: 'filter', as: 'filter',
replace: true, replace: true,
}, },
searchproperty: {
as: 'searchproperty',
empty: [['DestinationName', 'LocalBindAddress', 'LocalBindPort']],
},
}; };
model() { model() {
@ -14,7 +18,10 @@ export default class UpstreamsRoute extends Route {
.split('.') .split('.')
.slice(0, -1) .slice(0, -1)
.join('.'); .join('.');
return this.modelFor(parent); return {
...this.modelFor(parent),
searchProperties: this.queryParams.searchproperty.empty[0],
};
} }
setupController(controller, model) { setupController(controller, model) {

View File

@ -1,21 +0,0 @@
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
);
});
}

View File

@ -1,14 +0,0 @@
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
);
});
}

View File

@ -1,20 +0,0 @@
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 ||
(get(item, 'Policies') || []).some(function(item) {
return item.Name.toLowerCase().indexOf(sLower) !== -1;
}) ||
(get(item, 'ServiceIdentities') || []).some(function(item) {
return item.ServiceName.toLowerCase().indexOf(sLower) !== -1;
})
);
});
}

View File

@ -1,15 +0,0 @@
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 ||
`${get(item, 'Service.Address')}:${get(item, 'Service.Port')}`.indexOf(term) !== -1
);
});
}

View File

@ -1,12 +1,10 @@
export default () => term => item => { export default {
const lowerTerm = term.toLowerCase(); DestinationName: (item, value) =>
return Object.entries(item) item.DestinationName.toLowerCase().indexOf(value.toLowerCase()) !== -1,
.filter(([key, value]) => key !== 'DestinationType') LocalBindAddress: (item, value) =>
.some( item.LocalBindAddress.toLowerCase().indexOf(value.toLowerCase()) !== -1,
([key, value]) => LocalBindPort: (item, value) =>
value item.LocalBindPort.toString()
.toString() .toLowerCase()
.toLowerCase() .indexOf(value.toLowerCase()) !== -1,
.indexOf(lowerTerm) !== -1
);
}; };

View File

@ -14,20 +14,6 @@ import role from 'consul-ui/search/predicates/role';
import policy from 'consul-ui/search/predicates/policy'; import policy from 'consul-ui/search/predicates/policy';
import nspace from 'consul-ui/search/predicates/nspace'; import nspace from 'consul-ui/search/predicates/nspace';
import filteredRole from 'consul-ui/search/filters/role';
import filteredPolicy from 'consul-ui/search/filters/policy';
// service instance
import nodeService from 'consul-ui/search/filters/node/service';
import serviceNode from 'consul-ui/search/filters/service/node';
import filterableFactory from 'consul-ui/utils/search/filterable';
const filterable = filterableFactory();
const searchables = {
serviceInstance: serviceNode(filterable),
nodeservice: nodeService(filterable),
role: filteredRole(filterable),
policy: filteredPolicy(filterable),
};
export const search = spec => { export const search = spec => {
let possible = Object.keys(spec); let possible = Object.keys(spec);
return (term, options = {}) => { return (term, options = {}) => {
@ -47,7 +33,7 @@ const predicates = {
intention: search(intention), intention: search(intention),
service: search(service), service: search(service),
['service-instance']: search(serviceInstance), ['service-instance']: search(serviceInstance),
['upstream-instance']: upstreamInstance(), ['upstream-instance']: search(upstreamInstance),
['health-check']: search(healthCheck), ['health-check']: search(healthCheck),
node: search(node), node: search(node),
kv: search(kv), kv: search(kv),
@ -58,9 +44,6 @@ const predicates = {
nspace: search(nspace), nspace: search(nspace),
}; };
export default class SearchService extends Service { export default class SearchService extends Service {
searchable(name) {
return searchables[name];
}
predicate(name) { predicate(name) {
return predicates[name]; return predicates[name];
} }

View File

@ -1,37 +1,53 @@
<div class="tab-section"> <div class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
{{#let (or sortBy "DestinationName:asc") as |sort|}} {{#let (hash
{{#if (gt proxy.Service.Proxy.Upstreams.length 0)}} searchproperties=(if (not-eq searchproperty undefined)
<input type="checkbox" id="toolbar-toggle" /> (split searchproperty ',')
<Consul::UpstreamInstance::SearchBar searchProperties
@search={{search}} )
@onsearch={{action (mut search) value="target.value"}} ) as |filters|}}
{{#let (or sortBy "DestinationName:asc") as |sort|}}
{{#if (gt proxy.Service.Proxy.Upstreams.length 0)}}
<input type="checkbox" id="toolbar-toggle" />
<Consul::UpstreamInstance::SearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@searchproperties={{searchProperties}}
@sort={{sort}} @sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}} @onsort={{action (mut sortBy) value="target.selected"}}
/>
<Consul::UpstreamInstance::List @filter={{filters}}
@search={{search}} @onfilter={{hash
@sort={{sort}} searchproperty=(action (mut searchproperty) value="target.selectedItems")
@items={{proxy.Service.Proxy.Upstreams}} }}
@dc={{dc}} />
@nspace={{nspace}} <DataCollection
> @type="upstream-instance"
<:empty> @sort={{sort}}
<EmptyState> @filters={{filters}}
<BlockSlot @name="body"> @search={{search}}
<p> @items={{proxy.Service.Proxy.Upstreams}}
{{#if search.length}} as |collection|>
No upstreams where found matching that search. <collection.Collection>
{{else}} <Consul::UpstreamInstance::List
This service has no upstreams. @items={{collection.items}}
{{/if}} @dc={{dc}}
</p> @nspace={{nspace}}
</BlockSlot> />
</EmptyState> </collection.Collection>
</:empty> <collection.Empty>
</Consul::UpstreamInstance::List> <EmptyState>
{{/if}} <BlockSlot @name="body">
<p>
This service has no upstreams{{#if (gt proxy.Service.Proxy.Upstreams.length 0)}} matching that search{{/if}}.
</p>
</BlockSlot>
</EmptyState>
</collection.Empty>
</DataCollection>
{{/if}}
{{/let}}
{{/let}} {{/let}}
</div> </div>
</div> </div>

View File

@ -1,49 +0,0 @@
import RSVP from 'rsvp';
export default function(EventTarget = RSVP.EventTarget, P = Promise) {
// TODO: Class-ify
return function(filter) {
return EventTarget.mixin({
value: '',
_data: [],
add: function(data) {
this.data = this._data = data;
return this;
},
find: function(terms = []) {
this.value = terms
.filter(function(item) {
return typeof item === 'string' && item !== '';
})
.map(function(term) {
return term.trim();
});
return P.resolve(
this.value.reduce(function(prev, term) {
return prev.filter(item => {
return filter(item, { s: term });
});
}, this._data)
);
},
search: function(terms = []) {
// specifically no return here we return `this` instead
// right now filtering is sync but we introduce an async
// flow now for later on
this.find(Array.isArray(terms) ? terms : [terms]).then(data => {
// TODO: For the moment, lets just fake a target
this.data = data;
this.trigger('change', {
target: {
value: this.value.join('\n'),
// TODO: selectedOptions is what <select> uses, consider that
data: data,
},
});
// not returned
return data;
});
return this;
},
});
};
}

View File

@ -1,24 +0,0 @@
import { module, skip } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | changeable set', function(hooks) {
setupRenderingTest(hooks);
skip('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
await render(hbs`{{changeable-set}}`);
assert.equal(this.element.textContent.trim(), '');
// Template block usage:
await render(hbs`
{{#changeable-set}}
{{/changeable-set}}
`);
assert.equal(this.element.textContent.trim(), '');
});
});

View File

@ -1,49 +0,0 @@
import { selectableKeyValues } from 'consul-ui/helpers/selectable-key-values';
import { module, test } from 'qunit';
module('Unit | Helper | selectable-key-values', function() {
test('it turns arrays into key values and selects the first item by default', function(assert) {
const actual = selectableKeyValues([
['key-1', 'value-1'],
['key-2', 'value-2'],
]);
assert.equal(actual.items.length, 2);
assert.deepEqual(actual.selected, { key: 'key-1', value: 'value-1' });
});
test('it turns arrays into key values and selects the defined key', function(assert) {
const actual = selectableKeyValues(
[
['key-1', 'value-1'],
['key-2', 'value-2'],
],
{
selected: 'key-2',
}
);
assert.equal(actual.items.length, 2);
assert.deepEqual(actual.selected, { key: 'key-2', value: 'value-2' });
});
test('it turns arrays into key values and selects the defined index', function(assert) {
const actual = selectableKeyValues(
[
['key-1', 'value-1'],
['key-2', 'value-2'],
],
{
selected: 1,
}
);
assert.equal(actual.items.length, 2);
assert.deepEqual(actual.selected, { key: 'key-2', value: 'value-2' });
});
test('it turns arrays with only one element into key values and selects the defined index', function(assert) {
const actual = selectableKeyValues([['Value 1'], ['Value 2']], { selected: 1 });
assert.equal(actual.items.length, 2);
assert.deepEqual(actual.selected, { key: 'value-2', value: 'Value 2' });
});
test('it turns strings into key values and selects the defined index', function(assert) {
const actual = selectableKeyValues(['Value 1', 'Value 2'], { selected: 1 });
assert.equal(actual.items.length, 2);
assert.deepEqual(actual.selected, { key: 'value-2', value: 'Value 2' });
});
});

View File

@ -1,94 +0,0 @@
import getFilter from 'consul-ui/search/filters/node/service';
import { module, test } from 'qunit';
module('Unit | Search | Filter | node/service', function() {
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.notOk(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.notOk(actual);
});
});
});

View File

@ -1,36 +0,0 @@
import getFilter from 'consul-ui/search/filters/policy';
import { module, test } from 'qunit';
module('Unit | Search | Filter | policy', function() {
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.notOk(actual);
});
});
});

View File

@ -1,74 +0,0 @@
import getFilter from 'consul-ui/search/filters/service/node';
import { module, test } from 'qunit';
module('Unit | Search | Filter | service/node', function() {
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 found by address:port', function(assert) {
const instance = {
Service: {
ID: 'id',
Address: '0.0.0.0',
Port: 8000,
},
Node: {
Node: 'node-0',
},
};
['0.0.0.0', '8000', '0:8000', '0.0.0.0:8000'].forEach(function(item) {
let actual = filter(instance, {
s: item,
});
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.notOk(actual);
});
});
});

View File

@ -0,0 +1,29 @@
import predicates from 'consul-ui/search/predicates/policy';
import { search as create } from 'consul-ui/services/search';
import { module, test } from 'qunit';
module('Unit | Search | Predicate | policy', function() {
const search = create(predicates);
test('items are found by properties', function(assert) {
const actual = [
{
Name: 'name-HIT',
Description: 'description',
},
{
Name: 'name',
Description: 'desc-HIT-ription',
},
].filter(search('hit'));
assert.equal(actual.length, 2);
});
test('items are not found', function(assert) {
const actual = [
{
Name: 'name',
Description: 'description',
},
].filter(search('hit'));
assert.equal(actual.length, 0);
});
});

View File

@ -1,10 +1,11 @@
import getFilter from 'consul-ui/search/filters/role'; import predicates from 'consul-ui/search/predicates/role';
import { search as create } from 'consul-ui/services/search';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
module('Unit | Search | Filter | role', function() { module('Unit | Search | Predicate | role', function() {
const filter = getFilter(cb => cb); const search = create(predicates);
test('items are found by properties', function(assert) { test('items are found by properties', function(assert) {
[ const actual = [
{ {
Name: 'name-HIT', Name: 'name-HIT',
Description: 'description', Description: 'description',
@ -28,15 +29,11 @@ module('Unit | Search | Filter | role', function() {
{ ServiceName: 'service-identity-HIT' }, { ServiceName: 'service-identity-HIT' },
], ],
}, },
].forEach(function(item) { ].filter(search('hit'));
const actual = filter(item, { assert.equal(actual.length, 4);
s: 'hit',
});
assert.ok(actual);
});
}); });
test('items are not found', function(assert) { test('items are not found', function(assert) {
[ const actual = [
{ {
Name: 'name', Name: 'name',
Description: 'description', Description: 'description',
@ -53,15 +50,11 @@ module('Unit | Search | Filter | role', function() {
Description: 'description', Description: 'description',
ServiceIdenitities: [{ ServiceName: 'si' }, { ServiceName: 'si-second' }], ServiceIdenitities: [{ ServiceName: 'si' }, { ServiceName: 'si-second' }],
}, },
].forEach(function(item) { ].filter(search('hit'));
const actual = filter(item, { assert.equal(actual.length, 0);
s: 'hit',
});
assert.notOk(actual);
});
}); });
test('arraylike things can be empty', function(assert) { test('arraylike things can be empty', function(assert) {
[ const actual = [
{ {
Name: 'name', Name: 'name',
Description: 'description', Description: 'description',
@ -79,11 +72,7 @@ module('Unit | Search | Filter | role', function() {
Policies: [], Policies: [],
ServiceIdentities: [], ServiceIdentities: [],
}, },
].forEach(function(item) { ].filter(search('hit'));
const actual = filter(item, { assert.equal(actual.length, 0);
s: 'hit',
});
assert.notOk(actual);
});
}); });
}); });

View File

@ -1,10 +0,0 @@
import searchFilterable from 'consul-ui/utils/search/filterable';
import { module, test } from 'qunit';
module('Unit | Utility | search/filterable', function() {
// Replace this with your real tests.
test('it works', function(assert) {
let result = searchFilterable();
assert.ok(result);
});
});