ui: Remove WithSearching mixin, use helpers instead (#7961)

* ui: Remove WithSearching mixin, use composable helpers instead
This commit is contained in:
John Cowen 2020-05-29 16:42:46 +01:00 committed by John Cowen
parent d459bfd81c
commit 002797af82
81 changed files with 639 additions and 876 deletions

View File

@ -1,4 +0,0 @@
{{!<form>}}
<FreetextFilter @searchable={{searchable}} @value={{search}} @placeholder="Search by name/token" />
<RadioGroup @keyboardAccess={{true}} @name="type" @value={{type}} @items={{filters}} @onchange={{action onchange}} />
{{!</form>}}

View File

@ -1,8 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'form',
classNames: ['filter-bar'],
'data-test-acl-filter': true,
onchange: function() {},
});

View File

@ -1,4 +0,0 @@
{{!<form>}}
<FreetextFilter @searchable={{searchable}} @value={{search}} @placeholder="Search" />
<RadioGroup @keyboardAccess={{true}} @name="status" @value={{status}} @items={{array (hash label="All (Any Status)" value="") (hash label="Critical Checks" value="critical") (hash label="Warning Checks" value="warning") (hash label="Passing Checks" value="passing")}} @onchange={{action onchange}} />
{{!</form>}}

View File

@ -1,8 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'form',
classNames: ['filter-bar'],
'data-test-catalog-filter': true,
onchange: function() {},
});

View File

@ -1,10 +0,0 @@
<form class="catalog-toolbar" data-test-catalog-toolbar>
<FreetextFilter @searchable={{searchable}} @value={{value}} @placeholder="Search" />
<PopoverSelect
data-popover-select
@selected={{selected}}
@options={{options}}
@onchange={{onchange}}
@title='Sort By'
/>
</form>

View File

@ -1,5 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});

View File

@ -1,19 +1,28 @@
import Component from '@ember/component';
import { get, set } from '@ember/object';
import SlotsMixin from 'block-slots';
import WithListeners from 'consul-ui/mixins/with-listeners';
import { inject as service } from '@ember/service';
import Slotted from 'block-slots';
export default Component.extend(WithListeners, SlotsMixin, {
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);
this.removeListeners();
const dispatcher = this.dispatcher;
if (dispatcher) {
this.listen(dispatcher, 'change', e => {
set(this, 'items', e.target.data);
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(dispatcher, 'data'));
set(this, 'items', get(this.dispatcher, 'data'));
}
this.dispatcher.search(this.terms);
},
});

View File

@ -1,6 +1,6 @@
{{!<fieldset>}}
<fieldset class="freetext-filter">
<label class="type-search">
<span>Search</span>
<input type="search" onsearch={{action onchange}} oninput={{action onchange}} name="s" value="{{value}}" placeholder="{{placeholder}}" autofocus="autofocus" />
<input type="search" onsearch={{action "change"}} oninput={{action "change"}} onkeydown={{action "keydown"}} name="s" value={{value}} placeholder={{placeholder}} autofocus="autofocus" />
</label>
{{!</fieldset>}}
</fieldset>

View File

@ -1,14 +1,19 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
const ENTER = 13;
export default Component.extend({
tagName: 'fieldset',
classNames: ['freetext-filter'],
onchange: function(e) {
let searchable = this.searchable;
if (!Array.isArray(searchable)) {
searchable = [searchable];
}
searchable.forEach(function(item) {
item.search(e.target.value);
});
dom: service('dom'),
tagName: '',
actions: {
change: function(e) {
this.onsearch(
this.dom.setEventTargetProperty(e, 'value', value => (value === '' ? undefined : value))
);
},
keydown: function(e) {
if (e.keyCode === ENTER) {
e.preventDefault();
}
},
},
});

View File

@ -1,4 +0,0 @@
{{!<form>}}
<FreetextFilter @searchable={{searchable}} @value={{search}} @placeholder="Search by Source or Destination" />
<RadioGroup @keyboardAccess={{true}} @name="currentFilter" @value={{selected}} @items={{filters}} @onchange={{action onchange}} />
{{!</form>}}

View File

@ -1,8 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'form',
classNames: ['filter-bar'],
'data-test-intention-filter': true,
onchange: function() {},
});

View File

@ -1,19 +1,12 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({
tagName: '',
dom: service('dom'),
actions: {
change: function(option, e) {
// We fake an event here, which could be a bit of a footbun if we treat
// it completely like an event, we should be abe to avoid doing this
// when we move to glimmer components (this.args.selected vs this.selected)
this.onchange({
target: {
selected: option,
},
// make this vaguely event like to avoid
// having a separate property
preventDefault: function(e) {},
});
this.onchange(this.dom.setEventTargetProperty(e, 'selected', selected => option));
},
},
});

View File

