ui: Improved filtering and sorting (#8591)

This commit is contained in:
John Cowen 2020-09-01 19:13:11 +01:00 committed by GitHub
parent a9df6ac50b
commit 31ad6e39ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 1905 additions and 732 deletions

View File

@ -0,0 +1,74 @@
<form class="filter-bar">
<FreetextFilter
@onsearch={{action onsearch}}
@value={{search}}
@placeholder="Search"
/>
<div class="filters">
<PopoverSelect
@position="left"
@onchange={{action onfilter.access}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Permissions
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="allow" @selected={{contains 'allow' filter.accesses}}>Allow</Option>
<Option @value="deny" @selected={{contains 'deny' filter.accesses}}>Deny</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
<div class="sort">
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Action:asc" "Allow to Deny")
(array "Action:desc" "Deny to Allow")
(array "SourceName:asc" "Source: A to Z")
(array "SourceName:desc" "Source: Z to A")
(array "DestinationName:asc" "Destination: A to Z")
(array "DestinationName:desc" "Destination: Z to A")
(array "Precedence:asc" "Precedence: Ascending")
(array "Precedence:desc" "Precedence: Descending")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Permission">
<Option @value="Action:asc" @selected={{eq "Action:asc" sort}}>Allow to Deny</Option>
<Option @value="Action:desc" @selected={{eq "Action:desc" sort}}>Deny to Allow</Option>
</Optgroup>
<Optgroup @label="Source">
<Option @value="SourceName:asc" @selected={{eq "SourceName:asc" sort}}>A to Z</Option>
<Option @value="SourceName:desc" @selected={{eq "SourceName:desc" sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Destination">
<Option @value="DestinationName:asc" @selected={{eq "DestinationName:asc" sort}}>A to Z</Option>
<Option @value="DestinationName:desc" @selected={{eq "DestinationName:desc" sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Precedence">
<Option @value="Precedence:asc" @selected={{eq "Precedence:asc" sort}}>Ascending</Option>
<Option @value="Precedence:desc" @selected={{eq "Precedence:desc" sort}}>Descending</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>

View File

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

View File

@ -0,0 +1,65 @@
<form class="filter-bar">
<FreetextFilter
@onsearch={{action onsearch}}
@value={{search}}
@placeholder="Search"
/>
<div class="filters">
<PopoverSelect
class="type-status"
@position="left"
@onchange={{action onfilter.status}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Health Status
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option class="value-passing" @value="passing" @selected={{contains 'passing' filter.statuses}}>Passing</Option>
<Option class="value-warning" @value="warning" @selected={{contains 'warning' filter.statuses}}>Warning</Option>
<Option class="value-critical" @value="critical" @selected={{contains 'critical' filter.statuses}}>Failing</Option>
<Option class="value-empty" @value="empty" @selected={{contains 'empty' filter.statuses}}>No checks</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
<div class="sort">
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Node:asc" "A to Z")
(array "Node:desc" "Z to A")
(array "Status:asc" "Unhealthy to Healthy")
(array "Status:desc" "Healthy to Unhealthy")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Health Status">
<Option @value="Status:asc" @selected={{eq "Status:asc" sort}}>Unhealthy to Healthy</Option>
<Option @value="Status:desc" @selected={{eq "Status:desc" sort}}>Healthy to Unhealthy</Option>
</Optgroup>
<Optgroup @label="Service Name">
<Option @value="Node:asc" @selected={{eq "Node:asc" sort}}>A to Z</Option>
<Option @value="Node:desc" @selected={{eq "Node:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>

View File

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

View File

@ -0,0 +1,37 @@
<form class="filter-bar">
<FreetextFilter
@onsearch={{action onsearch}}
@value={{search}}
@placeholder="Search"
/>
<div class="sort">
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>

View File

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

View File

@ -0,0 +1,77 @@
<form class="filter-bar">
<FreetextFilter
@onsearch={{action onsearch}}
@value={{search}}
@placeholder="Search"
/>
<div class="filters">
<PopoverSelect
class="select-dcs"
@position="left"
@onchange={{action onfilter.dc}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Datacenters
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each dcs as |dc|}}
<Option @value={{dc.Name}} @selected={{contains dc.Name filter.dcs}}>{{dc.Name}}</Option>
{{/each}}
{{/let}}
<DataSource @src="/*/*/datacenters" @loading="lazy" @onchange={{action (mut dcs) value="data"}} />
</BlockSlot>
</PopoverSelect>
<PopoverSelect
class="select-type"
@position="left"
@onchange={{action onfilter.type}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Type
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="global-management" @selected={{contains 'global-management' filter.types}}>Global Management</Option>
<Option @value="standard" @selected={{contains 'standard' filter.types}}>Standard</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
<div class="sort">
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>

View File

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

View File

@ -0,0 +1,43 @@
<form class="filter-bar">
<FreetextFilter
@onsearch={{action onsearch}}
@value={{search}}
@placeholder="Search"
/>
<div class="sort">
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
(array "CreateIndex:desc" "Newest to oldest")
(array "CreateIndex:asc" "Oldest to newest")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Creation">
<Option @value="CreateIndex:desc" @selected={{eq "CreateIndex:desc" sort}}>Newest to oldest</Option>
<Option @value="CreateIndex:asc" @selected={{eq "CreateIndex:asc" sort}}>Oldest to newest</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>

View File

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

View File

@ -0,0 +1,86 @@
<form class="filter-bar">
<FreetextFilter
@onsearch={{action onsearch}}
@value={{search}}
@placeholder="Search"
/>
<div class="filters">
<PopoverSelect
class="type-status"
@position="left"
@onchange={{action onfilter.status}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Health Status
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option class="value-passing" @value="passing" @selected={{contains 'passing' filter.statuses}}>Passing</Option>
<Option class="value-warning" @value="warning" @selected={{contains 'warning' filter.statuses}}>Warning</Option>
<Option class="value-critical" @value="critical" @selected={{contains 'critical' filter.statuses}}>Failing</Option>
<Option class="value-empty" @value="empty" @selected={{contains 'empty' filter.statuses}}>No checks</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>
{{#if (gt sources.length 0)}}
<PopoverSelect
class="type-source"
@position="left"
@onchange={{action onfilter.source}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Source
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each sources as |source|}}
<Option class={{source}} @value={{source}} @selected={{contains source filter.sources}}>{{source}}</Option>
{{/each}}
{{/let}}
</BlockSlot>
</PopoverSelect>
{{/if}}
</div>
<div class="sort">
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
(array "Status:asc" "Unhealthy to Healthy")
(array "Status:desc" "Healthy to Unhealthy")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Health Status">
<Option @value="Status:asc" @selected={{eq "Status:asc" sort}}>Unhealthy to Healthy</Option>
<Option @value="Status:desc" @selected={{eq "Status:desc" sort}}>Healthy to Unhealthy</Option>
</Optgroup>
<Optgroup @label="Service Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>

View File

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

View File

@ -0,0 +1,111 @@
<form class="filter-bar">
<FreetextFilter
@onsearch={{action onsearch}}
@value={{search}}
@placeholder="Search"
/>
<div class="filters">
<PopoverSelect
class="type-status"
@position="left"
@onchange={{action onfilter.status}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Health Status
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option class="value-passing" @value="passing" @selected={{contains 'passing' filter.statuses}}>Passing</Option>
<Option class="value-warning" @value="warning" @selected={{contains 'warning' filter.statuses}}>Warning</Option>
<Option class="value-critical" @value="critical" @selected={{contains 'critical' filter.statuses}}>Failing</Option>
<Option class="value-empty" @value="empty" @selected={{contains 'empty' filter.statuses}}>No checks</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>
<PopoverSelect
@position="left"
@onchange={{action onfilter.type}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Service Type
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="service" @selected={{contains 'service' filter.types}}>Service</Option>
<Optgroup @label="Gateway">
<Option @value="ingress-gateway" @selected={{contains 'ingress-gateway' filter.types}}>Ingress Gateway</Option>
<Option @value="terminating-gateway" @selected={{contains 'terminating-gateway' filter.types}}>Terminating Gateway</Option>
<Option @value="mesh-gateway" @selected={{contains 'mesh-gateway' filter.types}}>Mesh Gateway</Option>
</Optgroup>
<Optgroup @label="Mesh">
<Option @value="mesh-enabled" @selected={{contains 'mesh-enabled' filter.types}}>In service mesh</Option>
<Option @value="mesh-disabled" @selected={{contains 'mesh-disabled' filter.types}}>Not in service mesh</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
{{#if (gt sources.length 0)}}
<PopoverSelect
class="type-source"
@position="left"
@onchange={{action onfilter.source}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Source
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each sources as |source|}}
<Option class={{source}} @value={{source}} @selected={{contains source filter.sources}}>{{source}}</Option>
{{/each}}
{{/let}}
</BlockSlot>
</PopoverSelect>
{{/if}}
</div>
<div class="sort">
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
(array "Status:asc" "Unhealthy to Healthy")
(array "Status:desc" "Healthy to Unhealthy")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Health Status">
<Option @value="Status:asc" @selected={{eq "Status:asc" sort}}>Unhealthy to Healthy</Option>
<Option @value="Status:desc" @selected={{eq "Status:desc" sort}}>Healthy to Unhealthy</Option>
</Optgroup>
<Optgroup @label="Service Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>

View File

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

View File

@ -0,0 +1,57 @@
<form class="filter-bar">
<FreetextFilter
@onsearch={{action onsearch}}
@value={{search}}
@placeholder="Search"
/>
<div class="filters">
<PopoverSelect
@position="left"
@onchange={{action onfilter.type}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Type
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="global-management" @selected={{contains 'global-management' filter.types}}>Global Management</Option>
<Option @value="global" @selected={{contains 'global' filter.types}}>Global</Option>
<Option @value="local" @selected={{contains 'local' filter.types}}>Local</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
<div class="sort">
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "CreateTime:desc" "Newest to oldest")
(array "CreateTime:asc" "Oldest to newest")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Creation">
<Option @value="CreateTime:desc" @selected={{eq "CreateTime:desc" sort}}>Newest to oldest</Option>
<Option @value="CreateTime:asc" @selected={{eq "CreateTime:asc" sort}}>Oldest to newest</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>

View File

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

View File

@ -0,0 +1,62 @@
<form class="filter-bar">
<FreetextFilter
@onsearch={{action onsearch}}
@value={{search}}
@placeholder="Search"
/>
<div class="filters">
<PopoverSelect
@position="left"
@onchange={{action onfilter.instance}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Type
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="registered" @selected={{contains 'registered' filter.instances}}>Registered</Option>
<Option @value="not-registered" @selected={{contains 'not-registered' filter.instances}}>Not Registered</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
<div class="sort">
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
(array "Status:asc" "Unhealthy to Healthy")
(array "Status:desc" "Healthy to Unhealthy")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Health Status">
<Option @value="Status:asc" @selected={{eq "Status:asc" sort}}>Unhealthy to Healthy</Option>
<Option @value="Status:desc" @selected={{eq "Status:desc" sort}}>Healthy to Unhealthy</Option>
</Optgroup>
<Optgroup @label="Service Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>

View File

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

View File

@ -6,7 +6,7 @@ export default Component.extend(Slotted, {
tagName: '', tagName: '',
dom: service('dom'), dom: service('dom'),
multiple: false, multiple: false,
subtractive: true, subtractive: false,
onchange: function() {}, onchange: function() {},
addOption: function(option) { addOption: function(option) {
if (typeof this._options === 'undefined') { if (typeof this._options === 'undefined') {

View File

@ -1,6 +1,7 @@
{{#let components.MenuItem as |MenuItem|}} {{#let components.MenuItem as |MenuItem|}}
<MenuItem <MenuItem
class={{if selected 'is-active'}} class={{if selected 'is-active'}}
...attributes
@onclick={{action 'click'}} @onclick={{action 'click'}}
@selected={{selected}} @selected={{selected}}
> >

View File

@ -2,6 +2,8 @@ import Controller from '@ember/controller';
export default Controller.extend({ export default Controller.extend({
queryParams: { queryParams: {
sortBy: 'sort', sortBy: 'sort',
dc: 'dc',
type: 'type',
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 Controller.extend({ export default Controller.extend({
queryParams: { queryParams: {
sortBy: 'sort', sortBy: 'sort',
access: 'access',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -3,6 +3,7 @@ import Controller from '@ember/controller';
export default Controller.extend({ export default Controller.extend({
queryParams: { queryParams: {
sortBy: 'sort', sortBy: 'sort',
status: 'status',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

@ -4,6 +4,9 @@ import { computed } from '@ember/object';
export default Controller.extend({ export default Controller.extend({
queryParams: { queryParams: {
sortBy: 'sort', sortBy: 'sort',
status: 'status',
source: 'source',
type: 'type',
search: { search: {
as: 'filter', as: 'filter',
}, },
@ -13,4 +16,11 @@ export default Controller.extend({
return item.Kind !== 'connect-proxy'; return item.Kind !== 'connect-proxy';
}); });
}), }),
externalSources: computed('services', function() {
const sources = this.services.reduce(function(prev, item) {
return prev.concat(item.ExternalSources || []);
}, []);
// unique, non-empty values, alpha sort
return [...new Set(sources)].filter(Boolean).sort();
}),
}); });

View File

@ -1,11 +1,21 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { computed } from '@ember/object';
export default Controller.extend({ export default Controller.extend({
queryParams: { queryParams: {
sortBy: 'sort', sortBy: 'sort',
status: 'status',
source: 'source',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,
}, },
}, },
externalSources: computed('items', function() {
const sources = this.items.reduce(function(prev, item) {
return prev.concat(item.ExternalSources || []);
}, []);
// unique, non-empty values, alpha sort
return [...new Set(sources)].filter(Boolean).sort();
}),
}); });

View File

@ -0,0 +1,12 @@
import Controller from '@ember/controller';
export default Controller.extend({
queryParams: {
sortBy: 'sort',
instance: 'instance',
search: {
as: 'filter',
replace: true,
},
},
});

View File

@ -0,0 +1,12 @@
import Controller from '@ember/controller';
export default Controller.extend({
queryParams: {
sortBy: 'sort',
instance: 'instance',
search: {
as: 'filter',
replace: true,
},
},
});

View File

@ -0,0 +1,9 @@
export default () => ({ accesses = [] }) => item => {
if (accesses.length > 0) {
if (accesses.includes(item.Action)) {
return true;
}
return false;
}
return true;
};

View File

@ -0,0 +1,8 @@
export default () => ({ statuses = [] }) => {
return item => {
if (statuses.length > 0 && !statuses.includes(item.Status)) {
return false;
}
return true;
};
};

View File

@ -0,0 +1,28 @@
import setHelpers from 'mnemonist/set';
export default () => ({ dcs = [], types = [] }) => {
const typeIncludes = ['global-management', 'standard'].reduce((prev, item) => {
prev[item] = types.includes(item);
return prev;
}, {});
const selectedDcs = new Set(dcs);
return item => {
let type = true;
let dc = true;
if (types.length > 0) {
type = false;
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

@ -0,0 +1,19 @@
import setHelpers from 'mnemonist/set';
export default () => ({ sources = [], statuses = [] }) => {
const uniqueSources = new Set(sources);
return item => {
if (statuses.length > 0) {
if (statuses.includes(item.Status)) {
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

@ -0,0 +1,67 @@
import setHelpers from 'mnemonist/set';
export default () => ({ instances = [], sources = [], statuses = [], types = [] }) => {
const uniqueSources = new Set(sources);
const typeIncludes = [
'ingress-gateway',
'terminating-gateway',
'mesh-gateway',
'service',
'mesh-enabled',
'mesh-disabled',
].reduce((prev, item) => {
prev[item] = types.includes(item);
return prev;
}, {});
const instanceIncludes = ['registered', 'not-registered'].reduce((prev, item) => {
prev[item] = instances.includes(item);
return prev;
}, {});
return item => {
if (statuses.length > 0) {
if (statuses.includes(item.MeshStatus)) {
return true;
}
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['mesh-enabled'] && typeof item.Proxy !== 'undefined') {
return true;
}
if (typeIncludes['mesh-disabled'] && typeof item.Proxy === 'undefined') {
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

@ -0,0 +1,21 @@
export default () => ({ types = [] }) => {
const typeIncludes = ['global-management', 'global', 'local'].reduce((prev, item) => {
prev[item] = types.includes(item);
return prev;
}, {});
return item => {
if (types.length > 0) {
if (typeIncludes['global-management'] && item.isGlobalManagement) {
return true;
}
if (typeIncludes['global'] && !item.Local) {
return true;
}
if (typeIncludes['local'] && item.Local) {
return true;
}
return false;
}
return true;
};
};

View File

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

View File

@ -1,7 +1,6 @@
import { helper } from '@ember/component/helper'; import { helper } from '@ember/component/helper';
import { get } from '@ember/object'; import { get } from '@ember/object';
import { MANAGEMENT_ID } from 'consul-ui/models/policy';
const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001';
export default helper(function policyGroup([items] /*, hash*/) { export default helper(function policyGroup([items] /*, hash*/) {
return items.reduce( return items.reduce(

View File

@ -1,4 +1,5 @@
import service from 'consul-ui/sort/comparators/service'; import service from 'consul-ui/sort/comparators/service';
import serviceInstance from 'consul-ui/sort/comparators/service-instance';
import kv from 'consul-ui/sort/comparators/kv'; import kv from 'consul-ui/sort/comparators/kv';
import check from 'consul-ui/sort/comparators/check'; import check from 'consul-ui/sort/comparators/check';
import intention from 'consul-ui/sort/comparators/intention'; import intention from 'consul-ui/sort/comparators/intention';
@ -13,6 +14,7 @@ export function initialize(container) {
const Sort = container.resolveRegistration('service:sort'); const Sort = container.resolveRegistration('service:sort');
const comparators = { const comparators = {
service: service(), service: service(),
serviceInstance: serviceInstance(),
kv: kv(), kv: kv(),
check: check(), check: check(),
intention: intention(), intention: intention(),

View File

@ -1,9 +1,12 @@
import Model from 'ember-data/model'; import Model from 'ember-data/model';
import attr from 'ember-data/attr'; import attr from 'ember-data/attr';
import { computed } from '@ember/object';
export const PRIMARY_KEY = 'uid'; export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'ID'; export const SLUG_KEY = 'ID';
export const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001';
export default Model.extend({ export default Model.extend({
[PRIMARY_KEY]: attr('string'), [PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'), [SLUG_KEY]: attr('string'),
@ -19,6 +22,9 @@ export default Model.extend({
// frontend only for ordering where CreateIndex can't be used // frontend only for ordering where CreateIndex can't be used
CreateTime: attr('date', { defaultValue: 0 }), CreateTime: attr('date', { defaultValue: 0 }),
// //
isGlobalManagement: computed('ID', function() {
return this.ID === MANAGEMENT_ID;
}),
Datacenter: attr('string'), Datacenter: attr('string'),
Namespace: attr('string'), Namespace: attr('string'),
SyncTime: attr('number'), SyncTime: attr('number'),

View File

@ -1,7 +1,8 @@
import Model from 'ember-data/model'; import Model from 'ember-data/model';
import attr from 'ember-data/attr'; import attr from 'ember-data/attr';
import { belongsTo } from 'ember-data/relationships'; import { belongsTo } from 'ember-data/relationships';
import { filter, alias } from '@ember/object/computed'; import { computed } from '@ember/object';
import { or, filter, alias } from '@ember/object/computed';
export const PRIMARY_KEY = 'uid'; export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'Node.Node,Service.ID'; export const SLUG_KEY = 'Node.Node,Service.ID';
@ -19,13 +20,52 @@ export default Model.extend({
Checks: attr(), Checks: attr(),
SyncTime: attr('number'), SyncTime: attr('number'),
meta: attr(), meta: attr(),
Name: or('Service.ID', 'Service.Service'),
Tags: alias('Service.Tags'), Tags: alias('Service.Tags'),
Meta: alias('Service.Meta'), Meta: alias('Service.Meta'),
Namespace: alias('Service.Namespace'), Namespace: alias('Service.Namespace'),
ServiceChecks: filter('Checks', function(item, i, arr) { ExternalSources: computed('Service.Meta', function() {
const sources = Object.entries(this.Service.Meta || {})
.filter(([key, value]) => key === 'external-source')
.map(([key, value]) => {
return value;
});
return [...new Set(sources)];
}),
ServiceChecks: filter('Checks.[]', function(item, i, arr) {
return item.ServiceID !== ''; return item.ServiceID !== '';
}), }),
NodeChecks: filter('Checks', function(item, i, arr) { NodeChecks: filter('Checks.[]', function(item, i, arr) {
return item.ServiceID === ''; return item.ServiceID === '';
}), }),
Status: computed('ChecksPassing', 'ChecksWarning', 'ChecksCritical', function() {
switch (true) {
case this.ChecksCritical.length !== 0:
return 'critical';
case this.ChecksWarning.length !== 0:
return 'warning';
case this.ChecksPassing.length !== 0:
return 'passing';
default:
return 'empty';
}
}),
ChecksPassing: computed('Checks.[]', function() {
return this.Checks.filter(item => item.Status === 'passing');
}),
ChecksWarning: computed('Checks.[]', function() {
return this.Checks.filter(item => item.Status === 'warning');
}),
ChecksCritical: computed('Checks.[]', function() {
return this.Checks.filter(item => item.Status === 'critical');
}),
PercentageChecksPassing: computed('Checks.[]', 'ChecksPassing', function() {
return (this.ChecksPassing.length / this.Checks.length) * 100;
}),
PercentageChecksWarning: computed('Checks.[]', 'ChecksWarning', function() {
return (this.ChecksWarning.length / this.Checks.length) * 100;
}),
PercentageChecksCritical: computed('Checks.[]', 'ChecksCritical', function() {
return (this.ChecksCritical.length / this.Checks.length) * 100;
}),
}); });

View File

@ -1,5 +1,7 @@
import Model from 'ember-data/model'; import Model from 'ember-data/model';
import attr from 'ember-data/attr'; import attr from 'ember-data/attr';
import { computed } from '@ember/object';
import { MANAGEMENT_ID } from 'consul-ui/models/policy';
export const PRIMARY_KEY = 'uid'; export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'AccessorID'; export const SLUG_KEY = 'AccessorID';
@ -24,6 +26,9 @@ export default Model.extend({
Datacenter: attr('string'), Datacenter: attr('string'),
Namespace: attr('string'), Namespace: attr('string'),
Local: attr('boolean'), Local: attr('boolean'),
isGlobalManagement: computed('Policies.[]', function() {
return (this.Policies || []).find(item => item.ID === MANAGEMENT_ID);
}),
Policies: attr({ Policies: attr({
defaultValue: function() { defaultValue: function() {
return []; return [];

View File

@ -0,0 +1,23 @@
import Service from '@ember/service';
import service from 'consul-ui/filter/predicates/service';
import serviceInstance from 'consul-ui/filter/predicates/service-instance';
import node from 'consul-ui/filter/predicates/node';
import intention from 'consul-ui/filter/predicates/intention';
import token from 'consul-ui/filter/predicates/token';
import policy from 'consul-ui/filter/predicates/policy';
const predicates = {
service: service(),
serviceInstance: serviceInstance(),
node: node(),
intention: intention(),
token: token(),
policy: policy(),
};
export default Service.extend({
predicate: function(type) {
return predicates[type];
},
});

View File

@ -16,7 +16,7 @@ export default RepositoryService.extend({
const query = { const query = {
dc: dc, dc: dc,
nspace: nspace, nspace: nspace,
filter: `SourceName == "${slug}" or DestinationName == "${slug}"`, filter: `SourceName == "${slug}" or DestinationName == "${slug}" or SourceName == "*" or DestinationName == "*"`,
}; };
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;

View File

@ -0,0 +1,23 @@
export default () => key => {
if (key.startsWith('Status:')) {
const [, dir] = key.split(':');
const props = [
'PercentageChecksPassing',
'PercentageChecksWarning',
'PercentageChecksCritical',
];
if (dir === 'asc') {
props.reverse();
}
return function(a, b) {
for (let i in props) {
let prop = props[i];
if (a[prop] === b[prop]) {
continue;
}
return a[prop] > b[prop] ? -1 : 1;
}
};
}
return key;
};

View File

@ -1,5 +1,2 @@
@import './skin'; @import './skin';
@import './layout'; @import './layout';
%sort-button {
@extend %split-button;
}

View File

@ -6,7 +6,6 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
position: relative; position: relative;
top: 2px;
} }
%popover-menu-toggle:checked + label > *::after { %popover-menu-toggle:checked + label > *::after {
@extend %with-chevron-up-mask; @extend %with-chevron-up-mask;

View File

@ -948,6 +948,16 @@
mask-image: $logo-bitbucket-monochrome-svg; mask-image: $logo-bitbucket-monochrome-svg;
} }
%with-logo-consul-color-icon {
@extend %with-icon;
background-image: $consul-logo-color-svg;
}
%with-logo-consul-color-mask {
@extend %with-mask;
-webkit-mask-image: $consul-logo-color-svg;
mask-image: $consul-logo-color-svg;
}
%with-logo-gcp-color-icon { %with-logo-gcp-color-icon {
@extend %with-icon; @extend %with-icon;
background-image: $logo-gcp-color-svg; background-image: $logo-gcp-color-svg;
@ -1047,6 +1057,15 @@
-webkit-mask-image: $logo-microsoft-color-svg; -webkit-mask-image: $logo-microsoft-color-svg;
mask-image: $logo-microsoft-color-svg; mask-image: $logo-microsoft-color-svg;
} }
%with-logo-nomad-color-icon {
@extend %with-icon;
background-image: $nomad-logo-color-svg;
}
%with-logo-nomad-color-mask {
@extend %with-mask;
-webkit-mask-image: $nomad-logo-color-svg;
mask-image: $nomad-logo-color-svg;
}
%with-logo-okta-color-icon { %with-logo-okta-color-icon {
@extend %with-icon; @extend %with-icon;
@ -1098,6 +1117,15 @@
mask-image: $logo-slack-monochrome-svg; mask-image: $logo-slack-monochrome-svg;
} }
%with-logo-terraform-color-icon {
@extend %with-icon;
background-image: $terraform-logo-color-svg;
}
%with-logo-terraform-color-mask {
@extend %with-mask;
-webkit-mask-image: $terraform-logo-color-svg;
mask-image: $terraform-logo-color-svg;
}
%with-logo-vmware-color-icon { %with-logo-vmware-color-icon {
@extend %with-icon; @extend %with-icon;
background-image: $logo-vmware-color-svg; background-image: $logo-vmware-color-svg;

View File

@ -72,10 +72,10 @@ main {
display: none; display: none;
} }
#toolbar-toggle:checked + * { #toolbar-toggle:checked + * {
display: block; display: flex;
} }
html.template-service.template-show #toolbar-toggle + * { html.template-service.template-show #toolbar-toggle + * {
display: block; display: flex;
padding: 4px; padding: 4px;
} }
html.template-service.template-show .actions { html.template-service.template-show .actions {

View File

@ -3,14 +3,18 @@
.filter-bar { .filter-bar {
@extend %filter-bar; @extend %filter-bar;
} }
%filter-bar { %filter-bar .popover-select {
height: 35px;
position: relative; position: relative;
z-index: 3; z-index: 3;
} }
%filter-bar:not(.with-sort) { %filter-bar [role='menuitem'] {
justify-content: normal !important;
}
html.template-acl.template-list .filter-bar {
@extend %filter-bar-reversed; @extend %filter-bar-reversed;
} }
%filter-bar [role='radiogroup'] { html.template-acl.template-list .filter-bar [role='radiogroup'] {
@extend %expanded-single-select; @extend %expanded-single-select;
} }
%filter-bar span::before { %filter-bar span::before {
@ -19,6 +23,11 @@
margin-left: -2px; margin-left: -2px;
} }
%filter-bar .popover-menu > [type='checkbox']:checked + label button {
color: $blue-500;
background-color: $gray-100;
}
%filter-bar .value-passing span::before { %filter-bar .value-passing span::before {
@extend %with-check-circle-fill-icon, %as-pseudo; @extend %with-check-circle-fill-icon, %as-pseudo;
} }

View File

@ -5,34 +5,50 @@
margin-top: 0 !important; margin-top: 0 !important;
margin-bottom: -12px; margin-bottom: -12px;
} }
%filter-bar .filters {
display: flex;
margin-right: 12px;
}
%filter-bar .filters > *:not(:last-child) {
margin-right: 6px;
}
%filter-bar + :not(.notice) { %filter-bar + :not(.notice) {
margin-top: 1.8em; margin-top: 1.8em;
} }
%filter-bar fieldset {
flex: 0 1 auto;
width: auto;
}
%filter-bar-reversed { %filter-bar-reversed {
flex-direction: row-reverse; flex-direction: row-reverse;
padding: 4px; padding: 4px;
margin-bottom: 8px !important; margin-bottom: 8px !important;
} }
%filter-bar fieldset { %filter-bar-reversed fieldset {
flex: 0 1 auto; min-width: 210px;
width: auto; width: auto;
} }
%filter-bar fieldset:first-child:not(:last-child) {
flex: 1 1 auto;
margin-right: 12px;
}
%filter-bar-reversed fieldset:first-child:not(:last-child) { %filter-bar-reversed fieldset:first-child:not(:last-child) {
flex: 0 1 auto; flex: 0 1 auto;
margin-left: auto; margin-left: auto;
} }
%filter-bar-reversed fieldset {
min-width: 210px;
width: auto;
}
%filter-bar-reversed > *:first-child { %filter-bar-reversed > *:first-child {
margin-left: 12px; margin-left: 12px;
} }
@media #{$--horizontal-filters} {
%filter-bar fieldset:first-child:not(:last-child) {
flex: 1 1 auto;
margin-right: 12px;
}
}
@media #{$--lt-horizontal-filters} { @media #{$--lt-horizontal-filters} {
%filter-bar {
flex-wrap: wrap;
}
%filter-bar fieldset {
flex: 0 1 100%;
margin-bottom: 8px;
}
%filter-bar-reversed > *:first-child { %filter-bar-reversed > *:first-child {
margin-left: 0; margin-left: 0;
} }

View File

@ -13,9 +13,6 @@
} }
} }
@media #{$--lt-horizontal-selects} { @media #{$--lt-horizontal-selects} {
%filter-bar label:not(:last-child) {
border-bottom: $decor-border-100;
}
} }
%filter-bar [role='radiogroup'] label { %filter-bar [role='radiogroup'] label {
cursor: pointer; cursor: pointer;

View File

@ -14,6 +14,9 @@
right: auto; right: auto;
top: 28px !important; top: 28px !important;
} }
%main-nav-horizontal .popover-menu > label > button::after {
top: 2px;
}
@media #{$--horizontal-nav} { @media #{$--horizontal-nav} {
%main-nav-horizontal > ul, %main-nav-horizontal > ul,
%main-nav-horizontal-panel { %main-nav-horizontal-panel {

View File

@ -1,6 +1,64 @@
.popover-select { .popover-select {
@extend %popover-select; @extend %popover-select;
} }
%popover-select label {
height: 100%;
}
%popover-select label > * { %popover-select label > * {
@extend %button;
padding: 0 8px !important;
height: 100% !important;
justify-content: space-between !important;
min-width: auto !important;
}
%popover-select label > *::after {
margin-left: 6px;
}
%popover-select.type-sort label > * {
@extend %sort-button; @extend %sort-button;
} }
%popover-select.type-access button::before,
%popover-select.type-source button::before,
%popover-select.type-status button::before {
margin-right: 10px;
}
%popover-select .value-allow button::before,
%popover-select .value-passing button::before {
@extend %with-check-circle-fill-mask, %as-pseudo;
color: $green-500;
}
%popover-select .value-warning button::before {
@extend %with-alert-triangle-mask, %as-pseudo;
color: $orange-500;
}
%popover-select .value-deny button::before,
%popover-select .value-critical button::before {
@extend %with-cancel-square-fill-mask, %as-pseudo;
color: $red-500;
}
%popover-select .value-empty button::before {
@extend %with-minus-square-fill-mask, %as-pseudo;
color: $gray-400;
}
%popover-select.type-source li button {
text-transform: capitalize;
}
%popover-select.type-source li.aws button {
text-transform: uppercase;
}
%popover-select .aws button::before {
@extend %with-logo-aws-color-icon, %as-pseudo;
}
%popover-select .kubernetes button::before {
@extend %with-logo-kubernetes-color-icon, %as-pseudo;
}
%popover-select .consul button::before {
@extend %with-logo-consul-color-icon, %as-pseudo;
}
%popover-select .nomad button::before {
@extend %with-logo-nomad-color-icon, %as-pseudo;
}
%popover-select .terraform button::before {
@extend %with-logo-terraform-color-icon, %as-pseudo;
}

View File

@ -3,115 +3,100 @@
{{else}} {{else}}
{{title 'Access Controls'}} {{title 'Access Controls'}}
{{/if}} {{/if}}
{{#let (hash
types=(if type (split type ',') undefined)
dcs=(if dc (split dc ',') undefined)
) as |filters|}}
{{#let (or sortBy "Name:asc") as |sort|}}
<AppView
@class="policy list"
@loading={{isLoading}}
@authorized={{isAuthorized}}
@enabled={{isEnabled}}
>
<BlockSlot @name="notification" as |status type|>
{{partial 'dc/acls/policies/notifications'}}
</BlockSlot>
<BlockSlot @name="header">
<h1>
Access Controls
</h1>
</BlockSlot>
<BlockSlot @name="nav">
{{#if isAuthorized }}
{{partial 'dc/acls/nav'}}
{{/if}}
</BlockSlot>
<BlockSlot @name="disabled">
{{partial 'dc/acls/disabled'}}
</BlockSlot>
<BlockSlot @name="authorization">
{{partial 'dc/acls/authorization'}}
</BlockSlot>
<BlockSlot @name="actions">
<a data-test-create href="{{href-to 'dc.acls.policies.create'}}" class="type-create">Create</a>
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0) }}
<ConsulPolicySearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
{{#let (or sortBy "Name:asc") as |sort|}} @sort={{sort}}
<AppView @onsort={{action (mut sortBy) value="target.selected"}}
@class="policy list"
@loading={{isLoading}} @filter={{filters}}
@authorized={{isAuthorized}} @onfilter={{hash
@enabled={{isEnabled}} dc=(action (mut dc) value="target.selectedItems")
> type=(action (mut type) value="target.selectedItems")
<BlockSlot @name="notification" as |status type|> }}
{{partial 'dc/acls/policies/notifications'}}
</BlockSlot>
<BlockSlot @name="header">
<h1>
Access Controls
</h1>
</BlockSlot>
<BlockSlot @name="nav">
{{#if isAuthorized }}
{{partial 'dc/acls/nav'}}
{{/if}}
</BlockSlot>
<BlockSlot @name="disabled">
{{partial 'dc/acls/disabled'}}
</BlockSlot>
<BlockSlot @name="authorization">
{{partial 'dc/acls/authorization'}}
</BlockSlot>
<BlockSlot @name="actions">
<a data-test-create href="{{href-to 'dc.acls.policies.create'}}" class="type-create">Create</a>
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0) }}
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value="target.value"}}
class="with-sort"
>
<BlockSlot @name="secondary">
<PopoverSelect
@position="right"
@onchange={{action (mut sortBy) value='target.selected'}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</BlockSlot>
</SearchBar>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
{{#let (sort-by (comparator 'policy' sort) items) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'policy' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<ConsulPolicyList
@items={{filtered}}
@ondelete={{action send 'delete'}}
/> />
{{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="empty"> <BlockSlot @name="content">
<EmptyState @allowLogin={{true}}> {{#let (filter (filter-predicate 'policy' filters) items) as |filtered|}}
<BlockSlot @name="header"> {{#let (sort-by (comparator 'policy' sort) filtered) as |sorted|}}
<h2> <ChangeableSet @dispatcher={{searchable 'policy' sorted}} @terms={{search}}>
{{#if (gt items.length 0)}} <BlockSlot @name="set" as |searched|>
No policies found <ConsulPolicyList
{{else}} @items={{searched}}
Welcome to Policies @ondelete={{action send 'delete'}}
{{/if}} />
</h2>
</BlockSlot> </BlockSlot>
<BlockSlot @name="body"> <BlockSlot @name="empty">
<p> <EmptyState @allowLogin={{true}}>
{{#if (gt items.length 0)}} <BlockSlot @name="header">
No policies where found matching that search, or you may not have access to view the policies you are searching for. <h2>
{{else}} {{#if (gt items.length 0)}}
There don't seem to be any policies, or you may not have access to view policies yet. No policies found
{{/if}} {{else}}
</p> Welcome to Policies
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
{{#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">
<li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}/commands/acl/policy" rel="noopener noreferrer" target="_blank">Documentation on policies</a>
</li>
<li class="learn-link">
<a href="{{env 'CONSUL_LEARN_URL'}}/consul/security-networking/managing-acl-policies" rel="noopener noreferrer" target="_blank">Read the guide</a>
</li>
</BlockSlot>
</EmptyState>
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions"> </ChangeableSet>
<li class="docs-link"> {{/let}}
<a href="{{env 'CONSUL_DOCS_URL'}}/commands/acl/policy" rel="noopener noreferrer" target="_blank">Documentation on policies</a> {{/let}}
</li> </BlockSlot>
<li class="learn-link"> </AppView>
<a href="{{env 'CONSUL_LEARN_URL'}}/consul/security-networking/managing-acl-policies" rel="noopener noreferrer" target="_blank">Read the guide</a>
</li>
</BlockSlot>
</EmptyState>
</BlockSlot>
</ChangeableSet>
{{/let}} {{/let}}
</BlockSlot>
</AppView>
{{/let}} {{/let}}

View File

@ -35,46 +35,13 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
{{#if (gt items.length 0) }} {{#if (gt items.length 0) }}
<SearchBar <ConsulRoleSearchBar
@value={{search}} @search={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
class="with-sort"
> @sort={{sort}}
<BlockSlot @name="secondary"> @onsort={{action (mut sortBy) value="target.selected"}}
<PopoverSelect />
@position="right"
@onchange={{action (mut sortBy) value='target.selected'}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
(array "CreateIndex:desc" "Newest to oldest")
(array "CreateIndex:asc" "Oldest to newest")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Creation">
<Option @value="CreateIndex:desc" @selected={{eq "CreateIndex:desc" sort}}>Newest to oldest</Option>
<Option @value="CreateIndex:asc" @selected={{eq "CreateIndex:asc" sort}}>Oldest to newest</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</BlockSlot>
</SearchBar>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="content">

View File

@ -4,113 +4,97 @@
{{title 'Access Controls'}} {{title 'Access Controls'}}
{{/if}} {{/if}}
{{#let (or sortBy "CreateTime:desc") as |sort|}} {{#let (hash
<AppView types=(if type (split type ',') undefined)
@class="token list" ) as |filters|}}
@loading={{isLoading}} {{#let (or sortBy "CreateTime:desc") as |sort|}}
@authorized={{isAuthorized}} <AppView
@enabled={{isEnabled}} @class="token list"
> @loading={{isLoading}}
<BlockSlot @name="notification" as |status type subject|> @authorized={{isAuthorized}}
{{partial 'dc/acls/tokens/notifications'}} @enabled={{isEnabled}}
</BlockSlot> >
<BlockSlot @name="header"> <BlockSlot @name="notification" as |status type subject|>
<h1> {{partial 'dc/acls/tokens/notifications'}}
Access Controls </BlockSlot>
</h1> <BlockSlot @name="header">
</BlockSlot> <h1>
<BlockSlot @name="nav"> Access Controls
{{#if isAuthorized }} </h1>
{{partial 'dc/acls/nav'}} </BlockSlot>
{{/if}} <BlockSlot @name="nav">
</BlockSlot> {{#if isAuthorized }}
<BlockSlot @name="disabled"> {{partial 'dc/acls/nav'}}
{{partial 'dc/acls/disabled'}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="authorization"> <BlockSlot @name="disabled">
{{partial 'dc/acls/authorization'}} {{partial 'dc/acls/disabled'}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions"> <BlockSlot @name="authorization">
<a data-test-create href="{{href-to 'dc.acls.tokens.create'}}" class="type-create">Create</a> {{partial 'dc/acls/authorization'}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="actions">
{{#if (gt items.length 0)}} <a data-test-create href="{{href-to 'dc.acls.tokens.create'}}" class="type-create">Create</a>
<SearchBar </BlockSlot>
@value={{search}} <BlockSlot @name="toolbar">
@onsearch={{action (mut search) value="target.value"}} {{#if (gt items.length 0)}}
class="with-sort" <ConsulTokenSearchBar
> @search={{search}}
<BlockSlot @name="secondary"> @onsearch={{action (mut search) value="target.value"}}
<PopoverSelect
@position="right" @sort={{sort}}
@onchange={{action (mut sortBy) value='target.selected'}} @onsort={{action (mut sortBy) value="target.selected"}}
@multiple={{false}}
as |components|> @filter={{filters}}
<BlockSlot @name="selected"> @onfilter={{hash
<span> type=(action (mut type) value="target.selectedItems")
{{#let (from-entries (array }}
(array "CreateTime:desc" "Newest to oldest") />
(array "CreateTime:asc" "Oldest to newest") {{/if}}
)) </BlockSlot>
as |selectable| <BlockSlot @name="content">
}} {{#if (token/is-legacy items)}}
{{get selectable sort}} <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>
{{/let}} {{/if}}
</span> {{#let (filter (filter-predicate 'token' filters) items) as |filtered|}}
</BlockSlot> {{#let (sort-by (comparator 'token' sort) filtered) as |sorted|}}
<BlockSlot @name="options"> <ChangeableSet @dispatcher={{searchable 'token' sorted}} @terms={{search}}>
{{#let components.Optgroup components.Option as |Optgroup Option|}} <BlockSlot @name="set" as |searched|>
<Optgroup @label="Creation"> <ConsulTokenList
<Option @value="CreateTime:desc" @selected={{eq "CreateTime:desc" sort}}>Newest to oldest</Option> @items={{searched}}
<Option @value="CreateTime:asc" @selected={{eq "CreateTime:asc" sort}}>Oldest to newest</Option> @token={{token}}
</Optgroup> @onuse={{action send 'use'}}
@ondelete={{action send 'delete'}}
@onlogout={{action send 'logout'}}
@onclone={{action send 'clone'}}
/>
</BlockSlot>
<BlockSlot @name="empty">
<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>
{{/let}}
{{/let}} {{/let}}
</BlockSlot>
</PopoverSelect>
</BlockSlot>
</SearchBar>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
{{#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}}
{{#let (sort-by (comparator 'token' sort) items) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'token' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<ConsulTokenList
@items={{filtered}}
@token={{token}}
@onuse={{action send 'use'}}
@ondelete={{action send 'delete'}}
@onlogout={{action send 'logout'}}
@onclone={{action send 'clone'}}
/>
</BlockSlot> </BlockSlot>
<BlockSlot @name="empty"> </AppView>
<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>
{{/let}} {{/let}}
</BlockSlot>
</AppView>
{{/let}} {{/let}}

View File

@ -6,11 +6,15 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="loaded"> <BlockSlot @name="loaded">
{{#let api.data as |items|}}
{{#let (hash
accesses=(if access (split access ',') undefined)
) as |filters|}}
{{#let (or sortBy "Action:asc") as |sort|}} {{#let (or sortBy "Action:asc") as |sort|}}
<AppView @class="intention list"> <AppView @class="intention list">
<BlockSlot @name="header"> <BlockSlot @name="header">
<h1> <h1>
Intentions <em>{{format-number api.data.length}} total</em> Intentions <em>{{format-number items.length}} total</em>
</h1> </h1>
<label for="toolbar-toggle"></label> <label for="toolbar-toggle"></label>
</BlockSlot> </BlockSlot>
@ -18,73 +22,34 @@
<a data-test-create href="{{href-to 'dc.intentions.create'}}" class="type-create">Create</a> <a data-test-create href="{{href-to 'dc.intentions.create'}}" class="type-create">Create</a>
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
{{#if (gt api.data.length 0) }} {{#if (gt items.length 0) }}
<SearchBar <ConsulIntentionSearchBar
@value={{search}} @search={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
class="with-sort"
> @sort={{sort}}
<BlockSlot @name="secondary"> @onsort={{action (mut sortBy) value="target.selected"}}
<PopoverSelect
@position="right" @filter={{filters}}
@onchange={{action (mut sortBy) value='target.selected'}} @onfilter={{hash
@multiple={{false}} access=(action (mut access) value="target.selectedItems")
as |components|> }}
<BlockSlot @name="selected"> />
<span> {{/if}}
{{#let (from-entries (array
(array "Action:asc" "Allow to Deny")
(array "Action:desc" "Deny to Allow")
(array "SourceName:asc" "Source: A to Z")
(array "SourceName:desc" "Source: Z to A")
(array "DestinationName:asc" "Destination: A to Z")
(array "DestinationName:desc" "Destination: Z to A")
(array "Precedence:asc" "Precedence: Ascending")
(array "Precedence:desc" "Precedence: Descending")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Permission">
<Option @value="Action:asc" @selected={{eq "Action:asc" sort}}>Allow to Deny</Option>
<Option @value="Action:desc" @selected={{eq "Action:desc" sort}}>Deny to Allow</Option>
</Optgroup>
<Optgroup @label="Source">
<Option @value="SourceName:asc" @selected={{eq "SourceName:asc" sort}}>A to Z</Option>
<Option @value="SourceName:desc" @selected={{eq "SourceName:desc" sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Destination">
<Option @value="DestinationName:asc" @selected={{eq "DestinationName:asc" sort}}>A to Z</Option>
<Option @value="DestinationName:desc" @selected={{eq "DestinationName:desc" sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Precedence">
<Option @value="Precedence:asc" @selected={{eq "Precedence:asc" sort}}>Ascending</Option>
<Option @value="Precedence:desc" @selected={{eq "Precedence:desc" sort}}>Descending</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</BlockSlot>
</SearchBar>
{{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="content">
{{#let (sort-by (comparator 'intention' sort) api.data) as |sorted|}} {{#let (filter (filter-predicate 'intention' filters) items) as |filtered|}}
{{#let (sort-by (comparator 'intention' sort) filtered) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'intention' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'intention' sorted}} @terms={{search}}>
<BlockSlot @name="content" as |filtered|> <BlockSlot @name="content" as |searched|>
<ConsulIntentionList <ConsulIntentionList
@items={{filtered}} @items={{searched}}
@ondelete={{refresh-route}} @ondelete={{refresh-route}}
> >
<EmptyState @allowLogin={{true}}> <EmptyState @allowLogin={{true}}>
<BlockSlot @name="header"> <BlockSlot @name="header">
<h2> <h2>
{{#if (gt api.data.length 0)}} {{#if (gt items.length 0)}}
No intentions found No intentions found
{{else}} {{else}}
Welcome to Intentions Welcome to Intentions
@ -93,7 +58,7 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="body"> <BlockSlot @name="body">
<p> <p>
{{#if (gt api.data.length 0)}} {{#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. No intentions where found matching that search, or you may not have access to view the intentions you are searching for.
{{else}} {{else}}
There don't seem to be any intentions, or you may not have access to view intentions yet. There don't seem to be any intentions, or you may not have access to view intentions yet.
@ -112,9 +77,12 @@
</ConsulIntentionList> </ConsulIntentionList>
</BlockSlot> </BlockSlot>
</ChangeableSet> </ChangeableSet>
{{/let}} {{/let}}
{{/let}}
</BlockSlot> </BlockSlot>
</AppView> </AppView>
{{/let}} {{/let}}
{{/let}}
{{/let}}
</BlockSlot> </BlockSlot>
</DataLoader> </DataLoader>

View File

@ -23,46 +23,48 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
{{#if (gt items.length 0) }} {{#if (gt items.length 0) }}
<SearchBar <form class="filter-bar with-sort">
@value={{search}} <FreetextFilter
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
class="with-sort" @value={{search}}
> @placeholder="Search"
<BlockSlot @name="secondary"> />
<PopoverSelect <div class="sort">
@position="right" <PopoverSelect
@onchange={{action (mut sortBy) value='target.selected'}} class="type-sort"
@multiple={{false}} @position="right"
as |components|> @onchange={{action (mut sortBy) value='target.selected'}}
<BlockSlot @name="selected"> @multiple={{false}}
<span> as |components|>
{{#let (from-entries (array <BlockSlot @name="selected">
(array "Key:asc" "A to Z") <span>
(array "Key:desc" "Z to A") {{#let (from-entries (array
(array "isFolder:desc" "Folders to Keys") (array "Key:asc" "A to Z")
(array "isFolder:asc" "Keys to Folders") (array "Key:desc" "Z to A")
)) (array "isFolder:desc" "Folders to Keys")
as |selectable| (array "isFolder:asc" "Keys to Folders")
}} ))
{{get selectable sort}} as |selectable|
{{/let}} }}
</span> {{get selectable sort}}
</BlockSlot> {{/let}}
<BlockSlot @name="options"> </span>
{{#let components.Optgroup components.Option as |Optgroup Option|}} </BlockSlot>
<Optgroup @label="Name"> <BlockSlot @name="options">
<Option @value="Key:asc" @selected={{eq "Key:asc" sort}}>A to Z</Option> {{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="Key:desc" @selected={{eq "Key:desc" sort}}>Z to A</Option> <Optgroup @label="Name">
</Optgroup> <Option @value="Key:asc" @selected={{eq "Key:asc" sort}}>A to Z</Option>
<Optgroup @label="Type"> <Option @value="Key:desc" @selected={{eq "Key:desc" sort}}>Z to A</Option>
<Option @value="isFolder:desc" @selected={{eq "isFolder:desc" sort}}>Folders to Keys</Option> </Optgroup>
<Option @value="isFolder:asc" @selected={{eq "isFolder:asc" sort}}>Keys to Folders</Option> <Optgroup @label="Type">
</Optgroup> <Option @value="isFolder:desc" @selected={{eq "isFolder:desc" sort}}>Folders to Keys</Option>
{{/let}} <Option @value="isFolder:asc" @selected={{eq "isFolder:asc" sort}}>Keys to Folders</Option>
</BlockSlot> </Optgroup>
</PopoverSelect> {{/let}}
</BlockSlot> </BlockSlot>
</SearchBar> </PopoverSelect>
</div>
</form>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions"> <BlockSlot @name="actions">

View File

@ -1,5 +1,8 @@
{{title 'Nodes'}} {{title 'Nodes'}}
<EventSource @src={{items}} /> <EventSource @src={{items}} />
{{#let (hash
statuses=(if status (split status ',') undefined)
) as |filters|}}
{{#let (or sortBy "Node:asc") as |sort|}} {{#let (or sortBy "Node:asc") as |sort|}}
<AppView @class="node list"> <AppView @class="node list">
<BlockSlot @name="header"> <BlockSlot @name="header">
@ -10,53 +13,26 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
{{#if (gt items.length 0) }} {{#if (gt items.length 0) }}
<SearchBar <ConsulNodeSearchBar
@value={{search}} @search={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
class="with-sort"
> @sort={{sort}}
<BlockSlot @name="secondary"> @onsort={{action (mut sortBy) value="target.selected"}}
<PopoverSelect
@position="right" @filter={{filters}}
@onchange={{action (mut sortBy) value='target.selected'}} @onfilter={{hash
@multiple={{false}} status=(action (mut status) value="target.selectedItems")
as |components|> }}
<BlockSlot @name="selected"> />
<span>
{{#let (from-entries (array
(array "Node:asc" "A to Z")
(array "Node:desc" "Z to A")
(array "Status:asc" "Unhealthy to Healthy")
(array "Status:desc" "Healthy to Unhealthy")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Name">
<Option @value="Node:asc" @selected={{eq "Node:asc" sort}}>A to Z</Option>
<Option @value="Node:desc" @selected={{eq "Node:desc" sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Health Status">
<Option @value="Status:asc" @selected={{eq "Status:asc" sort}}>Unhealthy to Healthy</Option>
<Option @value="Status:desc" @selected={{eq "Status:desc" sort}}>Healthy to Unhealthy</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</BlockSlot>
</SearchBar>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="content">
{{#let (sort-by (comparator 'node' sort) items) as |sorted|}} {{#let (filter (filter-predicate 'node' filters) items) as |filtered|}}
{{#let (sort-by (comparator 'node' sort) filtered) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'node' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'node' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="set" as |searched|>
<ConsulNodeList @items={{filtered}} @leader={{leader}} /> <ConsulNodeList @items={{searched}} @leader={{leader}} />
</BlockSlot> </BlockSlot>
<BlockSlot @name="empty"> <BlockSlot @name="empty">
<EmptyState> <EmptyState>
@ -68,7 +44,9 @@
</EmptyState> </EmptyState>
</BlockSlot> </BlockSlot>
</ChangeableSet> </ChangeableSet>
{{/let}} {{/let}}
{{/let}}
</BlockSlot> </BlockSlot>
</AppView> </AppView>
{{/let}}
{{/let}} {{/let}}

View File

@ -15,40 +15,14 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<SearchBar <ConsulNspaceSearchBar
@value={{search}} @search={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
class="with-sort"
> @sort={{sort}}
<BlockSlot @name="secondary"> @onsort={{action (mut sortBy) value="target.selected"}}
<PopoverSelect
@position="right" />
@onchange={{action (mut sortBy) value='target.selected'}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</BlockSlot>
</SearchBar>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="content">

View File

@ -1,65 +1,46 @@
{{title 'Services'}} {{title 'Services'}}
<EventSource @src={{items}} /> <EventSource @src={{items}} />
{{#let (or sortBy "Name:asc") as |sort|}} {{#let (hash
statuses=(if status (split status ',') undefined)
types=(if type (split type ',') undefined)
sources=(if source (split source ',') undefined)
) as |filters|}}
{{#let (or sortBy "Name:asc") as |sort|}}
<AppView @class="service list"> <AppView @class="service list">
<BlockSlot @name="notification" as |status type|> <BlockSlot @name="notification" as |status type|>
{{partial 'dc/services/notifications'}} {{partial 'dc/services/notifications'}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="header"> <BlockSlot @name="header">
<h1> <h1>
Services <em>{{format-number services.length}} total</em> Services <em>{{format-number services.length}} total</em>
</h1> </h1>
<label for="toolbar-toggle"></label> <label for="toolbar-toggle"></label>
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
{{#if (gt services.length 0) }} {{#if (gt services.length 0) }}
<SearchBar <ConsulServiceSearchBar
@value={{search}} @sources={{externalSources}}
@search={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
class="with-sort"
> @sort={{sort}}
<BlockSlot @name="secondary"> @onsort={{action (mut sortBy) value="target.selected"}}
<PopoverSelect
@position="right" @filter={{filters}}
@onchange={{action (mut sortBy) value='target.selected'}} @onfilter={{hash
@multiple={{false}} status=(action (mut status) value="target.selectedItems")
as |components|> type=(action (mut type) value="target.selectedItems")
<BlockSlot @name="selected"> source=(action (mut source) value="target.selectedItems")
<span> }}
{{#let (from-entries (array />
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
(array "Status:asc" "Unhealthy to Healthy")
(array "Status:desc" "Healthy to Unhealthy")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Health Status">
<Option @value="Status:asc" @selected={{eq "Status:asc" sort}}>Unhealthy to Healthy</Option>
<Option @value="Status:desc" @selected={{eq "Status:desc" sort}}>Healthy to Unhealthy</Option>
</Optgroup>
<Optgroup @label="Service Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</BlockSlot>
</SearchBar>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="content">
{{#let (sort-by (comparator 'service' sort) services) as |sorted|}} {{#let (filter (filter-predicate 'service' filters) services) as |filtered|}}
{{#let (sort-by (comparator 'service' sort) filtered) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'service' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'service' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="set" as |searched|>
<ConsulServiceList @items={{filtered}}/> <ConsulServiceList @items={{searched}} />
</BlockSlot> </BlockSlot>
<BlockSlot @name="empty"> <BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}> <EmptyState @allowLogin={{true}}>
@ -92,7 +73,9 @@
</EmptyState> </EmptyState>
</BlockSlot> </BlockSlot>
</ChangeableSet> </ChangeableSet>
{{/let}} {{/let}}
{{/let}}
</BlockSlot> </BlockSlot>
</AppView> </AppView>
{{/let}}
{{/let}} {{/let}}

View File

@ -1,15 +1,32 @@
<div id="instances" class="tab-section"> <div id="instances" class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
{{#if (gt items.length 0) }} {{#let (hash
statuses=(if status (split status ',') undefined)
sources=(if source (split source ',') undefined)
) as |filters|}}
{{#let (or sortBy "Name:asc") as |sort|}}
{{#if (gt items.length 0) }}
<input type="checkbox" id="toolbar-toggle" /> <input type="checkbox" id="toolbar-toggle" />
<SearchBar <ConsulServiceInstanceSearchBar
@value={{search}} @sources={{externalSources}}
@search={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
/>
{{/if}} @sort={{sort}}
<ChangeableSet @dispatcher={{searchable 'serviceInstance' items}} @terms={{search}}> @onsort={{action (mut sortBy) value="target.selected"}}
<BlockSlot @name="set" as |filtered|>
<ConsulServiceInstanceList @routeName="dc.services.instance" @items={{filtered}}/> @filter={{filters}}
@onfilter={{hash
status=(action (mut status) value="target.selectedItems")
source=(action (mut source) value="target.selectedItems")
}}
/>
{{/if}}
{{#let (filter (filter-predicate 'serviceInstance' filters) items) as |filtered|}}
{{#let (sort-by (comparator 'serviceInstance' sort) filtered) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'serviceInstance' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |searched|>
<ConsulServiceInstanceList @routeName="dc.services.instance" @items={{searched}}/>
</BlockSlot> </BlockSlot>
<BlockSlot @name="empty"> <BlockSlot @name="empty">
<EmptyState> <EmptyState>
@ -21,5 +38,9 @@
</EmptyState> </EmptyState>
</BlockSlot> </BlockSlot>
</ChangeableSet> </ChangeableSet>
{{/let}}
{{/let}}
{{/let}}
{{/let}}
</div> </div>
</div> </div>

View File

@ -3,87 +3,55 @@
<ErrorState @error={{api.error}} /> <ErrorState @error={{api.error}} />
</BlockSlot> </BlockSlot>
<BlockSlot @name="loaded"> <BlockSlot @name="loaded">
{{#let api.data as |items|}}
{{#let (hash
accesses=(if access (split access ',') undefined)
) as |filters|}}
{{#let (or sortBy "Action:asc") as |sort|}} {{#let (or sortBy "Action:asc") as |sort|}}
<div id="intentions" class="tab-section"> <div id="intentions" class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
<Portal @target="app-view-actions"> <Portal @target="app-view-actions">
<a data-test-create href={{href-to 'dc.services.show.intentions.create'}} class="type-create">Create</a> <a data-test-create href={{href-to 'dc.services.show.intentions.create'}} class="type-create">Create</a>
</Portal> </Portal>
{{#if (gt api.data.length 0) }} {{#if (gt items.length 0) }}
<SearchBar <ConsulIntentionSearchBar
@value={{search}} @search={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
class="with-sort"
> @sort={{sort}}
<BlockSlot @name="secondary"> @onsort={{action (mut sortBy) value="target.selected"}}
<PopoverSelect
@position="right" @filter={{filters}}
@onchange={{action (mut sortBy) value='target.selected'}} @onfilter={{hash
@multiple={{false}} access=(action (mut access) value="target.selectedItems")
as |components|> }}
<BlockSlot @name="selected"> />
<span> {{/if}}
{{#let (from-entries (array {{#let (filter (filter-predicate 'intention' filters) items) as |filtered|}}
(array "Action:asc" "Allow to Deny") {{#let (sort-by (comparator 'intention' sort) filtered) as |sorted|}}
(array "Action:desc" "Deny to Allow")
(array "SourceName:asc" "Source: A to Z")
(array "SourceName:desc" "Source: Z to A")
(array "DestinationName:asc" "Destination: A to Z")
(array "DestinationName:desc" "Destination: Z to A")
(array "Precedence:asc" "Precedence: Ascending")
(array "Precedence:desc" "Precedence: Descending")
))
as |selectable|
}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Permission">
<Option @value="Action:asc" @selected={{eq "Action:asc" sort}}>Allow to Deny</Option>
<Option @value="Action:desc" @selected={{eq "Action:desc" sort}}>Deny to Allow</Option>
</Optgroup>
<Optgroup @label="Source">
<Option @value="SourceName:asc" @selected={{eq "SourceName:asc" sort}}>A to Z</Option>
<Option @value="SourceName:desc" @selected={{eq "SourceName:desc" sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Destination">
<Option @value="DestinationName:asc" @selected={{eq "DestinationName:asc" sort}}>A to Z</Option>
<Option @value="DestinationName:desc" @selected={{eq "DestinationName:desc" sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Precedence">
<Option @value="Precedence:asc" @selected={{eq "Precedence:asc" sort}}>Ascending</Option>
<Option @value="Precedence:desc" @selected={{eq "Precedence:desc" sort}}>Descending</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</BlockSlot>
</SearchBar>
{{/if}}
{{#let (sort-by (comparator 'intention' sort) api.data) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'intention' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'intention' sorted}} @terms={{search}}>
<BlockSlot @name="content" as |filtered|> <BlockSlot @name="content" as |searched|>
<ConsulIntentionList <ConsulIntentionList
@items={{filtered}} @items={{searched}}
@ondelete={{refresh-route}} @ondelete={{refresh-route}}
@routeName="dc.services.show.intentions.edit" @routeName="dc.services.show.intentions.edit"
> >
<EmptyState> <EmptyState>
<BlockSlot @name="body"> <BlockSlot @name="body">
<p> <p>
There are no intentions {{if (gt intentions.length 0) 'found '}} for this service. There are no intentions {{if (gt items.length 0) 'found '}} for this service.
</p> </p>
</BlockSlot> </BlockSlot>
</EmptyState> </EmptyState>
</ConsulIntentionList> </ConsulIntentionList>
</BlockSlot> </BlockSlot>
</ChangeableSet> </ChangeableSet>
{{/let}} {{/let}}
{{/let}}
</div> </div>
</div> </div>
{{/let}} {{/let}}
{{/let}}
{{/let}}
</BlockSlot> </BlockSlot>
</DataLoader> </DataLoader>

View File

@ -1,22 +1,50 @@
<div id="services" class="tab-section"> <div id="services" class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
{{#if (gt gatewayServices.length 0)}} {{#let (hash
<p> instances=(if instance (split instance ',') undefined)
The following services may receive traffic from external services through this gateway. Learn more about configuring gateways in our ) as |filters|}}
<a href="{{env 'CONSUL_DOCS_URL'}}/connect/terminating-gateway" target="_blank" rel="noopener noreferrer">step-by-step guide</a>. {{#let (or sortBy "Name:asc") as |sort|}}
</p> {{#if (gt gatewayServices.length 0)}}
<ConsulServiceList <input type="checkbox" id="toolbar-toggle" />
@items={{gatewayServices}} <ConsulUpstreamSearchBar
@nspace={{nspace}} @search={{search}}
/> @onsearch={{action (mut search) value="target.value"}}
{{else}}
<EmptyState> @sort={{sort}}
<BlockSlot @name="body"> @onsort={{action (mut sortBy) value="target.selected"}}
<p>
There are no linked services. @filter={{filters}}
</p> @onfilter={{hash
</BlockSlot> instance=(action (mut instance) value="target.selectedItems")
</EmptyState> }}
{{/if}} />
{{/if}}
<p>
The following services may receive traffic from external services through this gateway. Learn more about configuring gateways in our
<a href="{{env 'CONSUL_DOCS_URL'}}/connect/terminating-gateway" target="_blank" rel="noopener noreferrer">step-by-step guide</a>.
</p>
{{#let (filter (filter-predicate 'service' filters) gatewayServices) as |filtered|}}
{{#let (sort-by (comparator 'service' sort) filtered) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'service' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |searched|>
<ConsulServiceList
@items={{searched}}
@nspace={{nspace}}
/>
</BlockSlot>
<BlockSlot @name="empty">
<EmptyState>
<BlockSlot @name="body">
<p>
There are no linked services.
</p>
</BlockSlot>
</EmptyState>
</BlockSlot>
</ChangeableSet>
{{/let}}
{{/let}}
{{/let}}
{{/let}}
</div> </div>
</div> </div>

View File

@ -1,18 +1,49 @@
<div id="upstreams" class="tab-section"> <div id="upstreams" class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
{{#if (gt gatewayServices.length 0)}} {{#let (hash
instances=(if instance (split instance ',') undefined)
) as |filters|}}
{{#let (or sortBy "Name:asc") as |sort|}}
{{#if (gt gatewayServices.length 0)}}
<input type="checkbox" id="toolbar-toggle" />
<ConsulUpstreamSearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
@filter={{filters}}
@onfilter={{hash
instance=(action (mut instance) value="target.selectedItems")
}}
/>
{{/if}}
<p> <p>
Upstreams are services that may receive traffic from this gateway. Learn more about configuring gateways in our <a href="{{env 'CONSUL_DOCS_URL'}}/connect/ingress-gateway" target="_blank" rel="noopener noreferrer">documentation</a>. Upstreams are services that may receive traffic from this gateway. Learn more about configuring gateways in our <a href="{{env 'CONSUL_DOCS_URL'}}/connect/ingress-gateway" target="_blank" rel="noopener noreferrer">documentation</a>.
</p> </p>
<ConsulUpstreamList @items={{gatewayServices}} @dc={{dc}} @nspace={{nspace}} /> {{#let (filter (filter-predicate 'service' filters) gatewayServices) as |filtered|}}
{{else}} {{#let (sort-by (comparator 'service' sort) filtered) as |sorted|}}
<EmptyState> <ChangeableSet @dispatcher={{searchable 'service' sorted}} @terms={{search}}>
<BlockSlot @name="body"> <BlockSlot @name="set" as |searched|>
<p> <ConsulServiceList
There are no upstreams. @items={{searched}}
</p> @nspace={{nspace}}
</BlockSlot> />
</EmptyState> </BlockSlot>
{{/if}} <BlockSlot @name="empty">
<EmptyState>
<BlockSlot @name="body">
<p>
There are no upstreams.
</p>
</BlockSlot>
</EmptyState>
</BlockSlot>
</ChangeableSet>
{{/let}}
{{/let}}
{{/let}}
{{/let}}
</div> </div>
</div> </div>

View File

@ -1,36 +1,36 @@
@setupApplicationTest @setupApplicationTest
Feature: dc / acls / policies / sorting Feature: dc / acls / policies / sorting
Scenario: Sorting Policies Scenario: Sorting Policies
Given 1 datacenter model with the value "dc-1" Given 1 datacenter model with the value "dc-1"
And 4 policy models from yaml And 4 policy models from yaml
--- ---
- Name: "system-A" - Name: "system-A"
- Name: "system-D" - Name: "system-D"
- Name: "system-C" - Name: "system-C"
- Name: "system-B" - Name: "system-B"
--- ---
When I visit the policies page for yaml When I visit the policies page for yaml
--- ---
dc: dc-1 dc: dc-1
--- ---
Then the url should be /dc-1/acls/policies Then the url should be /dc-1/acls/policies
Then I see 4 policy models Then I see 4 policy models
When I click selected on the sort When I click selected on the sort
When I click options.1.button on the sort When I click options.1.button on the sort
Then I see name on the policies vertically like yaml Then I see name on the policies vertically like yaml
--- ---
- "system-D" - "system-D"
- "system-C" - "system-C"
- "system-B" - "system-B"
- "system-A" - "system-A"
--- ---
When I click selected on the sort When I click selected on the sort
When I click options.0.button on the sort When I click options.0.button on the sort
Then I see name on the policies vertically like yaml Then I see name on the policies vertically like yaml
--- ---
- "system-A" - "system-A"
- "system-B" - "system-B"
- "system-C" - "system-C"
- "system-D" - "system-D"
--- ---

View File

@ -1,73 +1,73 @@
@setupApplicationTest @setupApplicationTest
Feature: dc / nodes / sorting Feature: dc / nodes / sorting
Scenario: Scenario:
Given 1 datacenter model with the value "dc-1" Given 1 datacenter model with the value "dc-1"
And 6 node models from yaml And 6 node models from yaml
--- ---
- Node: Node-A - Node: Node-A
Checks: Checks:
- Status: critical - Status: critical
- Node: Node-B - Node: Node-B
Checks: Checks:
- Status: passing - Status: passing
- Node: Node-C - Node: Node-C
Checks: Checks:
- Status: warning - Status: warning
- Node: Node-D - Node: Node-D
Checks: Checks:
- Status: critical - Status: critical
- Node: Node-E - Node: Node-E
Checks: Checks:
- Status: critical - Status: critical
- Node: Node-F - Node: Node-F
Checks: Checks:
- Status: warning - Status: warning
--- ---
When I visit the nodes page for yaml When I visit the nodes page for yaml
--- ---
dc: dc-1 dc: dc-1
--- ---
When I click selected on the sort When I click selected on the sort
When I click options.3.button on the sort When I click options.0.button on the sort
Then I see name on the nodes vertically like yaml Then I see name on the nodes vertically like yaml
--- ---
- Node-B - Node-A
- Node-C - Node-D
- Node-F - Node-E
- Node-A - Node-C
- Node-D - Node-F
- Node-E - Node-B
--- ---
When I click selected on the sort When I click selected on the sort
When I click options.2.button on the sort When I click options.1.button on the sort
Then I see name on the nodes vertically like yaml Then I see name on the nodes vertically like yaml
--- ---
- Node-A - Node-B
- Node-D - Node-C
- Node-E - Node-F
- Node-C - Node-A
- Node-F - Node-D
- Node-B - Node-E
--- ---
When I click selected on the sort When I click selected on the sort
When I click options.0.button on the sort When I click options.2.button on the sort
Then I see name on the nodes vertically like yaml Then I see name on the nodes vertically like yaml
--- ---
- Node-A - Node-A
- Node-B - Node-B
- Node-C - Node-C
- Node-D - Node-D
- Node-E - Node-E
- Node-F - Node-F
--- ---
When I click selected on the sort When I click selected on the sort
When I click options.1.button on the sort When I click options.3.button on the sort
Then I see name on the nodes vertically like yaml Then I see name on the nodes vertically like yaml
--- ---
- Node-F - Node-F
- Node-E - Node-E
- Node-D - Node-D
- Node-C - Node-C
- Node-B - Node-B
- Node-A - Node-A
--- ---

View File

@ -28,6 +28,7 @@ Feature: dc / services / show / upstreams
--- ---
And the title should be "ingress-gateway-1 - Consul" And the title should be "ingress-gateway-1 - Consul"
When I click upstreams on the tabs When I click upstreams on the tabs
And I see upstreamsIsSelected on the tabs
Then I see 3 service models on the tabs.upstreamsTab component Then I see 3 service models on the tabs.upstreamsTab component
Scenario: Don't see the Upstreams tab Scenario: Don't see the Upstreams tab
Given 1 datacenter model with the value "dc1" Given 1 datacenter model with the value "dc1"

View File

@ -40,15 +40,28 @@ Feature: dc / services / sorting
dc: dc-1 dc: dc-1
--- ---
When I click selected on the sort When I click selected on the sort
When I click options.3.button on the sort # unhealthy / healthy
When I click options.0.button on the sort
Then I see name on the services vertically like yaml Then I see name on the services vertically like yaml
--- ---
- Service-B
- Service-C
- Service-A
- Service-D
- Service-F - Service-F
- Service-E - Service-E
---
When I click selected on the sort
# healthy / unhealthy
When I click options.1.button on the sort
Then I see name on the services vertically like yaml
---
- Service-E
- Service-F
- Service-D - Service-D
- Service-A
- Service-C - Service-C
- Service-B - Service-B
- Service-A
--- ---
When I click selected on the sort When I click selected on the sort
When I click options.2.button on the sort When I click options.2.button on the sort
@ -62,24 +75,13 @@ Feature: dc / services / sorting
- Service-F - Service-F
--- ---
When I click selected on the sort When I click selected on the sort
When I click options.0.button on the sort When I click options.3.button on the sort
Then I see name on the services vertically like yaml Then I see name on the services vertically like yaml
--- ---
- Service-B
- Service-C
- Service-A
- Service-D
- Service-F - Service-F
- Service-E - Service-E
---
When I click selected on the sort
When I click options.1.button on the sort
Then I see name on the services vertically like yaml
---
- Service-E
- Service-F
- Service-D - Service-D
- Service-A
- Service-C - Service-C
- Service-B - Service-B
- Service-A
--- ---

View File

@ -2,6 +2,6 @@ export default function(visitable, creatable, policies, popoverSelect) {
return creatable({ return creatable({
visit: visitable('/:dc/acls/policies'), visit: visitable('/:dc/acls/policies'),
policies: policies(), policies: policies(),
sort: popoverSelect(), sort: popoverSelect('[data-test-sort-control]'),
}); });
} }

View File

@ -2,7 +2,7 @@ export default function(visitable, creatable, roles, popoverSelect) {
return { return {
visit: visitable('/:dc/acls/roles'), visit: visitable('/:dc/acls/roles'),
roles: roles(), roles: roles(),
sort: popoverSelect(), sort: popoverSelect('[data-test-sort-control]'),
...creatable(), ...creatable(),
}; };
} }

View File

@ -3,7 +3,7 @@ export default function(visitable, creatable, text, tokens, popoverSelect) {
visit: visitable('/:dc/acls/tokens'), visit: visitable('/:dc/acls/tokens'),
update: text('[data-test-notification-update]'), update: text('[data-test-notification-update]'),
tokens: tokens(), tokens: tokens(),
sort: popoverSelect(), sort: popoverSelect('[data-test-sort-control]'),
...creatable(), ...creatable(),
}; };
} }

View File

@ -2,7 +2,7 @@ export default function(visitable, creatable, clickable, intentions, popoverSele
return creatable({ return creatable({
visit: visitable('/:dc/intentions'), visit: visitable('/:dc/intentions'),
intentions: intentions(), intentions: intentions(),
sort: popoverSelect(), sort: popoverSelect('[data-test-sort-control]'),
create: clickable('[data-test-create]'), create: clickable('[data-test-create]'),
}); });
} }

View File

@ -8,6 +8,6 @@ export default function(visitable, text, clickable, attribute, collection, popov
visit: visitable('/:dc/nodes'), visit: visitable('/:dc/nodes'),
nodes: collection('.consul-node-list [data-test-list-row]', node), nodes: collection('.consul-node-list [data-test-list-row]', node),
home: clickable('[data-test-home]'), home: clickable('[data-test-home]'),
sort: popoverSelect(), sort: popoverSelect('[data-test-sort-control]'),
}; };
} }

View File

@ -2,6 +2,6 @@ export default function(visitable, creatable, nspaces, popoverSelect) {
return creatable({ return creatable({
visit: visitable('/:dc/namespaces'), visit: visitable('/:dc/namespaces'),
nspaces: nspaces(), nspaces: nspaces(),
sort: popoverSelect(), sort: popoverSelect('[data-test-sort-control]'),
}); });
} }

View File

@ -10,6 +10,6 @@ export default function(visitable, clickable, text, attribute, present, collecti
visit: visitable('/:dc/services'), visit: visitable('/:dc/services'),
services: collection('.consul-service-list > ul > li:not(:first-child)', service), services: collection('.consul-service-list > ul > li:not(:first-child)', service),
home: clickable('[data-test-home]'), home: clickable('[data-test-home]'),
sort: popoverSelect(), sort: popoverSelect('[data-test-sort-control]'),
}; };
} }

View File

@ -23,7 +23,7 @@ export default function(visitable, attribute, collection, text, intentions, filt
intentions: intentions(), intentions: intentions(),
}; };
page.tabs.upstreamsTab = { page.tabs.upstreamsTab = {
services: collection('.consul-upstream-list > ul > li:not(:first-child)', { services: collection('.consul-service-list > ul > li:not(:first-child)', {
name: text('[data-test-service-name]'), name: text('[data-test-service-name]'),
}), }),
}; };

View File

@ -0,0 +1,43 @@
import factory from 'consul-ui/filter/predicates/intention';
import { module, test } from 'qunit';
module('Unit | Filter | Predicates | intention', function() {
const predicate = factory();
test('it returns items depending on Action', function(assert) {
const items = [
{
Action: 'allow',
},
{
Action: 'deny',
},
];
let expected, actual;
expected = [items[0]];
actual = items.filter(
predicate({
accesses: ['allow'],
})
);
assert.deepEqual(actual, expected);
expected = [items[1]];
actual = items.filter(
predicate({
accesses: ['deny'],
})
);
assert.deepEqual(actual, expected);
expected = items;
actual = items.filter(
predicate({
accesses: ['allow', 'deny'],
})
);
assert.deepEqual(actual, expected);
});
});

View File

@ -0,0 +1,171 @@
import factory from 'consul-ui/filter/predicates/service';
import { module, test } from 'qunit';
module('Unit | Filter | Predicates | service', function() {
const predicate = factory();
test('it returns registered/unregistered items depending on instance count', function(assert) {
const items = [
{
InstanceCount: 1,
},
{
InstanceCount: 0,
},
];
let expected, actual;
expected = [items[0]];
actual = items.filter(
predicate({
instances: ['registered'],
})
);
assert.deepEqual(actual, expected);
expected = [items[1]];
actual = items.filter(
predicate({
instances: ['not-registered'],
})
);
assert.deepEqual(actual, expected);
expected = items;
actual = items.filter(
predicate({
instances: ['registered', 'not-registered'],
})
);
assert.deepEqual(actual, expected);
});
test('it returns items depending on status', function(assert) {
const items = [
{
MeshStatus: 'passing',
},
{
MeshStatus: 'warning',
},
{
MeshStatus: 'critical',
},
];
let expected, actual;
expected = [items[0]];
actual = items.filter(
predicate({
statuses: ['passing'],
})
);
assert.deepEqual(actual, expected);
expected = [items[1]];
actual = items.filter(
predicate({
statuses: ['warning'],
})
);
assert.deepEqual(actual, expected);
expected = items;
actual = items.filter(
predicate({
statuses: ['passing', 'warning', 'critical'],
})
);
assert.deepEqual(actual, expected);
});
test('it returns items depending on service type', function(assert) {
const items = [
{
Kind: 'ingress-gateway',
},
{
Kind: 'mesh-gateway',
},
{},
];
let expected, actual;
expected = [items[0]];
actual = items.filter(
predicate({
types: ['ingress-gateway'],
})
);
assert.deepEqual(actual, expected);
expected = [items[1]];
actual = items.filter(
predicate({
types: ['mesh-gateway'],
})
);
assert.deepEqual(actual, expected);
expected = items;
actual = items.filter(
predicate({
types: ['ingress-gateway', 'mesh-gateway', 'service'],
})
);
assert.deepEqual(actual, expected);
});
test('it returns items depending on a mixture of properties', function(assert) {
const items = [
{
Kind: 'ingress-gateway',
MeshStatus: 'passing',
InstanceCount: 1,
},
{
Kind: 'mesh-gateway',
MeshStatus: 'warning',
InstanceCount: 1,
},
{
MeshStatus: 'critical',
InstanceCount: 0,
},
];
let expected, actual;
expected = [items[0]];
actual = items.filter(
predicate({
types: ['ingress-gateway'],
statuses: ['passing'],
instances: ['registered'],
})
);
assert.deepEqual(actual, expected);
expected = [items[1]];
actual = items.filter(
predicate({
types: ['mesh-gateway'],
statuses: ['warning'],
instances: ['registered'],
})
);
assert.deepEqual(actual, expected);
expected = items;
actual = items.filter(
predicate({
types: ['ingress-gateway', 'mesh-gateway', 'service'],
statuses: ['passing', 'warning', 'critical'],
instances: ['registered', 'not-registered'],
})
);
assert.deepEqual(actual, expected);
});
});