ui: Search/sort improvements (#9183)

This commit is contained in:
John Cowen 2020-11-13 15:55:40 +00:00 committed by GitHub
parent b8d6e195ed
commit 8a954f0639
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 287 additions and 290 deletions

View File

@ -1,4 +1,7 @@
<form class="consul-intention-search-bar filter-bar"> <form
class="consul-intention-search-bar filter-bar"
...attributes
>
<FreetextFilter <FreetextFilter
@onsearch={{action onsearch}} @onsearch={{action onsearch}}
@value={{search}} @value={{search}}
@ -19,6 +22,7 @@
{{#let components.Optgroup components.Option as |Optgroup Option|}} {{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option class="value-allow" @value="allow" @selected={{contains 'allow' filter.accesses}}>Allow</Option> <Option class="value-allow" @value="allow" @selected={{contains 'allow' filter.accesses}}>Allow</Option>
<Option class="value-deny" @value="deny" @selected={{contains 'deny' filter.accesses}}>Deny</Option> <Option class="value-deny" @value="deny" @selected={{contains 'deny' filter.accesses}}>Deny</Option>
<Option class="value-" @value="app-aware" @selected={{contains 'app-aware' filter.accesses}}>App aware</Option>
{{/let}} {{/let}}
</BlockSlot> </BlockSlot>
</PopoverSelect> </PopoverSelect>

View File

@ -1,57 +1,59 @@
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<ListCollection @items={{items}} @linkable={{action "isLinkable"}} class="consul-nspace-list" as |item|> <ListCollection @items={{items}} @linkable={{action "isLinkable"}} class="consul-nspace-list" as |item|>
<BlockSlot @name="header"> <BlockSlot @name="header">
{{#if item.DeletedAt}} {{#if item.DeletedAt}}
<p> <p>
Deleting {{item.Name}}... Deleting {{item.Name}}...
</p> </p>
{{else}} {{else}}
<a data-test-nspace={{item.Name}} href={{href-to 'dc.nspaces.edit' item.Name}}>{{item.Name}}</a> <a data-test-nspace={{item.Name}} href={{href-to 'dc.nspaces.edit' item.Name}}>{{item.Name}}</a>
{{/if}}
</BlockSlot>
<BlockSlot @name="details">
<dl>
<dt>Description</dt>
<dd data-test-description>
{{item.Description}}
</dd>
</dl>
{{#if (env 'CONSUL_ACLS_ENABLED')}}
<Consul::Token::Ruleset::List @item={{item}} />
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions" as |Actions|> <BlockSlot @name="details">
{{#if (not item.DeletedAt)}} <dl>
<Actions as |Action|> <dt>Description</dt>
<Action data-test-edit-action @href={{href-to 'dc.nspaces.edit' item.Name}}> <dd data-test-description>
<BlockSlot @name="label"> {{item.Description}}
Edit </dd>
</BlockSlot> </dl>
</Action> {{#if (env 'CONSUL_ACLS_ENABLED')}}
{{#if (not-eq item.Name 'default') }} <Consul::Token::Ruleset::List @item={{item}} />
<Action data-test-delete-action @onclick={{action ondelete item}} class="dangerous"> {{/if}}
<BlockSlot @name="label"> </BlockSlot>
Delete <BlockSlot @name="actions" as |Actions|>
</BlockSlot> {{#if (not item.DeletedAt)}}
<BlockSlot @name="confirmation" as |Confirmation|> <Actions as |Action|>
<Confirmation class="warning"> <Action data-test-edit-action @href={{href-to 'dc.nspaces.edit' item.Name}}>
<BlockSlot @name="header"> <BlockSlot @name="label">
Confirm delete Edit
</BlockSlot> </BlockSlot>
<BlockSlot @name="body"> </Action>
<p> {{#if (not-eq item.Name 'default') }}
Are you sure you want to delete this namespace? <Action data-test-delete-action @onclick={{action ondelete item}} class="dangerous">
</p> <BlockSlot @name="label">
</BlockSlot> Delete
<BlockSlot @name="confirm" as |Confirm|> </BlockSlot>
<Confirm>Delete</Confirm> <BlockSlot @name="confirmation" as |Confirmation|>
</BlockSlot> <Confirmation class="warning">
</Confirmation> <BlockSlot @name="header">
</BlockSlot> Confirm delete
</Action> </BlockSlot>
{{/if}} <BlockSlot @name="body">
</Actions> <p>
{{/if}} Are you sure you want to delete this namespace?
</BlockSlot> </p>
</ListCollection> </BlockSlot>
<BlockSlot @name="confirm" as |Confirm|>
<Confirm>Delete</Confirm>
</BlockSlot>
</Confirmation>
</BlockSlot>
</Action>
{{/if}}
</Actions>
{{/if}}
</BlockSlot>
</ListCollection>
{{else}}
{{yield to="empty"}}
{{/if}} {{/if}}

View File

@ -28,7 +28,7 @@
<PopoverSelect <PopoverSelect
class="select-type" class="select-type"
@position="left" @position="left"
@onchange={{action onfilter.type}} @onchange={{action onfilter.kind}}
@multiple={{true}} @multiple={{true}}
as |components|> as |components|>
<BlockSlot @name="selected"> <BlockSlot @name="selected">
@ -38,8 +38,8 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="options"> <BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}} {{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="global-management" @selected={{contains 'global-management' filter.types}}>Global Management</Option> <Option @value="global-management" @selected={{contains 'global-management' filter.kinds}}>Global Management</Option>
<Option @value="standard" @selected={{contains 'standard' filter.types}}>Standard</Option> <Option @value="standard" @selected={{contains 'standard' filter.kinds}}>Standard</Option>
{{/let}} {{/let}}
</BlockSlot> </BlockSlot>
</PopoverSelect> </PopoverSelect>

View File

@ -27,7 +27,7 @@
</PopoverSelect> </PopoverSelect>
<PopoverSelect <PopoverSelect
@position="left" @position="left"
@onchange={{action onfilter.type}} @onchange={{action onfilter.kind}}
@multiple={{true}} @multiple={{true}}
as |components|> as |components|>
<BlockSlot @name="selected"> <BlockSlot @name="selected">
@ -37,15 +37,15 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="options"> <BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}} {{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="service" @selected={{contains 'service' filter.types}}>Service</Option> <Option @value="service" @selected={{contains 'service' filter.kinds}}>Service</Option>
<Optgroup @label="Gateway"> <Optgroup @label="Gateway">
<Option @value="ingress-gateway" @selected={{contains 'ingress-gateway' filter.types}}>Ingress Gateway</Option> <Option @value="ingress-gateway" @selected={{contains 'ingress-gateway' filter.kinds}}>Ingress Gateway</Option>
<Option @value="terminating-gateway" @selected={{contains 'terminating-gateway' filter.types}}>Terminating Gateway</Option> <Option @value="terminating-gateway" @selected={{contains 'terminating-gateway' filter.kinds}}>Terminating Gateway</Option>
<Option @value="mesh-gateway" @selected={{contains 'mesh-gateway' filter.types}}>Mesh Gateway</Option> <Option @value="mesh-gateway" @selected={{contains 'mesh-gateway' filter.kinds}}>Mesh Gateway</Option>
</Optgroup> </Optgroup>
<Optgroup @label="Mesh"> <Optgroup @label="Mesh">
<Option @value="in-mesh" @selected={{contains 'in-mesh' filter.types}}>In service mesh</Option> <Option @value="in-mesh" @selected={{contains 'in-mesh' filter.kinds}}>In service mesh</Option>
<Option @value="not-in-mesh" @selected={{contains 'not-in-mesh' filter.types}}>Not in service mesh</Option> <Option @value="not-in-mesh" @selected={{contains 'not-in-mesh' filter.kinds}}>Not in service mesh</Option>
</Optgroup> </Optgroup>
{{/let}} {{/let}}
</BlockSlot> </BlockSlot>

View File

@ -7,7 +7,7 @@
<div class="filters"> <div class="filters">
<PopoverSelect <PopoverSelect
@position="left" @position="left"
@onchange={{action onfilter.type}} @onchange={{action onfilter.kind}}
@multiple={{true}} @multiple={{true}}
as |components|> as |components|>
<BlockSlot @name="selected"> <BlockSlot @name="selected">
@ -17,9 +17,9 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="options"> <BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}} {{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="global-management" @selected={{contains 'global-management' filter.types}}>Global Management</Option> <Option @value="global-management" @selected={{contains 'global-management' filter.kinds}}>Global Management</Option>
<Option @value="global" @selected={{contains 'global' filter.types}}>Global</Option> <Option @value="global" @selected={{contains 'global' filter.kinds}}>Global</Option>
<Option @value="local" @selected={{contains 'local' filter.types}}>Local</Option> <Option @value="local" @selected={{contains 'local' filter.kinds}}>Local</Option>
{{/let}} {{/let}}
</BlockSlot> </BlockSlot>
</PopoverSelect> </PopoverSelect>

View File

@ -1,9 +1,10 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
export default class IndexController extends Controller { export default class IndexController extends Controller {
queryParams = { queryParams = {
sortBy: 'sort', sortBy: 'sort',
dc: 'dc', dc: 'dc',
type: 'type', kind: 'kind',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -1,7 +1,9 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
export default class IndexController extends Controller { export default class IndexController extends Controller {
queryParams = { queryParams = {
sortBy: 'sort', sortBy: 'sort',
kind: 'kind',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -1,6 +1,7 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
export default class IndexController extends Controller { export default class IndexController extends Controller {
queryParams = { queryParams = {
sortBy: 'sort',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -6,9 +6,10 @@ export default class IndexController extends Controller {
sortBy: 'sort', sortBy: 'sort',
status: 'status', status: 'status',
source: 'source', source: 'source',
type: 'type', kind: 'kind',
search: { search: {
as: 'filter', as: 'filter',
replace: true,
}, },
}; };

View File

@ -2,6 +2,7 @@ import Controller from '@ember/controller';
export default class IndexController extends Controller { export default class IndexController extends Controller {
queryParams = { queryParams = {
sortBy: 'sort', sortBy: 'sort',
access: 'access',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -1,9 +1,9 @@
export default () => ({ accesses = [] }) => item => { import { andOr } from 'consul-ui/utils/filter';
if (accesses.length > 0) {
if (accesses.includes(item.Action)) { export default andOr({
return true; accesses: {
} allow: (item, value) => item.Action === value,
return false; deny: (item, value) => item.Action === value,
} 'app-aware': (item, value) => typeof item.Action === 'undefined',
return true; },
}; });

View File

@ -1,8 +1,10 @@
export default () => ({ statuses = [] }) => { import setHelpers from 'mnemonist/set';
return item => { import { andOr } from 'consul-ui/utils/filter';
if (statuses.length > 0 && !statuses.includes(item.Status)) {
return false; export default andOr({
} statuses: {
return true; passing: (item, value) => item.Status === value,
}; warning: (item, value) => item.Status === value,
}; critical: (item, value) => item.Status === value,
},
});

View File

@ -1,28 +1,15 @@
import setHelpers from 'mnemonist/set'; import setHelpers from 'mnemonist/set';
export default () => ({ dcs = [], types = [] }) => { import { andOr } from 'consul-ui/utils/filter';
const typeIncludes = ['global-management', 'standard'].reduce((prev, item) => {
prev[item] = types.includes(item); export default andOr({
return prev; kinds: {
}, {}); 'global-management': (item, value) => item.isGlobalManagement,
const selectedDcs = new Set(dcs); standard: (item, value) => !item.isGlobalManagement,
return item => { },
let type = true; dcs: (item, values) => {
let dc = true; return (
if (types.length > 0) { typeof item.Datacenters === 'undefined' ||
type = false; setHelpers.intersectionSize(values, new Set(item.Datacenters)) > 0
if (typeIncludes['global-management'] && item.isGlobalManagement) { );
type = true; },
} });
if (typeIncludes['standard'] && !item.isGlobalManagement) {
type = true;
}
}
if (dcs.length > 0) {
// if datacenters is undefined it means the policy is applicable to all datacenters
dc =
typeof item.Datacenters === 'undefined' ||
setHelpers.intersectionSize(selectedDcs, new Set(item.Datacenters)) > 0;
}
return type && dc;
};
};

View File

@ -1,19 +1,13 @@
import setHelpers from 'mnemonist/set'; import setHelpers from 'mnemonist/set';
export default () => ({ sources = [], statuses = [] }) => { import { andOr } from 'consul-ui/utils/filter';
const uniqueSources = new Set(sources);
return item => { export default andOr({
if (statuses.length > 0) { statuses: {
if (statuses.includes(item.Status)) { passing: (item, value) => item.Status === value,
return true; warning: (item, value) => item.Status === value,
} critical: (item, value) => item.Status === value,
return false; },
} sources: (item, values) => {
if (sources.length > 0) { return setHelpers.intersectionSize(values, new Set(item.ExternalSources || [])) !== 0;
if (setHelpers.intersectionSize(uniqueSources, new Set(item.ExternalSources || [])) !== 0) { },
return true; });
}
return false;
}
return true;
};
};

View File

@ -1,71 +1,25 @@
import setHelpers from 'mnemonist/set'; import setHelpers from 'mnemonist/set';
export default () => ({ instances = [], sources = [], statuses = [], types = [] }) => { import { andOr } from 'consul-ui/utils/filter';
const uniqueSources = new Set(sources);
const typeIncludes = [ export default andOr({
'ingress-gateway', kinds: {
'terminating-gateway', 'ingress-gateway': (item, value) => item.Kind === value,
'mesh-gateway', 'terminating-gateway': (item, value) => item.Kind === value,
'service', 'mesh-gateway': (item, value) => item.Kind === value,
'in-mesh', service: (item, value) => !item.Kind,
'not-in-mesh', 'in-mesh': (item, value) => item.InMesh,
].reduce((prev, item) => { 'not-in-mesh': (item, value) => !item.InMesh,
prev[item] = types.includes(item); },
return prev; statuses: {
}, {}); passing: (item, value) => item.MeshStatus === value,
const instanceIncludes = ['registered', 'not-registered'].reduce((prev, item) => { warning: (item, value) => item.MeshStatus === value,
prev[item] = instances.includes(item); critical: (item, value) => item.MeshStatus === value,
return prev; },
}, {}); instances: {
return item => { registered: (item, value) => item.InstanceCount > 0,
if (statuses.length > 0) { 'not-registered': (item, value) => item.InstanceCount === 0,
if (statuses.includes(item.MeshStatus)) { },
return true; sources: (item, values) => {
} return setHelpers.intersectionSize(values, new Set(item.ExternalSources || [])) !== 0;
return false; },
} });
if (instances.length > 0) {
if (item.InstanceCount > 0) {
if (instanceIncludes['registered']) {
return true;
}
} else {
if (instanceIncludes['not-registered']) {
return true;
}
}
return false;
}
if (types.length > 0) {
if (typeIncludes['ingress-gateway'] && item.Kind === 'ingress-gateway') {
return true;
}
if (typeIncludes['terminating-gateway'] && item.Kind === 'terminating-gateway') {
return true;
}
if (typeIncludes['mesh-gateway'] && item.Kind === 'mesh-gateway') {
return true;
}
if (typeIncludes['service'] && typeof item.Kind === 'undefined') {
return true;
}
if (typeIncludes['in-mesh']) {
if (item.InMesh) {
return true;
}
}
if (typeIncludes['not-in-mesh']) {
if (!item.InMesh) {
return true;
}
}
return false;
}
if (sources.length > 0) {
if (setHelpers.intersectionSize(uniqueSources, new Set(item.ExternalSources || [])) !== 0) {
return true;
}
return false;
}
return true;
};
};

View File

@ -1,21 +1,10 @@
export default () => ({ types = [] }) => { import setHelpers from 'mnemonist/set';
const typeIncludes = ['global-management', 'global', 'local'].reduce((prev, item) => { import { andOr } from 'consul-ui/utils/filter';
prev[item] = types.includes(item);
return prev; export default andOr({
}, {}); kinds: {
return item => { 'global-management': (item, value) => item.isGlobalManagement,
if (types.length > 0) { global: (item, value) => !item.Local,
if (typeIncludes['global-management'] && item.isGlobalManagement) { local: (item, value) => item.Local,
return true; },
} });
if (typeIncludes['global'] && !item.Local) {
return true;
}
if (typeIncludes['local'] && item.Local) {
return true;
}
return false;
}
return true;
};
};

View File

@ -5,11 +5,12 @@ import { hash } from 'rsvp';
import WithPolicyActions from 'consul-ui/mixins/policy/with-actions'; import WithPolicyActions from 'consul-ui/mixins/policy/with-actions';
export default class IndexRoute extends Route.extend(WithPolicyActions) { export default class IndexRoute extends Route.extend(WithPolicyActions) {
@service('repository/policy') @service('repository/policy') repo;
repo;
queryParams = { queryParams = {
sortBy: 'sort', sortBy: 'sort',
dc: 'dc',
kind: 'kind',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -5,8 +5,7 @@ import { hash } from 'rsvp';
import WithRoleActions from 'consul-ui/mixins/role/with-actions'; import WithRoleActions from 'consul-ui/mixins/role/with-actions';
export default class IndexRoute extends Route.extend(WithRoleActions) { export default class IndexRoute extends Route.extend(WithRoleActions) {
@service('repository/role') @service('repository/role') repo;
repo;
queryParams = { queryParams = {
sortBy: 'sort', sortBy: 'sort',

View File

@ -3,15 +3,14 @@ import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp'; import { hash } from 'rsvp';
import { get } from '@ember/object'; import { get } from '@ember/object';
import WithTokenActions from 'consul-ui/mixins/token/with-actions'; import WithTokenActions from 'consul-ui/mixins/token/with-actions';
export default class IndexRoute extends Route.extend(WithTokenActions) {
@service('repository/token')
repo;
@service('settings') export default class IndexRoute extends Route.extend(WithTokenActions) {
settings; @service('repository/token') repo;
@service('settings') settings;
queryParams = { queryParams = {
sortBy: 'sort', sortBy: 'sort',
kind: 'kind',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -3,6 +3,7 @@ import Route from 'consul-ui/routing/route';
export default class IndexRoute extends Route { export default class IndexRoute extends Route {
queryParams = { queryParams = {
sortBy: 'sort', sortBy: 'sort',
access: 'access',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -5,16 +5,16 @@ import { get, action } from '@ember/object';
import isFolder from 'consul-ui/utils/isFolder'; import isFolder from 'consul-ui/utils/isFolder';
export default class IndexRoute extends Route { export default class IndexRoute extends Route {
@service('repository/kv') repo;
queryParams = { queryParams = {
sortBy: 'sort',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,
}, },
}; };
@service('repository/kv')
repo;
beforeModel() { beforeModel() {
// we are index or folder, so if the key doesn't have a trailing slash // we are index or folder, so if the key doesn't have a trailing slash
// add one to force a fake findBySlug // add one to force a fake findBySlug

View File

@ -3,11 +3,11 @@ import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp'; import { hash } from 'rsvp';
export default class IndexRoute extends Route { export default class IndexRoute extends Route {
@service('data-source/service') @service('data-source/service') data;
data;
queryParams = { queryParams = {
sortBy: 'sort', sortBy: 'sort',
status: 'status',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -4,11 +4,8 @@ import { hash } from 'rsvp';
import WithNspaceActions from 'consul-ui/mixins/nspace/with-actions'; import WithNspaceActions from 'consul-ui/mixins/nspace/with-actions';
export default class IndexRoute extends Route.extend(WithNspaceActions) { export default class IndexRoute extends Route.extend(WithNspaceActions) {
@service('data-source/service') @service('data-source/service') data;
data; @service('repository/nspace') repo;
@service('repository/nspace')
repo;
queryParams = { queryParams = {
sortBy: 'sort', sortBy: 'sort',

View File

@ -3,18 +3,17 @@ import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp'; import { hash } from 'rsvp';
export default class IndexRoute extends Route { export default class IndexRoute extends Route {
@service('data-source/service') @service('data-source/service') data;
data;
queryParams = { queryParams = {
sortBy: 'sort',
status: 'status',
source: 'source',
kind: 'kind',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,
}, },
// temporary support of old style status
status: {
as: 'status',
},
}; };
model(params) { model(params) {

View File

@ -2,6 +2,9 @@ import Route from 'consul-ui/routing/route';
export default class InstancesRoute extends Route { export default class InstancesRoute extends Route {
queryParams = { queryParams = {
sortBy: 'sort',
status: 'status',
source: 'source',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -2,6 +2,8 @@ import Route from 'consul-ui/routing/route';
export default class IndexRoute extends Route { export default class IndexRoute extends Route {
queryParams = { queryParams = {
sortBy: 'sort',
access: 'access',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -4,7 +4,7 @@
{{title 'Access Controls'}} {{title 'Access Controls'}}
{{/if}} {{/if}}
{{#let (hash {{#let (hash
types=(if type (split type ',') undefined) kinds=(if kind (split kind ',') undefined)
dcs=(if dc (split dc ',') undefined) dcs=(if dc (split dc ',') undefined)
) as |filters|}} ) as |filters|}}
{{#let (or sortBy "Name:asc") as |sort|}} {{#let (or sortBy "Name:asc") as |sort|}}
@ -45,7 +45,7 @@
@filter={{filters}} @filter={{filters}}
@onfilter={{hash @onfilter={{hash
dc=(action (mut dc) value="target.selectedItems") dc=(action (mut dc) value="target.selectedItems")
type=(action (mut type) value="target.selectedItems") kind=(action (mut kind) value="target.selectedItems")
}} }}
/> />
{{/if}} {{/if}}

View File

@ -5,7 +5,7 @@
{{/if}} {{/if}}
{{#let (hash {{#let (hash
types=(if type (split type ',') undefined) kinds=(if kind (split kind ',') undefined)
) as |filters|}} ) as |filters|}}
{{#let (or sortBy "CreateTime:desc") as |sort|}} {{#let (or sortBy "CreateTime:desc") as |sort|}}
<AppView <AppView
@ -44,7 +44,7 @@
@filter={{filters}} @filter={{filters}}
@onfilter={{hash @onfilter={{hash
type=(action (mut type) value="target.selectedItems") kind=(action (mut kind) value="target.selectedItems")
}} }}
/> />
{{/if}} {{/if}}

View File

@ -32,39 +32,41 @@
<BlockSlot @name="content"> <BlockSlot @name="content">
{{#let (sort-by (comparator 'nspace' sort) items) as |sorted|}} {{#let (sort-by (comparator 'nspace' sort) items) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'nspace' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'nspace' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="content" as |filtered|>
<Consul::Nspace::List <Consul::Nspace::List
@items={{filtered}} @items={{filtered}}
@ondelete={{queue (action send 'delete')}} @ondelete={{queue (action send 'delete')}}
> >
<EmptyState @allowLogin={{true}}> <:empty>
<BlockSlot @name="header"> <EmptyState @allowLogin={{true}}>
<h2> <BlockSlot @name="header">
{{#if (gt items.length 0)}} <h2>
No namespaces found {{#if (gt items.length 0)}}
{{else}} No namespaces found
Welcome to Namespaces {{else}}
{{/if}} Welcome to Namespaces
</h2> {{/if}}
</BlockSlot> </h2>
<BlockSlot @name="body"> </BlockSlot>
<p> <BlockSlot @name="body">
{{#if (gt items.length 0)}} <p>
No namespaces where found matching that search, or you may not have access to view the namespaces you are searching for. {{#if (gt items.length 0)}}
{{else}} No namespaces where found matching that search, or you may not have access to view the namespaces you are searching for.
There don't seem to be any namespaces, or you may not have access to view namespaces yet. {{else}}
{{/if}} There don't seem to be any namespaces, or you may not have access to view namespaces yet.
</p> {{/if}}
</BlockSlot> </p>
<BlockSlot @name="actions"> </BlockSlot>
<li class="docs-link"> <BlockSlot @name="actions">
<a href="{{env 'CONSUL_DOCS_URL'}}/commands/namespace" rel="noopener noreferrer" target="_blank">Documentation on namespaces</a> <li class="docs-link">
</li> <a href="{{env 'CONSUL_DOCS_URL'}}/commands/namespace" rel="noopener noreferrer" target="_blank">Documentation on namespaces</a>
<li class="learn-link"> </li>
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/namespaces/secure-namespaces" rel="noopener noreferrer" target="_blank">Read the guide</a> <li class="learn-link">
</li> <a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/namespaces/secure-namespaces" rel="noopener noreferrer" target="_blank">Read the guide</a>
</BlockSlot> </li>
</EmptyState> </BlockSlot>
</EmptyState>
</:empty>
</Consul::Nspace::List> </Consul::Nspace::List>
</BlockSlot> </BlockSlot>
</ChangeableSet> </ChangeableSet>

View File

@ -2,7 +2,7 @@
<EventSource @src={{items}} /> <EventSource @src={{items}} />
{{#let (hash {{#let (hash
statuses=(if status (split status ',') undefined) statuses=(if status (split status ',') undefined)
types=(if type (split type ',') undefined) kinds=(if kind (split kind ',') undefined)
sources=(if source (split source ',') undefined) sources=(if source (split source ',') undefined)
) as |filters|}} ) as |filters|}}
{{#let (or sortBy "Name:asc") as |sort|}} {{#let (or sortBy "Name:asc") as |sort|}}
@ -32,7 +32,7 @@
@filter={{filters}} @filter={{filters}}
@onfilter={{hash @onfilter={{hash
status=(action (mut status) value="target.selectedItems") status=(action (mut status) value="target.selectedItems")
type=(action (mut type) value="target.selectedItems") kind=(action (mut kind) value="target.selectedItems")
source=(action (mut source) value="target.selectedItems") source=(action (mut source) value="target.selectedItems")
}} }}
/> />

View File

@ -0,0 +1,56 @@
import setHelpers from 'mnemonist/set';
const createPossibles = function(predicates) {
// create arrays of allowed values
return Object.entries(predicates).reduce((prev, [key, value]) => {
if (typeof value !== 'function') {
prev[key] = new Set(Object.keys(value));
} else {
prev[key] = null;
}
return prev;
}, {});
};
const sanitize = function(values, possibles) {
return Object.keys(possibles).reduce((prev, key) => {
// only set the value if the value has a length of > 0
const value = typeof values[key] === 'undefined' ? [] : values[key];
if (value.length > 0) {
if (possibles[key] !== null) {
// only include possible values
prev[key] = [...setHelpers.intersection(possibles[key], new Set(value))];
} else {
// only unique values
prev[key] = [...new Set(value)];
}
}
return prev;
}, {});
};
const execute = function(item, values, predicates) {
// every/and the top level values
return Object.entries(values).every(([key, values]) => {
let predicate = predicates[key];
if (typeof predicate === 'function') {
return predicate(item, values);
} else {
// if the top level values can have multiple values some/or them
return values.some(val => predicate[val](item, val));
}
});
};
// exports a function that requires a hash of predicates passed in
export const andOr = predicates => {
// figure out all possible values from the hash of predicates
const possibles = createPossibles(predicates);
return () => values => {
// this is what is called post injection
// the actual user values are passed in here so 'sanitize' them which is
// basically checking against the possibles
values = sanitize(values, possibles);
// this is your actual filter predicate
return item => {
return execute(item, values, predicates);
};
};
};

View File

@ -97,7 +97,7 @@ module('Unit | Filter | Predicates | service', function() {
expected = [items[0]]; expected = [items[0]];
actual = items.filter( actual = items.filter(
predicate({ predicate({
types: ['ingress-gateway'], kinds: ['ingress-gateway'],
}) })
); );
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
@ -105,7 +105,7 @@ module('Unit | Filter | Predicates | service', function() {
expected = [items[1]]; expected = [items[1]];
actual = items.filter( actual = items.filter(
predicate({ predicate({
types: ['mesh-gateway'], kinds: ['mesh-gateway'],
}) })
); );
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
@ -113,7 +113,7 @@ module('Unit | Filter | Predicates | service', function() {
expected = items; expected = items;
actual = items.filter( actual = items.filter(
predicate({ predicate({
types: ['ingress-gateway', 'mesh-gateway', 'service'], kinds: ['ingress-gateway', 'mesh-gateway', 'service'],
}) })
); );
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
@ -141,7 +141,7 @@ module('Unit | Filter | Predicates | service', function() {
expected = [items[0]]; expected = [items[0]];
actual = items.filter( actual = items.filter(
predicate({ predicate({
types: ['ingress-gateway'], kinds: ['ingress-gateway'],
statuses: ['passing'], statuses: ['passing'],
instances: ['registered'], instances: ['registered'],
}) })
@ -151,7 +151,7 @@ module('Unit | Filter | Predicates | service', function() {
expected = [items[1]]; expected = [items[1]];
actual = items.filter( actual = items.filter(
predicate({ predicate({
types: ['mesh-gateway'], kinds: ['mesh-gateway'],
statuses: ['warning'], statuses: ['warning'],
instances: ['registered'], instances: ['registered'],
}) })
@ -161,7 +161,7 @@ module('Unit | Filter | Predicates | service', function() {
expected = items; expected = items;
actual = items.filter( actual = items.filter(
predicate({ predicate({
types: ['ingress-gateway', 'mesh-gateway', 'service'], kinds: ['ingress-gateway', 'mesh-gateway', 'service'],
statuses: ['passing', 'warning', 'critical'], statuses: ['passing', 'warning', 'critical'],
instances: ['registered', 'not-registered'], instances: ['registered', 'not-registered'],
}) })