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: '',
dom: service('dom'),
multiple: false,
subtractive: true,
subtractive: false,
onchange: function() {},
addOption: function(option) {
if (typeof this._options === 'undefined') {

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,9 @@ import { computed } from '@ember/object';
export default Controller.extend({
queryParams: {
sortBy: 'sort',
status: 'status',
source: 'source',
type: 'type',
search: {
as: 'filter',
},
@ -13,4 +16,11 @@ export default Controller.extend({
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 { computed } from '@ember/object';
export default Controller.extend({
queryParams: {
sortBy: 'sort',
status: 'status',
source: 'source',
search: {
as: 'filter',
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 { get } from '@ember/object';
const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001';
import { MANAGEMENT_ID } from 'consul-ui/models/policy';
export default helper(function policyGroup([items] /*, hash*/) {
return items.reduce(

View File

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

View File

@ -1,9 +1,12 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { computed } from '@ember/object';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'ID';
export const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001';
export default Model.extend({
[PRIMARY_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
CreateTime: attr('date', { defaultValue: 0 }),
//
isGlobalManagement: computed('ID', function() {
return this.ID === MANAGEMENT_ID;
}),
Datacenter: attr('string'),
Namespace: attr('string'),
SyncTime: attr('number'),

View File

@ -1,7 +1,8 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
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 SLUG_KEY = 'Node.Node,Service.ID';
@ -19,13 +20,52 @@ export default Model.extend({
Checks: attr(),
SyncTime: attr('number'),
meta: attr(),
Name: or('Service.ID', 'Service.Service'),
Tags: alias('Service.Tags'),
Meta: alias('Service.Meta'),
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 !== '';
}),
NodeChecks: filter('Checks', function(item, i, arr) {
NodeChecks: filter('Checks.[]', function(item, i, arr) {
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 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 SLUG_KEY = 'AccessorID';
@ -24,6 +26,9 @@ export default Model.extend({
Datacenter: attr('string'),
Namespace: attr('string'),
Local: attr('boolean'),
isGlobalManagement: computed('Policies.[]', function() {
return (this.Policies || []).find(item => item.ID === MANAGEMENT_ID);
}),
Policies: attr({
defaultValue: function() {
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 = {
dc: dc,
nspace: nspace,
filter: `SourceName == "${slug}" or DestinationName == "${slug}"`,
filter: `SourceName == "${slug}" or DestinationName == "${slug}" or SourceName == "*" or DestinationName == "*"`,
};
if (typeof configuration.cursor !== 'undefined') {
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 './layout';
%sort-button {
@extend %split-button;
}

View File

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

View File

@ -948,6 +948,16 @@
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 {
@extend %with-icon;
background-image: $logo-gcp-color-svg;
@ -1047,6 +1057,15 @@
-webkit-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 {
@extend %with-icon;
@ -1098,6 +1117,15 @@
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 {
@extend %with-icon;
background-image: $logo-vmware-color-svg;

View File

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

View File

@ -3,14 +3,18 @@
.filter-bar {
@extend %filter-bar;
}
%filter-bar {
%filter-bar .popover-select {
height: 35px;
position: relative;
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;
}
%filter-bar [role='radiogroup'] {
html.template-acl.template-list .filter-bar [role='radiogroup'] {
@extend %expanded-single-select;
}
%filter-bar span::before {
@ -19,6 +23,11 @@
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 {
@extend %with-check-circle-fill-icon, %as-pseudo;
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,64 @@
.popover-select {
@extend %popover-select;
}
%popover-select label {
height: 100%;
}
%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;
}
%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}}
{{title 'Access Controls'}}
{{/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|}}
<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) }}
<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'}}
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
@filter={{filters}}
@onfilter={{hash
dc=(action (mut dc) value="target.selectedItems")
type=(action (mut type) value="target.selectedItems")
}}
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>
{{#if (gt items.length 0)}}
No policies found
{{else}}
Welcome to Policies
{{/if}}
</h2>
<BlockSlot @name="content">
{{#let (filter (filter-predicate 'policy' filters) items) as |filtered|}}
{{#let (sort-by (comparator 'policy' sort) filtered) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'policy' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |searched|>
<ConsulPolicyList
@items={{searched}}
@ondelete={{action send 'delete'}}
/>
</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 @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>
{{#if (gt items.length 0)}}
No policies found
{{else}}
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 @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>
</ChangeableSet>
</ChangeableSet>
{{/let}}
{{/let}}
</BlockSlot>
</AppView>
{{/let}}
</BlockSlot>
</AppView>
{{/let}}

View File

@ -35,46 +35,13 @@
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0) }}
<SearchBar
@value={{search}}
<ConsulRoleSearchBar
@search={{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")
(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>
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">

View File

@ -4,113 +4,97 @@
{{title 'Access Controls'}}
{{/if}}
{{#let (or sortBy "CreateTime:desc") as |sort|}}
<AppView
@class="token list"
@loading={{isLoading}}
@authorized={{isAuthorized}}
@enabled={{isEnabled}}
>
<BlockSlot @name="notification" as |status type subject|>
{{partial 'dc/acls/tokens/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.tokens.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 "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 (hash
types=(if type (split type ',') undefined)
) as |filters|}}
{{#let (or sortBy "CreateTime:desc") as |sort|}}
<AppView
@class="token list"
@loading={{isLoading}}
@authorized={{isAuthorized}}
@enabled={{isEnabled}}
>
<BlockSlot @name="notification" as |status type subject|>
{{partial 'dc/acls/tokens/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.tokens.create'}}" class="type-create">Create</a>
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0)}}
<ConsulTokenSearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
@filter={{filters}}
@onfilter={{hash
type=(action (mut type) value="target.selectedItems")
}}
/>
{{/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 (filter (filter-predicate 'token' filters) items) as |filtered|}}
{{#let (sort-by (comparator 'token' sort) filtered) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'token' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |searched|>
<ConsulTokenList
@items={{searched}}
@token={{token}}
@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}}
</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 @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>
</AppView>
{{/let}}
</BlockSlot>
</AppView>
{{/let}}

View File

@ -6,11 +6,15 @@
</BlockSlot>
<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|}}
<AppView @class="intention list">
<BlockSlot @name="header">
<h1>
Intentions <em>{{format-number api.data.length}} total</em>
Intentions <em>{{format-number items.length}} total</em>
</h1>
<label for="toolbar-toggle"></label>
</BlockSlot>
@ -18,73 +22,34 @@
<a data-test-create href="{{href-to 'dc.intentions.create'}}" class="type-create">Create</a>
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt api.data.length 0) }}
<SearchBar
@value={{search}}
{{#if (gt items.length 0) }}
<ConsulIntentionSearchBar
@search={{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 "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}}
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
@filter={{filters}}
@onfilter={{hash
access=(action (mut access) value="target.selectedItems")
}}
/>
{{/if}}
</BlockSlot>
<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}}>
<BlockSlot @name="content" as |filtered|>
<BlockSlot @name="content" as |searched|>
<ConsulIntentionList
@items={{filtered}}
@items={{searched}}
@ondelete={{refresh-route}}
>
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>
{{#if (gt api.data.length 0)}}
{{#if (gt items.length 0)}}
No intentions found
{{else}}
Welcome to Intentions
@ -93,7 +58,7 @@
</BlockSlot>
<BlockSlot @name="body">
<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.
{{else}}
There don't seem to be any intentions, or you may not have access to view intentions yet.
@ -112,9 +77,12 @@
</ConsulIntentionList>
</BlockSlot>
</ChangeableSet>
{{/let}}
{{/let}}
{{/let}}
</BlockSlot>
</AppView>
{{/let}}
{{/let}}
{{/let}}
{{/let}}
</BlockSlot>
</DataLoader>

View File

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

View File

@ -1,5 +1,8 @@
{{title 'Nodes'}}
<EventSource @src={{items}} />
{{#let (hash
statuses=(if status (split status ',') undefined)
) as |filters|}}
{{#let (or sortBy "Node:asc") as |sort|}}
<AppView @class="node list">
<BlockSlot @name="header">
@ -10,53 +13,26 @@
</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 "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>
<ConsulNodeSearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
@filter={{filters}}
@onfilter={{hash
status=(action (mut status) value="target.selectedItems")
}}
/>
{{/if}}
</BlockSlot>
<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}}>
<BlockSlot @name="set" as |filtered|>
<ConsulNodeList @items={{filtered}} @leader={{leader}} />
<BlockSlot @name="set" as |searched|>
<ConsulNodeList @items={{searched}} @leader={{leader}} />
</BlockSlot>
<BlockSlot @name="empty">
<EmptyState>
@ -68,7 +44,9 @@
</EmptyState>
</BlockSlot>
</ChangeableSet>
{{/let}}
{{/let}}
{{/let}}
</BlockSlot>
</AppView>
{{/let}}
{{/let}}

View File

@ -15,40 +15,14 @@
</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>
<ConsulNspaceSearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">

View File

@ -1,65 +1,46 @@
{{title 'Services'}}
<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">
<BlockSlot @name="notification" as |status type|>
{{partial 'dc/services/notifications'}}
</BlockSlot>
<BlockSlot @name="header">
<h1>
Services <em>{{format-number services.length}} total</em>
Services <em>{{format-number services.length}} total</em>
</h1>
<label for="toolbar-toggle"></label>
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt services.length 0) }}
<SearchBar
@value={{search}}
<ConsulServiceSearchBar
@sources={{externalSources}}
@search={{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")
(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>
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
@filter={{filters}}
@onfilter={{hash
status=(action (mut status) value="target.selectedItems")
type=(action (mut type) value="target.selectedItems")
source=(action (mut source) value="target.selectedItems")
}}
/>
{{/if}}
</BlockSlot>
<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}}>
<BlockSlot @name="set" as |filtered|>
<ConsulServiceList @items={{filtered}}/>
<BlockSlot @name="set" as |searched|>
<ConsulServiceList @items={{searched}} />
</BlockSlot>
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
@ -92,7 +73,9 @@
</EmptyState>
</BlockSlot>
</ChangeableSet>
{{/let}}
{{/let}}
{{/let}}
</BlockSlot>
</AppView>
{{/let}}
{{/let}}

View File

@ -1,15 +1,32 @@
<div id="instances" class="tab-section">
<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" />
<SearchBar
@value={{search}}
<ConsulServiceInstanceSearchBar
@sources={{externalSources}}
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
/>
{{/if}}
<ChangeableSet @dispatcher={{searchable 'serviceInstance' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<ConsulServiceInstanceList @routeName="dc.services.instance" @items={{filtered}}/>
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
@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 @name="empty">
<EmptyState>
@ -21,5 +38,9 @@
</EmptyState>
</BlockSlot>
</ChangeableSet>
{{/let}}
{{/let}}
{{/let}}
{{/let}}
</div>
</div>

View File

@ -3,87 +3,55 @@
<ErrorState @error={{api.error}} />
</BlockSlot>
<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|}}
<div id="intentions" class="tab-section">
<div role="tabpanel">
<Portal @target="app-view-actions">
<a data-test-create href={{href-to 'dc.services.show.intentions.create'}} class="type-create">Create</a>
</Portal>
{{#if (gt api.data.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 "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}}
{{#let (sort-by (comparator 'intention' sort) api.data) as |sorted|}}
{{#if (gt items.length 0) }}
<ConsulIntentionSearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
@filter={{filters}}
@onfilter={{hash
access=(action (mut access) value="target.selectedItems")
}}
/>
{{/if}}
{{#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}}>
<BlockSlot @name="content" as |filtered|>
<BlockSlot @name="content" as |searched|>
<ConsulIntentionList
@items={{filtered}}
@items={{searched}}
@ondelete={{refresh-route}}
@routeName="dc.services.show.intentions.edit"
>
<EmptyState>
<BlockSlot @name="body">
<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>
</BlockSlot>
</EmptyState>
</ConsulIntentionList>
</BlockSlot>
</ChangeableSet>
{{/let}}
{{/let}}
{{/let}}
</div>
</div>
{{/let}}
{{/let}}
{{/let}}
{{/let}}
</BlockSlot>
</DataLoader>

View File

@ -1,22 +1,50 @@
<div id="services" class="tab-section">
<div role="tabpanel">
{{#if (gt gatewayServices.length 0)}}
<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>
<ConsulServiceList
@items={{gatewayServices}}
@nspace={{nspace}}
/>
{{else}}
<EmptyState>
<BlockSlot @name="body">
<p>
There are no linked services.
</p>
</BlockSlot>
</EmptyState>
{{/if}}
{{#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>
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>

View File

@ -1,18 +1,49 @@
<div id="upstreams" class="tab-section">
<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>
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>
<ConsulUpstreamList @items={{gatewayServices}} @dc={{dc}} @nspace={{nspace}} />
{{else}}
<EmptyState>
<BlockSlot @name="body">
<p>
There are no upstreams.
</p>
</BlockSlot>
</EmptyState>
{{/if}}
{{#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 upstreams.
</p>
</BlockSlot>
</EmptyState>
</BlockSlot>
</ChangeableSet>
{{/let}}
{{/let}}
{{/let}}
{{/let}}
</div>
</div>

View File

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

View File

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

View File

@ -28,6 +28,7 @@ Feature: dc / services / show / upstreams
---
And the title should be "ingress-gateway-1 - Consul"
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
Scenario: Don't see the Upstreams tab
Given 1 datacenter model with the value "dc1"

View File

@ -40,15 +40,28 @@ Feature: dc / services / sorting
dc: dc-1
---
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
---
- Service-B
- Service-C
- Service-A
- Service-D
- Service-F
- 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-A
- Service-C
- Service-B
- Service-A
---
When I click selected on the sort
When I click options.2.button on the sort
@ -62,24 +75,13 @@ Feature: dc / services / sorting
- Service-F
---
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
---
- Service-B
- Service-C
- Service-A
- Service-D
- Service-F
- 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-A
- Service-C
- Service-B
- Service-A
---

View File

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

View File

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

View File

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

View File

@ -8,6 +8,6 @@ export default function(visitable, text, clickable, attribute, collection, popov
visit: visitable('/:dc/nodes'),
nodes: collection('.consul-node-list [data-test-list-row]', node),
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({
visit: visitable('/:dc/namespaces'),
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'),
services: collection('.consul-service-list > ul > li:not(:first-child)', service),
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(),
};
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]'),
}),
};

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);
});
});