@ -1,10 +1,12 @@
<fieldset>
<div role="radiogroup" id="radiogroup_{{name}}" data-test-radiogroup={{name}}>{{! menu?}}
{{#each items as |item|}}
<label tabindex={{if keyboardAccess '0'}} onkeydown={{if keyboardAccess (action 'keydown')}} class="type-radio value-{{item.value}}" data-test-radiobutton="{{name}}_{{item.value}}"> {{! slugify }}
<input type="radio" name={{name}} value={{item.value}} checked={{if (eq (concat value) item.value) 'checked'}} onchange={{action onchange}} />
<span>{{item.label}}</span>
{{#let (if (not-eq item.key undefined) item.key item.value) (or item.label item.value) as |_key _value|}}
<label tabindex={{if keyboardAccess '0'}} onkeydown={{if keyboardAccess (action 'keydown')}} class="type-radio value-{{_key}}" data-test-radiobutton="{{name}}_{{_key}}"> {{! slugify }}
<input type="radio" name={{name}} value={{_key}} checked={{if (eq (concat value) _key) 'checked'}} onchange={{action "change"}} />
<span>{{_value}}</span>
</label>
{{/let}}
{{/each}}
</div>
</fieldset>

View File

@ -1,14 +1,25 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
const ENTER = 13;
export default Component.extend({
tagName: '',
keyboardAccess: false,
dom: service('dom'),
init: function() {
this._super(...arguments);
this.name = this.dom.guid(this);
},
actions: {
keydown: function(e) {
if (e.keyCode === ENTER) {
e.target.dispatchEvent(new MouseEvent('click'));
}
},
change: function(e) {
this.onchange(
this.dom.setEventTargetProperty(e, 'value', value => (value === '' ? undefined : value))
);
},
},
});

View File

@ -0,0 +1,61 @@
## SearchBar
```handlebars
<SearchBar
@value={{"search term"}}
@onsearch={{action "search"}}
/>
```
### Arguments
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `String` | | The string `value` of the freetext search bar |
| `onsearch` | `Function` | | The action to fire when the freetext search bar changes. Emits a native event with a `target.value` property containing the text typed into the search bar |
| `options` | `Array` | | An array of Key/Values pairs to use for options for either a filter interface or a sort interface |
| `selected` | `Object` | | An object containing a Key/Value pair of the currently selected option |
| `onchange` | `Function` | | The action to fire when the filter/sort changes. Emits an Event-like object, when filtering this has a `target.value` property containg the key of the selected filter, when sorting this has a `target.selected` property containing the selected Key/Value pair |
| `secondary` | `string` | | String identifier to signify what type of secondary filter to show. Currently only value here is 'sort' |
`SearchBar` is used for a variety of searching behaviours, freetext searching, filtering and sorting. It is also slot based to enable you to completely overwrite the secondary search if need be.
### Examples
```handlebars
{{! Freetext only search bar}}
<SearchBar
@value={{"search term"}}
@onsearch={{action "search"}}
/>
```
```handlebars
{{! Freetext and filter search bar}}
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value='target.value'}}
@selected={{filter.selected}}
@options={{filter.items}}
@onchange={{action (mut filterBy) value='target.value'}}
/>
```
```handlebars
{{! Freetext and sort search bar}}
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value='target.value'}}
@secondary="sort"
@selected={{sort.selected}}
@options={{sort.items}}
@onchange={{action (mut sortBy) value='target.selected.key'}}
/>
```
### See
- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)
---

View File

@ -0,0 +1,32 @@
{{yield}}
<form class={{concat 'filter-bar' (if (eq secondary 'sort') ' with-sort')}} ...attributes>
<FreetextFilter
@onsearch={{action onsearch}}
@value={{value}}
@placeholder={{or placeholder 'Search'}}
/>
{{#yield-slot name="secondary"}}
{{yield}}
{{else}}
{{#if options}}
{{#if (eq secondary 'sort')}}
<fieldset>
<PopoverSelect
data-popover-select
@selected={{selected}}
@options={{options}}
@onchange={{action onchange}}
@title="Sort By"
/>
</fieldset>
{{else}}
<RadioGroup
@keyboardAccess={{true}}
@value={{selected.key}}
@items={{options}}
@onchange={{action onchange}}
/>
{{/if}}
{{/if}}
{{/yield-slot}}
</form>

View File

@ -0,0 +1,6 @@
import Component from '@ember/component';
import Slotted from 'block-slots';
export default Component.extend(Slotted, {
tagName: '',
});

View File

@ -1,47 +1,14 @@
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';
const countType = function(items, type) {
return type === '' ? get(items, 'length') : items.filterBy('Type', type).length;
};
export default Controller.extend(WithSearching, WithFiltering, {
export default Controller.extend({
queryParams: {
type: {
filterBy: {
as: 'type',
},
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
acl: 's',
};
this._super(...arguments);
},
searchable: computed('filtered', function() {
return get(this, 'searchables.acl')
.add(get(this, 'filtered'))
.search(get(this, this.searchParams.acl));
}),
typeFilters: computed('items', function() {
const items = get(this, 'items');
return ['', 'management', 'client'].map(function(item) {
return {
label: `${item === '' ? 'All' : ucfirst(item)} (${countType(
items,
item
).toLocaleString()})`,
value: item,
};
});
}),
filter: function(item, { type = '' }) {
return type === '' || get(item, 'Type') === type;
},
actions: {
sendClone: function(item) {
this.send('clone', item);

View File

@ -1,23 +1,9 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
policy: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.policy')
.add(this.items)
.search(get(this, this.searchParams.policy));
}),
actions: {},
});

View File

@ -1,23 +1,9 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
role: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.role')
.add(this.items)
.search(get(this, this.searchParams.role));
}),
actions: {},
});

View File

@ -1,24 +1,11 @@
import Controller from '@ember/controller';
import { computed, get } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
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);

View File

@ -1,52 +1,15 @@
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 WithEventSource from 'consul-ui/mixins/with-event-source';
import ucfirst from 'consul-ui/utils/ucfirst';
// TODO: DRY out in acls at least
const createCounter = function(prop) {
return function(items, val) {
return val === '' ? get(items, 'length') : items.filterBy(prop, val).length;
};
};
const countAction = createCounter('Action');
export default Controller.extend(WithSearching, WithFiltering, WithEventSource, {
export default Controller.extend(WithEventSource, {
queryParams: {
currentFilter: {
filterBy: {
as: 'action',
},
s: {
search: {
as: 'filter',
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) {
return {
label: `${item === '' ? 'All' : ucfirst(item)} (${countAction(
items,
item
).toLocaleString()})`,
value: item,
};
});
}),
filter: function(item, { s = '', currentFilter = '' }) {
return currentFilter === '' || get(item, 'Action') === currentFilter;
},
actions: {
route: function() {
this.send(...arguments);

View File

@ -1,22 +1,9 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
kv: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.kv')
.add(this.items)
.search(get(this, this.searchParams.kv));
}),
});

View File

@ -1,38 +1,28 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
import { get } from '@ember/object';
export default Controller.extend(WithEventSource, WithSearching, WithHealthFiltering, {
init: function() {
this.searchParams = {
healthyNode: 's',
unhealthyNode: 's',
};
this._super(...arguments);
export default Controller.extend(WithEventSource, {
queryParams: {
filterBy: {
as: 'status',
},
search: {
as: 'filter',
replace: true,
},
},
searchableHealthy: computed('healthy', function() {
return get(this, 'searchables.healthyNode')
.add(this.healthy)
.search(get(this, this.searchParams.healthyNode));
}),
searchableUnhealthy: computed('unhealthy', function() {
return get(this, 'searchables.unhealthyNode')
.add(this.unhealthy)
.search(get(this, this.searchParams.unhealthyNode));
}),
unhealthy: computed('filtered', function() {
return this.filtered.filter(function(item) {
return get(item, 'isUnhealthy');
});
}),
healthy: computed('filtered', function() {
return this.filtered.filter(function(item) {
return get(item, 'isHealthy');
});
}),
filter: function(item, { s = '', status = '' }) {
return item.hasStatus(status);
actions: {
hasStatus: function(status, checks) {
if (status === '') {
return true;
}
return checks.some(item => item.Status === status);
},
isHealthy: function(checks) {
return !this.actions.isUnhealthy.apply(this, [checks]);
},
isUnhealthy: function(checks) {
return checks.some(item => item.Status === 'critical' || item.Status === 'warning');
},
},
});

View File

@ -1,27 +1,12 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { get, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
dom: service('dom'),
export default Controller.extend({
items: alias('item.Services'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
nodeservice: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.nodeservice')
.add(this.items)
.search(get(this, this.searchParams.nodeservice));
}),
});

View File

@ -1,23 +1,10 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithEventSource, WithSearching, {
export default Controller.extend(WithEventSource, {
queryParams: {
s: {
search: {
as: 'filter',
},
},
init: function() {
this.searchParams = {
nspace: 's',
};
this._super(...arguments);
},
searchable: computed('items.[]', function() {
return get(this, 'searchables.nspace')
.add(this.items)
.search(get(this, this.searchParams.nspace));
}),
});

View File

@ -1,25 +1,13 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import { computed } from '@ember/object';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithEventSource, WithSearching, {
export default Controller.extend(WithEventSource, {
queryParams: {
sortBy: 'sort',
s: {
search: {
as: 'filter',
},
},
init: function() {
this.searchParams = {
service: 's',
};
this._super(...arguments);
},
searchable: computed('services.[]', function() {
return get(this, 'searchables.service')
.add(this.services)
.search(this.terms);
}),
services: computed('items.[]', function() {
return this.items.filter(function(item) {
return item.Kind !== 'connect-proxy';

View File

@ -1,29 +1,15 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
dom: service('dom'),
export default Controller.extend({
items: alias('item.Nodes'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
serviceInstance: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.serviceInstance')
.add(this.items)
.search(get(this, this.searchParams.serviceInstance));
}),
keyedProxies: computed('proxies.[]', function() {
const proxies = {};
this.proxies.forEach(item => {

View File

@ -1,25 +1,15 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
filterBy: {
as: 'action',
},
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
intention: 's',
};
this._super(...arguments);
},
searchable: computed('intentions', function() {
return get(this, 'searchables.intention')
.add(this.intentions)
.search(get(this, this.searchParams.intention));
}),
actions: {
route: function() {
this.send(...arguments);

View File

@ -0,0 +1,9 @@
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,49 +0,0 @@
import Mixin from '@ember/object/mixin';
import { computed, get, set } from '@ember/object';
const toKeyValue = function(el) {
const key = el.name;
let value = '';
switch (el.type) {
case 'radio':
value = el.value === 'on' ? '' : el.value;
break;
case 'search':
case 'text':
value = el.value;
break;
}
return { [key]: value };
};
export default Mixin.create({
filters: {},
filtered: computed('items.[]', 'filters', function() {
const filters = get(this, 'filters');
return get(this, 'items').filter(item => {
return this.filter(item, filters);
});
}),
setProperties: function() {
this._super(...arguments);
const query = get(this, 'queryParams');
query.forEach((item, i, arr) => {
const filters = get(this, 'filters');
Object.keys(item).forEach(key => {
set(filters, key, get(this, key));
});
set(this, 'filters', filters);
});
},
actions: {
filter: function(e) {
const obj = toKeyValue(e.target);
Object.keys(obj).forEach((key, i, arr) => {
set(this, key, obj[key] != '' ? obj[key] : null);
});
set(this, 'filters', {
...this.filters,
...obj,
});
},
},
});

View File

@ -1,13 +0,0 @@
import Mixin from '@ember/object/mixin';
import WithFiltering from 'consul-ui/mixins/with-filtering';
export default Mixin.create(WithFiltering, {
queryParams: {
status: {
as: 'status',
},
s: {
as: 'filter',
},
},
});

View File

@ -1,32 +0,0 @@
import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember/service';
import { 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] = this.builder.searchable(type);
this.listen(this.searchables[type], 'change', e => {
const value = e.target.value;
set(this, key, value === '' ? null : value);
});
});
},
});

View File

@ -1,8 +1,5 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { computed, get } from '@ember/object';
import sumOfUnhealthy from 'consul-ui/utils/sumOfUnhealthy';
import hasStatus from 'consul-ui/utils/hasStatus';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'ID';
@ -23,13 +20,4 @@ export default Model.extend({
Coord: attr(),
SyncTime: attr('number'),
meta: attr(),
hasStatus: function(status) {
return hasStatus(get(this, 'Checks'), status);
},
isHealthy: computed('Checks', function() {
return sumOfUnhealthy(get(this, 'Checks')) === 0;
}),
isUnhealthy: computed('Checks', function() {
return sumOfUnhealthy(get(this, 'Checks')) > 0;
}),
});

View File

@ -9,7 +9,7 @@ export default Route.extend(WithAclActions, {
repo: service('repository/acl'),
settings: service('settings'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -7,7 +7,7 @@ import WithPolicyActions from 'consul-ui/mixins/policy/with-actions';
export default Route.extend(WithPolicyActions, {
repo: service('repository/policy'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -7,7 +7,7 @@ import WithRoleActions from 'consul-ui/mixins/role/with-actions';
export default Route.extend(WithRoleActions, {
repo: service('repository/role'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -7,7 +7,7 @@ export default Route.extend(WithTokenActions, {
repo: service('repository/token'),
settings: service('settings'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -7,10 +7,10 @@ import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
export default Route.extend(WithIntentionActions, {
repo: service('repository/intention'),
queryParams: {
currentFilter: {
filterBy: {
as: 'action',
},
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -7,7 +7,7 @@ import WithKvActions from 'consul-ui/mixins/kv/with-actions';
export default Route.extend(WithKvActions, {
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -5,7 +5,7 @@ import { hash } from 'rsvp';
export default Route.extend({
repo: service('repository/node'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -1,6 +1,12 @@
import Route from '@ember/routing/route';
export default Route.extend({
queryParams: {
search: {
as: 'filter',
replace: true,
},
},
model: function() {
const parent = this.routeName
.split('.')

View File

@ -6,7 +6,7 @@ import WithNspaceActions from 'consul-ui/mixins/nspace/with-actions';
export default Route.extend(WithNspaceActions, {
repo: service('repository/nspace'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -5,7 +5,7 @@ import { hash } from 'rsvp';
export default Route.extend({
repo: service('repository/service'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
export default Route.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View File

@ -3,6 +3,12 @@ import { inject as service } from '@ember/service';
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
export default Route.extend(WithIntentionActions, {
queryParams: {
search: {
as: 'filter',
replace: true,
},
},
repo: service('repository/intention'),
model: function() {
const parent = this.routeName

View File

@ -51,6 +51,24 @@ export default Service.extend({
sibling: sibling,
isOutside: isOutside,
normalizeEvent: normalizeEvent,
setEventTargetProperty: function(e, property, cb) {
const target = e.target;
return new Proxy(e, {
get: function(obj, prop, receiver) {
if (prop === 'target') {
return new Proxy(target, {
get: function(obj, prop, receiver) {
if (prop === property) {
return cb(e.target[property]);
}
return target[prop];
},
});
}
return Reflect.get(...arguments);
},
});
},
listeners: createListeners,
root: function() {
return this.doc.documentElement;

View File

@ -73,7 +73,7 @@
}
/* */
/* TODO: Think about an %app-form or similar */
%app-view-content fieldset:not(.freetext-filter) {
%app-view-content form:not(.filter-bar) fieldset {
padding-bottom: 0.3em;
margin-bottom: 2em;
}

View File

@ -1,3 +1,8 @@
%expanded-single-select {
border: $decor-border-100;
border-color: $gray-300;
border-radius: $decor-radius-100;
}
%expanded-single-select label {
cursor: pointer;
}

View File

@ -3,11 +3,8 @@
.filter-bar {
@extend %filter-bar;
}
.catalog-toolbar {
@extend %catalog-toolbar;
}
%catalog-toolbar {
@extend %filter-bar;
%filter-bar:not(.with-sort) {
@extend %filter-bar-reversed;
}
%filter-bar [role='radiogroup'] {
@extend %expanded-single-select;

View File

@ -1,45 +1,38 @@
%filter-bar {
padding: 4px;
display: block;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
padding: 4px 8px;
margin-top: 0 !important;
margin-bottom: -12px;
}
%filter-bar + :not(.notice) {
margin-top: 1.8em;
}
%catalog-toolbar {
padding: 4px 8px;
display: flex;
margin-top: 0 !important;
margin-bottom: -12px !important;
border-bottom: 1px solid $gray-200;
%filter-bar-reversed {
flex-direction: row-reverse;
padding: 4px;
margin-bottom: 8px !important;
}
@media #{$--horizontal-filters} {
%filter-bar {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
}
%catalog-toolbar {
flex-direction: row;
}
%filter-bar > *:first-child {
margin-left: 12px;
}
%catalog-toolbar > *:first-child {
margin-left: 0px;
}
%filter-bar fieldset {
min-width: 210px;
width: auto;
}
%catalog-toolbar fieldset {
min-width: none;
width: 100%;
}
%filter-bar fieldset {
flex: 0 1 auto;
width: auto;
}
%filter-bar fieldset:first-child:not(:last-child) {
flex: 1 1 auto;
}
%filter-bar-reversed fieldset:first-child:not(:last-child) {
flex: 0 1 auto;
margin-left: auto;
}
%filter-bar-reversed fieldset {
min-width: 210px;
width: auto;
}
%filter-bar-reversed > *:first-child {
margin-left: 12px;
}
@media #{$--lt-horizontal-filters} {
%filter-bar > *:first-child {
margin: 2px 0;
%filter-bar-reversed > *:first-child {
margin-left: 0;
}
}

View File

@ -1,10 +1,10 @@
// decoration/color
%filter-bar > * {
border: $decor-border-100;
border-radius: $decor-radius-100;
%filter-bar {
border-bottom: $decor-border-100;
border-color: $gray-200;
}
%catalog-toolbar > div {
border: none;
%filter-bar-reversed {
border-bottom: none;
}
// TODO: Move this elsewhere
@media #{$--horizontal-selects} {

View File

@ -1,5 +1,8 @@
%freetext-filter {
cursor: pointer;
border: $decor-border-100;
border-color: $gray-200;
border-radius: $decor-radius-100;
}
%freetext-filter input {
-webkit-appearance: none;

View File

@ -23,6 +23,9 @@
display: grid;
grid-auto-rows: 12px;
}
%card-grid li.empty {
grid-column: 1 / -1;
}
@media #{$--fixed-grid} {
%card-grid > ul,
%card-grid > ol {

View File

@ -1,3 +1,12 @@
{{#let (filter-by "Type" "client" items) as |client|}}
{{#let (selectable-key-values
(array "" (concat "All (" items.length ")"))
(array "management" (concat "Management (" (sub items.length client.length) ")"))
(array "client" (concat "Client (" client.length ")"))
selected=filterBy
)
as |filter|
}}
<AppView @class="acl list" @loading={{isLoading}}>
<BlockSlot @name="notification" as |status type|>
{{partial 'dc/acls/notifications'}}
@ -13,11 +22,18 @@
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0) }}
<AclFilter @searchable={{searchable}} @filters={{typeFilters}} @search={{filters.s}} @type={{filters.type}} @onchange={{action "filter"}} />
<SearchBar
data-test-acl-filter="true"
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
@selected={{filter.selected}}
@options={{filter.items}}
@onchange={{action (mut filterBy) value='target.value'}}
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'acl' (if (eq filter.selected.key "") items (filter-by "Type" filter.selected.key items))}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection @items={{sort-by "Name:asc" filtered}} as |item index|>
<BlockSlot @name="header">
@ -130,23 +146,27 @@
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>No ACLs</h2>
<h2>
{{#if (gt items.length 0)}}
No ACLs found
{{else}}
Welcome to ACLs
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
There don't seem to be any ACLs yet, or you may not have access to view ACLs yet.
{{#if (gt items.length 0)}}
No ACLs where found matching that search, or you may not have access to view the ACLs you are searching for.
{{else}}
There don't seem to be any ACLs yet, or you may not have access to view ACLs yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}" rel="noopener noreferrer" target="_blank">Read the documentation</a>
</li>
<li class="learn-link">
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/" rel="noopener noreferrer" target="_blank">Follow the guide</a>
</li>
</BlockSlot>
</EmptyState>
</BlockSlot>
</ChangeableSet>
</BlockSlot>
</AppView>
</AppView>
{{/let}}
{{/let}}

View File

@ -33,11 +33,12 @@
</BlockSlot>
<BlockSlot @name="content">
{{#if (gt items.length 0) }}
<form class="filter-bar">
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
</form>
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
/>
{{/if}}
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'policy' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection @items={{sort-by "CreateIndex:desc" "Name:asc" filtered}} as |item index|>
<BlockSlot @name="header">
@ -103,11 +104,21 @@
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>Welcome to Policies</h2>
<h2>
{{#if (gt items.length 0)}}
No policies found
{{else}}
Welcome to Policies
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
There don't seem to be any policies, or you may not have access to view policies yet.
{{#if (gt items.length 0)}}
No policies where found matching that search, or you may not have access to view the policies you are searching for.
{{else}}
There don't seem to be any policies, or you may not have access to view policies yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">

View File

@ -33,11 +33,12 @@
</BlockSlot>
<BlockSlot @name="content">
{{#if (gt items.length 0) }}
<form class="filter-bar">
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
</form>
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
/>
{{/if}}
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'role' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection @items={{sort-by "CreateIndex:desc" "Name:asc" filtered}} as |item index|>
<BlockSlot @name="header">
@ -98,11 +99,21 @@
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>Welcome to Roles</h2>
<h2>
{{#if (gt items.length 0)}}
No roles found
{{else}}
Welcome to Roles
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
There don't seem to be any roles, or you may not have access to view roles yet.
{{#if (gt items.length 0)}}
No roles where found matching that search, or you may not have access to view the roles you are searching for.
{{else}}
There don't seem to be any roles, or you may not have access to view roles yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">

View File

@ -33,14 +33,15 @@
</BlockSlot>
<BlockSlot @name="content">
{{#if (gt items.length 0) }}
<form class="filter-bar">
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
</form>
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
/>
{{/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_DOCS_URL'}}/guides/acl-migrate-tokens.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p>
{{/if}}
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'token' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection @items={{sort-by "CreateTime:desc" filtered}} as |item index|>
<BlockSlot @name="header">
@ -167,9 +168,26 @@
</TabularCollection>
</BlockSlot>
<BlockSlot @name="empty">
<p>
There are no Tokens.
</p>
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>
{{#if (gt items.length 0)}}
No tokens found
{{else}}
Welcome to ACL Tokens
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
{{#if (gt items.length 0)}}
No tokens where found matching that search, or you may not have access to view the tokens you are searching for.
{{else}}
There don't seem to be any tokens, or you may not have access to view tokens yet.
{{/if}}
</p>
</BlockSlot>
</EmptyState>
</BlockSlot>
</ChangeableSet>
</BlockSlot>

View File

@ -1,4 +1,13 @@
{{title 'Intentions'}}
{{#let (filter-by "Action" "deny" items) as |denied|}}
{{#let (selectable-key-values
(array "" (concat "All (" items.length ")"))
(array "allow" (concat "Allow (" (sub items.length denied.length) ")"))
(array "deny" (concat "Deny (" denied.length ")"))
selected=filterBy
)
as |filter|
}}
<AppView @class="intention list">
<BlockSlot @name="notification" as |status type|>
{{partial 'dc/intentions/notifications'}}
@ -14,11 +23,18 @@
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0) }}
<IntentionFilter @searchable={{searchable}} @selected={{currentFilter}} @filters={{actionFilters}} @search={{filters.s}} @type={{filters.action}} @onchange={{action "filter"}} />
<SearchBar
data-test-intention-filter="true"
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
@selected={{filter.selected}}
@options={{filter.items}}
@onchange={{action (mut filterBy) value='target.value'}}
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'intention' (if (eq filter.selected.key "") items (filter-by "Action" filter.selected.key items))}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<ConsulIntentionList
@items={{filtered}}
@ -28,11 +44,21 @@
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>Welcome to Intentions</h2>
<h2>
{{#if (gt items.length 0)}}
No intentions found
{{else}}
Welcome to Intentions
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
There don't seem to be any intentions, or you may not have access to view intentions yet.
{{#if (gt items.length 0)}}
No intentions where found matching that search, or you may not have access to view the intentions you are searching for.
{{else}}
There don't seem to be any intentions, or you may not have access to view intentions yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">
@ -47,4 +73,6 @@
</BlockSlot>
</ChangeableSet>
</BlockSlot>
</AppView>
</AppView>
{{/let}}
{{/let}}

View File

@ -25,9 +25,11 @@
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0) }}
<form class="filter-bar">
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search by name" />
</form>
<SearchBar
@placeholder="Search by name"
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="actions">
@ -38,7 +40,7 @@
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'kv' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection @items={{sort-by "isFolder:desc" "Key:asc" filtered}} as |item index|>
<BlockSlot @name="header">
@ -89,11 +91,21 @@
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>Welcome to Key/Value</h2>
<h2>
{{#if (gt items.length 0)}}
No K/V pairs found
{{else}}
Welcome to Key/Value
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
You don't have any K/V pairs, or you may not have access to view K/V pairs yet.
{{#if (gt items.length 0)}}
No K/V pairs where found matching that search, or you may not have access to view the K/V pairs you are searching for.
{{else}}
You don't have any K/V pairs, or you may not have access to view K/V pairs yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">

View File

@ -1,4 +1,13 @@
{{title 'Nodes'}}
{{#let (selectable-key-values
(array "" "All (Any Status)")
(array "critical" "Critical Checks")
(array "warning" "Warning Checks")
(array "passing" "Passing Checks")
selected=filterBy
)
as |filter|
}}
<AppView @class="node list">
<BlockSlot @name="header">
<h1>
@ -8,17 +17,35 @@
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0) }}
<CatalogFilter @searchable={{array searchableHealthy searchableUnhealthy}} @search={{s}} @status={{filters.status}} @onchange={{action "filter"}} />
<SearchBar
data-test-catalog-filter
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
@selected={{filter.selected}}
@options={{filter.items}}
@onchange={{action (mut filterBy) value='target.value'}}
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
{{#if (gt unhealthy.length 0) }}
{{#let (filter-by "Checks" (action "isUnhealthy") items) as |unhealthy|}}
{{#if (gt unhealthy.length 0) }}
<div class="unhealthy">
<h2>Unhealthy Nodes</h2>
<div>
{{! think about 2 differing views here }}
<ul>
<ChangeableSet @dispatcher={{searchableUnhealthy}}>
<ChangeableSet
@dispatcher={{
searchable
'unhealthyNode'
(if (eq filter.selected.key "")
unhealthy
(filter-by "Checks" (action "hasStatus" filter.selected.key) unhealthy filter.selected.key)
)
}}
@terms={{search}}
>
<BlockSlot @name="set" as |unhealthy|>
{{#each unhealthy as |item|}}
<HealthcheckedResource @tagName="li" @data-test-node={{item.Node}} @href={{href-to "dc.nodes.show" item.Node}} @name={{item.Node}} @address={{item.Address}} @checks={{item.Checks}}>
@ -31,19 +58,40 @@
{{/each}}
</BlockSlot>
<BlockSlot @name="empty">
<p>
There are no unhealthy nodes for that search.
</p>
<li class="empty">
<EmptyState>
<BlockSlot @name="header">
<h2>No nodes found</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
There don't seem to be any nodes matching that search.
</p>
</BlockSlot>
</EmptyState>
</li>
</BlockSlot>
</ChangeableSet>
</ul>
</div>
</div>
{{/if}}
{{#if (gt healthy.length 0) }}
{{/if}}
{{/let}}
{{#let (filter-by "Checks" (action "isHealthy") items) as |healthy|}}
{{#if (gt healthy.length 0) }}
<div class="healthy">
<h2>Healthy Nodes</h2>
<ChangeableSet @dispatcher={{searchableHealthy}}>
<ChangeableSet
@dispatcher={{
searchable
'healthyNode'
(if (eq filter.selected.key "")
healthy
(filter-by "Checks" (action "hasStatus" filter.selected.key) healthy filter.selected.key)
)
}}
@terms={{search}}
>
<BlockSlot @name="set" as |healthy|>
<GridCollection @cellHeight={{92}} @items={{healthy}} as |item index|>
<HealthcheckedResource @data-test-node={{item.Node}} @href={{href-to "dc.nodes.show" item.Node}} @name={{item.Node}} @address={{item.Address}} @checks={{item.Checks}}>
@ -56,14 +104,22 @@
</GridCollection>
</BlockSlot>
<BlockSlot @name="empty">
<p>
There are no healthy nodes for that search.
</p>
<EmptyState>
<BlockSlot @name="header">
<h2>No nodes found</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
There don't seem to be any nodes matching that search.
</p>
</BlockSlot>
</EmptyState>
</BlockSlot>
</ChangeableSet>
</div>
{{/if}}
{{#if (and (eq healthy.length 0) (eq unhealthy.length 0)) }}
{{/if}}
{{/let}}
{{#if (eq items.length 0) }}
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>Welcome to Nodes</h2>
@ -73,15 +129,8 @@
There don't seem to be any nodes, or you may not have access to view nodes yet.
</p>
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}" rel="noopener noreferrer" target="_blank">Documentation on nodes</a>
</li>
<li class="learn-link">
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/" rel="noopener noreferrer" target="_blank">Read the guide</a>
</li>
</BlockSlot>
</EmptyState>
{{/if}}
</BlockSlot>
</AppView>
</AppView>
{{/let}}

View File

@ -2,11 +2,13 @@
<div role="tabpanel">
{{#if (gt items.length 0) }}
<input type="checkbox" id="toolbar-toggle" />
<form class="filter-bar">
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search by name/port" />
</form>
<SearchBar
@placeholder="Search by name/port"
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
/>
{{/if}}
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'nodeservice' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection
data-test-services

View File

@ -13,11 +13,13 @@
</BlockSlot>
<BlockSlot @name="content">
{{#if (gt items.length 0) }}
<form class="filter-bar">
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
</form>
<SearchBar
@placeholder="Search by name"
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
/>
{{/if}}
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'nspace' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection @items={{filtered}} as |item index|>
<BlockSlot @name="header">
@ -92,11 +94,21 @@
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>Welcome to Namespaces</h2>
<h2>
{{#if (gt items.length 0)}}
No namespaces found
{{else}}
Welcome to Namespaces
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
There don't seem to be any namespaces, or you may not have access to view namespaces yet.
{{#if (gt items.length 0)}}
No namespaces where found matching that search, or you may not have access to view the namespaces you are searching for.
{{else}}
There don't seem to be any namespaces, or you may not have access to view namespaces yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">

View File

@ -12,15 +12,16 @@
</BlockSlot>
<BlockSlot @name="header">
<h1>
Services <em>{{format-number items.length}} total</em>
Services <em>{{format-number services.length}} total</em>
</h1>
<label for="toolbar-toggle"></label>
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0) }}
<CatalogToolbar
@searchable={{searchable}}
{{#if (gt services.length 0) }}
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
@secondary="sort"
@selected={{sort.selected}}
@options={{sort.items}}
@onchange={{action (mut sortBy) value='target.selected.key'}}
@ -28,18 +29,28 @@
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'service' (sort-by sort.selected.key services)}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<ConsulServiceList @routeName="dc.services.show" @items={{sort-by sort.selected.key filtered}} @proxies={{proxies}}/>
<ConsulServiceList @routeName="dc.services.show" @items={{filtered}} @proxies={{proxies}}/>
</BlockSlot>
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>Welcome to Services</h2>
<h2>
{{#if (gt services.length 0)}}
No services found
{{else}}
Welcome to Services
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
There don't seem to be any registered services, or you may not have access to view services yet.
{{#if (gt services.length 0)}}
No services where found matching that search, or you may not have access to view the services you are searching for.
{{else}}
There don't seem to be any registered services, or you may not have access to view services yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">

View File

@ -2,11 +2,12 @@
<div role="tabpanel">
{{#if (gt items.length 0) }}
<input type="checkbox" id="toolbar-toggle" />
<form class="filter-bar">
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
</form>
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
/>
{{/if}}
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet @dispatcher={{searchable 'serviceInstance' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<ConsulServiceInstanceList @routeName="dc.services.instance" @items={{filtered}} @proxies={{keyedProxies}}/>
</BlockSlot>

View File

@ -1,12 +1,32 @@
{{#let (filter-by "Action" "deny" intentions) as |denied|}}
{{#let (selectable-key-values
(array "" (concat "All (" intentions.length ")"))
(array "allow" (concat "Allow (" (sub intentions.length denied.length) ")"))
(array "deny" (concat "Deny (" denied.length ")"))
selected=filterBy
)
as |filter|
}}
<div id="intentions" class="tab-section">
<div role="tabpanel">
{{#if (gt intentions.length 0) }}
<input type="checkbox" id="toolbar-toggle" />
<form class="filter-bar">
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
</form>
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
@selected={{filter.selected}}
@options={{filter.items}}
@onchange={{action (mut filterBy) value='target.value'}}
/>
{{/if}}
<ChangeableSet @dispatcher={{searchable}}>
<ChangeableSet
@dispatcher={{
searchable
'intention'
(if (eq filter.selected.key "") intentions (filter-by "Action" filter.selected.key intentions))
}}
@terms={{search}}
>
<BlockSlot @name="set" as |filtered|>
<ConsulIntentionList
@items={{filtered}}
@ -21,3 +41,5 @@
</ChangeableSet>
</div>
</div>
{{/let}}
{{/let}}

View File

@ -1,15 +0,0 @@
import { get } from '@ember/object';
export default function(checks, status) {
let num = 0;
switch (status) {
case 'passing':
case 'critical':
case 'warning':
num = get(checks.filterBy('Status', status), 'length');
break;
case '': // all
num = 1;
break;
}
return num > 0;
}

View File

@ -1,7 +0,0 @@
import { get } from '@ember/object';
export default function(items) {
return items.reduce(function(sum, check) {
const status = get(check, 'Status');
return status === 'critical' || status === 'warning' ? sum + 1 : sum;
}, 0);
}

View File

@ -1,24 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | acl filter', function(hooks) {
setupRenderingTest(hooks);
test('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`{{acl-filter}}`);
assert.dom('*').hasText('Search');
// Template block usage:
await render(hbs`
{{#acl-filter}}{{/acl-filter}}
`);
assert.dom('*').hasText('Search');
});
});

View File

@ -1,24 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | catalog filter', function(hooks) {
setupRenderingTest(hooks);
test('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`{{catalog-filter}}`);
assert.equal(this.$().find('form').length, 1);
// Template block usage:
await render(hbs`
{{#catalog-filter}}{{/catalog-filter}}
`);
assert.equal(this.$().find('form').length, 1);
});
});

View File

@ -1,26 +0,0 @@
import { module, skip } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | catalog-toolbar', function(hooks) {
setupRenderingTest(hooks);
skip('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<CatalogToolbar />`);
assert.equal(this.element.querySelector('form').length, 1);
// Template block usage:
await render(hbs`
<CatalogToolbar>
template block text
</CatalogToolbar>
`);
assert.equal(this.element.textContent.trim(), 'template block text');
});
});

View File

@ -1,4 +1,4 @@
import { module, test } from 'qunit';
import { module, skip } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
@ -6,7 +6,7 @@ import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | changeable set', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
skip('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });

View File

@ -1,26 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | intention filter', function(hooks) {
setupRenderingTest(hooks);
test('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`{{intention-filter}}`);
assert.dom('*').hasText('Search');
// // Template block usage:
// this.render(hbs`
// {{#intention-filter}}
// template block text
// {{/intention-filter}}
// `);
// assert.equal(this.$().text().trim(), 'template block text');
});
});

View File

@ -0,0 +1,24 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | search-bar', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
this.set('search', function(e) {});
await render(hbs`<SearchBar @onsearch={{action search}}/>`);
assert.equal(this.element.textContent.trim(), 'Search');
// Template block usage:
await render(hbs`
<SearchBar @onsearch={{action search}}></SearchBar>
`);
assert.equal(this.element.textContent.trim(), 'Search');
});
});

View File

@ -0,0 +1,17 @@
import { module, skip } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Helper | searchable', function(hooks) {
setupRenderingTest(hooks);
// Replace this with your real tests.
skip('it renders', async function(assert) {
this.set('inputValue', '1234');
await render(hbs`{{searchable inputValue}}`);
assert.equal(this.element.textContent.trim(), '1234');
});
});

View File

@ -1,5 +1,7 @@
import { is, clickable } from 'ember-cli-page-object';
import ucfirst from 'consul-ui/utils/ucfirst';
// TODO: We no longer need to use name here
// remove the arg in all objects
export default function(name, items, blankKey = 'all') {
return items.reduce(function(prev, item, i, arr) {
// if item is empty then it means 'all'
@ -18,9 +20,9 @@ export default function(name, items, blankKey = 'all') {
...{
[`${key}IsSelected`]: is(
':checked',
`[data-test-radiobutton="${name}_${item}"] > input[type="radio"]`
`[data-test-radiobutton$="_${item}"] > input[type="radio"]`
),
[key]: clickable(`[data-test-radiobutton="${name}_${item}"]`),
[key]: clickable(`[data-test-radiobutton$="_${item}"]`),
},
};
}, {});

View File

@ -1,12 +0,0 @@
import EmberObject from '@ember/object';
import WithFilteringMixin from 'consul-ui/mixins/with-filtering';
import { module, test } from 'qunit';
module('Unit | Mixin | with filtering', function() {
// Replace this with your real tests.
test('it works', function(assert) {
let WithFilteringObject = EmberObject.extend(WithFilteringMixin);
let subject = WithFilteringObject.create();
assert.ok(subject);
});
});

View File

@ -1,12 +0,0 @@
import EmberObject from '@ember/object';
import WithHealthFilteringMixin from 'consul-ui/mixins/with-health-filtering';
import { module, test } from 'qunit';
module('Unit | Mixin | with health filtering', function() {
// Replace this with your real tests.
test('it works', function(assert) {
let WithHealthFilteringObject = EmberObject.extend(WithHealthFilteringMixin);
let subject = WithHealthFilteringObject.create();
assert.ok(subject);
});
});

View File

@ -1,23 +0,0 @@
import { module } from 'qunit';
import { setupTest } from 'ember-qunit';
import test from 'ember-sinon-qunit/test-support/test';
import Controller from '@ember/controller';
import Mixin from 'consul-ui/mixins/with-searching';
module('Unit | Mixin | with searching', function(hooks) {
setupTest(hooks);
hooks.beforeEach(function() {
this.subject = function() {
const MixedIn = Controller.extend(Mixin);
this.owner.register('test-container:with-searching-object', MixedIn);
return this.owner.lookup('test-container:with-searching-object');
};
});
// Replace this with your real tests.
test('it works', function(assert) {
const subject = this.subject();
assert.ok(subject);
});
});

View File

@ -1,19 +0,0 @@
import hasStatus from 'consul-ui/utils/hasStatus';
import { module, test, skip } from 'qunit';
module('Unit | Utility | has status', function() {
const checks = {
filterBy: function(prop, value) {
return { length: 0 };
},
};
test('it returns true when passing an empty string (therefore "all")', function(assert) {
assert.ok(hasStatus(checks, ''));
});
test('it returns false when passing an actual status', function(assert) {
['passing', 'critical', 'warning'].forEach(function(item) {
assert.ok(!hasStatus(checks, item), `, with ${item}`);
});
});
skip('it works as a factory, passing ember `get` in to create the function');
});

View File

@ -1,93 +0,0 @@
import sumOfUnhealthy from 'consul-ui/utils/sumOfUnhealthy';
import { module, test, skip } from 'qunit';
module('Unit | Utility | sum of unhealthy', function() {
test('it returns the correct single count', function(assert) {
const expected = 1;
[
[
{
Status: 'critical',
},
],
[
{
Status: 'warning',
},
],
].forEach(function(checks) {
const actual = sumOfUnhealthy(checks);
assert.equal(actual, expected);
});
});
test('it returns the correct single count when there are none', function(assert) {
const expected = 0;
[
[
{
Status: 'passing',
},
{
Status: 'passing',
},
{
Status: 'passing',
},
{
Status: 'passing',
},
],
[
{
Status: 'passing',
},
],
].forEach(function(checks) {
const actual = sumOfUnhealthy(checks);
assert.equal(actual, expected);
});
});
test('it returns the correct multiple count', function(assert) {
const expected = 3;
[
[
{
Status: 'critical',
},
{
Status: 'warning',
},
{
Status: 'warning',
},
{
Status: 'passing',
},
],
[
{
Status: 'passing',
},
{
Status: 'critical',
},
{
Status: 'passing',
},
{
Status: 'warning',
},
{
Status: 'warning',
},
{
Status: 'passing',
},
],
].forEach(function(checks) {
const actual = sumOfUnhealthy(checks);
assert.equal(actual, expected);
});
});
skip('it works as a factory, passing ember `get` in to create the function');
});