ui: Add Optgroups and selectedItems to multiple select dropdown and use (#8476)

* ui: Switch selects to use more HTML-like approach for optgroups

* Add KV comparator

* Use new option/optgroup approach for sort/select

* Fix up tests for new order of menu items
This commit is contained in:
John Cowen 2020-08-11 18:02:51 +01:00 committed by GitHub
parent 7f711bb68f
commit a686de0414
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 636 additions and 320 deletions

View File

@ -27,7 +27,12 @@
</a> </a>
{{/let}} {{/let}}
{{else}} {{else}}
<button role="menuitem" tabindex="-1" type="button" onclick={{action this.onclick}}> <button
type="button"
role="menuitem"
aria-selected={{if selected 'true'}}
tabindex="-1"
onclick={{action (or this.onclick (noop))}}>
<YieldSlot @name="label"> <YieldSlot @name="label">
{{yield}} {{yield}}
</YieldSlot> </YieldSlot>

View File

@ -1,28 +1,31 @@
<div class="popover-select" ...attributes> <PopoverMenu @position={{or position "left"}} class="popover-select" ...attributes as |components menu|>
<PopoverMenu as |components menu|> {{yield}}
{{#let
(component 'popover-select/optgroup' components=components)
(component 'popover-select/option'
select=this components=components
onclick=(queue
(action "click")
(if multiple (noop) menu.toggle)
)
)
as |Optgroup Option|
}}
<BlockSlot @name="trigger"> <BlockSlot @name="trigger">
<span> <YieldSlot @name="selected">
{{selected.value}} {{yield (hash
</span> Optgroup=Optgroup
Option=Option
)}}
</YieldSlot>
</BlockSlot> </BlockSlot>
<BlockSlot @name="menu"> <BlockSlot @name="menu">
{{#let components.MenuItem components.MenuSeparator as |MenuItem MenuSeparator|}} <YieldSlot @name="options">
<MenuSeparator> {{yield (hash
<BlockSlot @name="label"> Optgroup=Optgroup
{{title}} Option=Option
</BlockSlot> )}}
</MenuSeparator> </YieldSlot>
{{#each options as |option|}}
<MenuItem
class={{if (eq selected.key option.key) 'is-active'}}
@onclick={{action (queue (action 'change' option) (if multiple (noop) menu.toggle))}}
>
<BlockSlot @name="label">
{{option.value}}
</BlockSlot>
</MenuItem>
{{/each}}
{{/let}}
</BlockSlot> </BlockSlot>
{{/let}}
</PopoverMenu> </PopoverMenu>
</div>

View File

@ -1,12 +1,53 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Slotted from 'block-slots';
export default Component.extend({ export default Component.extend(Slotted, {
tagName: '', tagName: '',
dom: service('dom'), dom: service('dom'),
multiple: false, multiple: false,
subtractive: true,
onchange: function() {}, onchange: function() {},
addOption: function(option) {
if (typeof this._options === 'undefined') {
this._options = new Set();
}
if (this.subtractive) {
if (!option.selected) {
this._options.add(option.value);
}
} else {
if (option.selected) {
this._options.add(option.value);
}
}
},
removeOption: function(option) {
this._options.delete(option.value);
},
actions: { actions: {
click: function(e, value) {
let options = [value];
if (this.multiple) {
if (this._options.has(value)) {
this._options.delete(value);
} else {
this._options.add(value);
}
options = this._options;
}
this.onchange(
this.dom.setEventTargetProperties(e, {
selected: target => value,
selectedItems: target => {
const opts = [...options];
if (opts.length > 0) {
return opts.join(',');
}
},
})
);
},
change: function(option, e) { change: function(option, e) {
this.onchange(this.dom.setEventTargetProperty(e, 'selected', selected => option)); this.onchange(this.dom.setEventTargetProperty(e, 'selected', selected => option));
}, },

View File

@ -0,0 +1,8 @@
{{#let components.MenuSeparator as |MenuSeparator|}}
<MenuSeparator>
<BlockSlot @name="label">
{{label}}
</BlockSlot>
</MenuSeparator>
{{yield}}
{{/let}}

View File

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

View File

@ -0,0 +1,11 @@
{{#let components.MenuItem as |MenuItem|}}
<MenuItem
class={{if selected 'is-active'}}
@onclick={{action 'click'}}
@selected={{selected}}
>
<BlockSlot @name="label">
{{yield}}
</BlockSlot>
</MenuItem>
{{/let}}

View File

@ -0,0 +1,20 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({
tagName: '',
dom: service('dom'),
didInsertElement: function() {
this._super(...arguments);
this.select.addOption(this);
},
willDestroyElement: function() {
this._super(...arguments);
this.select.removeOption(this);
},
actions: {
click: function(e) {
this.onclick(e, this.value);
},
},
});

View File

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

View File

@ -5,6 +5,7 @@ import { alias } from '@ember/object/computed';
export default Controller.extend({ export default Controller.extend({
items: alias('item.Nodes'), items: alias('item.Nodes'),
queryParams: { queryParams: {
sortBy: 'sort',
search: { search: {
as: 'filter', as: 'filter',
replace: true, replace: true,

View File

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

View File

@ -1,4 +1,5 @@
import service from 'consul-ui/sort/comparators/service'; import service from 'consul-ui/sort/comparators/service';
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';
import token from 'consul-ui/sort/comparators/token'; import token from 'consul-ui/sort/comparators/token';
@ -11,6 +12,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(),
kv: kv(),
check: check(), check: check(),
intention: intention(), intention: intention(),
token: token(), token: token(),

View File

@ -69,6 +69,24 @@ export default Service.extend({
}, },
}); });
}, },
setEventTargetProperties: function(e, propObj) {
const target = e.target;
return new Proxy(e, {
get: function(obj, prop, receiver) {
if (prop === 'target') {
return new Proxy(target, {
get: function(obj, prop, receiver) {
if (typeof propObj[prop] !== 'undefined') {
return propObj[prop](e.target);
}
return target[prop];
},
});
}
return Reflect.get(...arguments);
},
});
},
listeners: createListeners, listeners: createListeners,
root: function() { root: function() {
return this.doc.documentElement; return this.doc.documentElement;

View File

@ -0,0 +1,3 @@
export default () => key => {
return key;
};

View File

@ -5,7 +5,7 @@
border-bottom: $decor-border-200; border-bottom: $decor-border-200;
} }
%app-view-content h2, %app-view-content h2,
%app-view-content fieldset { %app-view-content form:not(.filter-bar) fieldset {
border-bottom: $decor-border-200; border-bottom: $decor-border-200;
} }
%app-view-content fieldset h2 { %app-view-content fieldset h2 {
@ -22,7 +22,7 @@
} }
%app-view-title, %app-view-title,
%app-view-content h2, %app-view-content h2,
%app-view-content fieldset { %app-view-content form:not(.filter-bar) fieldset {
border-color: $gray-200; border-color: $gray-200;
} }
// We know that any sibling navs might have a top border // We know that any sibling navs might have a top border

View File

@ -5,7 +5,6 @@ a.type-create {
// TODO: Once we move action-groups to use aria menu we can get rid of // TODO: Once we move action-groups to use aria menu we can get rid of
// some of this and just use not(aria-haspopup) // some of this and just use not(aria-haspopup)
button[type='reset'], button[type='reset'],
%app-view-content form button[type='button']:not([aria-haspopup='menu']),
header .actions button[type='button']:not(.copy-btn), header .actions button[type='button']:not(.copy-btn),
button.type-cancel, button.type-cancel,
html.template-error div > a { html.template-error div > a {

View File

@ -4,14 +4,7 @@
{{title 'Access Controls'}} {{title 'Access Controls'}}
{{/if}} {{/if}}
{{#let (selectable-key-values {{#let (or sortBy "Name:asc") as |sort|}}
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
selected=sortBy
)
as |sort|
}}
<AppView <AppView
@class="policy list" @class="policy list"
@loading={{isLoading}} @loading={{isLoading}}
@ -45,15 +38,41 @@
<SearchBar <SearchBar
@value={{search}} @value={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
@secondary="sort" class="with-sort"
@selected={{sort.selected}} >
@options={{sort.items}} <BlockSlot @name="secondary">
@onchange={{action (mut sortBy) value='target.selected.key'}} <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">
{{#let (sort-by (comparator 'policy' sort.selected.key) items) as |sorted|}} {{#let (sort-by (comparator 'policy' sort) items) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'policy' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'policy' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="set" as |filtered|>
<ConsulPolicyList <ConsulPolicyList

View File

@ -4,16 +4,7 @@
{{title 'Access Controls'}} {{title 'Access Controls'}}
{{/if}} {{/if}}
{{#let (selectable-key-values {{#let (or sortBy "Name:asc") as |sort|}}
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
(array "CreateIndex:asc" "Newest to oldest")
(array "CreateIndex:desc" "Oldest to newest")
selected=sortBy
)
as |sort|
}}
<AppView <AppView
@class="role list" @class="role list"
@loading={{isLoading}} @loading={{isLoading}}
@ -47,15 +38,47 @@
<SearchBar <SearchBar
@value={{search}} @value={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
@secondary="sort" class="with-sort"
@selected={{sort.selected}} >
@options={{sort.items}} <BlockSlot @name="secondary">
@onchange={{action (mut sortBy) value='target.selected.key'}} <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">
{{#let (sort-by (comparator 'role' sort.selected.key) items) as |sorted|}} {{#let (sort-by (comparator 'role' sort) items) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'role' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'role' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="set" as |filtered|>
<ConsulRoleList <ConsulRoleList

View File

@ -4,13 +4,7 @@
{{title 'Access Controls'}} {{title 'Access Controls'}}
{{/if}} {{/if}}
{{#let (selectable-key-values {{#let (or sortBy "CreateTime:desc") as |sort|}}
(array "CreateTime:desc" "Newest to oldest")
(array "CreateTime:asc" "Oldest to newest")
selected=sortBy
)
as |sort|
}}
<AppView <AppView
@class="token list" @class="token list"
@loading={{isLoading}} @loading={{isLoading}}
@ -41,22 +35,47 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<SearchBar <SearchBar
data-test-intention-filter="true" @value={{search}}
@value={{search}} @onsearch={{action (mut search) value="target.value"}}
@onsearch={{action (mut search) value="target.value"}} class="with-sort"
@secondary="sort" >
@selected={{sort.selected}} <BlockSlot @name="secondary">
@options={{sort.items}} <PopoverSelect
@onchange={{action (mut sortBy) value='target.selected.key'}} @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}}
</BlockSlot>
</PopoverSelect>
</BlockSlot>
</SearchBar>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="content">
{{#if (token/is-legacy items)}} {{#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> <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}} {{/if}}
{{#let (sort-by (comparator 'token' sort.selected.key) items) as |sorted|}} {{#let (sort-by (comparator 'token' sort) items) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'token' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'token' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="set" as |filtered|>
<ConsulTokenList <ConsulTokenList

View File

@ -6,21 +6,7 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="loaded"> <BlockSlot @name="loaded">
{{#let (or sortBy "Action:asc") as |sort|}}
{{#let (selectable-key-values
(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: Asc")
(array "Precedence:desc" "Precedence: Desc")
selected=sortBy
)
as |sort|
}}
<AppView @class="intention list"> <AppView @class="intention list">
<BlockSlot @name="header"> <BlockSlot @name="header">
<h1> <h1>
@ -34,18 +20,61 @@
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
{{#if (gt api.data.length 0) }} {{#if (gt api.data.length 0) }}
<SearchBar <SearchBar
data-test-intention-filter="true"
@value={{search}} @value={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
@secondary="sort" class="with-sort"
@selected={{sort.selected}} >
@options={{sort.items}} <BlockSlot @name="secondary">
@onchange={{action (mut sortBy) value='target.selected.key'}} <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}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="content">
{{#let (sort-by (comparator 'intention' sort.selected.key) api.data) as |sorted|}} {{#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 |filtered|>
<ConsulIntentionList <ConsulIntentionList

View File

@ -1,79 +1,118 @@
{{title 'Key/Value'}} {{title 'Key/Value'}}
<AppView @class="kv list"> {{#let (or sortBy "isFolder:asc") as |sort|}}
<BlockSlot @name="breadcrumbs"> <AppView @class="kv list">
<ol> <BlockSlot @name="breadcrumbs">
{{#if (not-eq parent.Key '/') }} <ol>
<li><a href={{href-to 'dc.kv'}}>Key / Values</a></li> {{#if (not-eq parent.Key '/') }}
{{/if}} <li><a href={{href-to 'dc.kv'}}>Key / Values</a></li>
{{#each (slice 0 -2 (split parent.Key '/')) as |breadcrumb index|}} {{/if}}
<li><a href={{href-to 'dc.kv.folder' (join '/' (append (slice 0 (add index 1) (split parent.Key '/')) ''))}}>{{breadcrumb}}</a></li> {{#each (slice 0 -2 (split parent.Key '/')) as |breadcrumb index|}}
{{/each}} <li><a href={{href-to 'dc.kv.folder' (join '/' (append (slice 0 (add index 1) (split parent.Key '/')) ''))}}>{{breadcrumb}}</a></li>
</ol> {{/each}}
</BlockSlot> </ol>
<BlockSlot @name="header"> </BlockSlot>
<h1> <BlockSlot @name="header">
{{#if (eq parent.Key '/')}} <h1>
Key / Value {{#if (eq parent.Key '/')}}
{{else}} Key / Value
{{take 1 (drop 1 (reverse (split parent.Key '/')))}} {{else}}
{{/if}} {{take 1 (drop 1 (reverse (split parent.Key '/')))}}
</h1> {{/if}}
<label for="toolbar-toggle"></label> </h1>
</BlockSlot> <label for="toolbar-toggle"></label>
<BlockSlot @name="toolbar"> </BlockSlot>
{{#if (gt items.length 0) }} <BlockSlot @name="toolbar">
<SearchBar {{#if (gt items.length 0) }}
@placeholder="Search by name" <SearchBar
@value={{search}} @value={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
/> class="with-sort"
{{/if}} >
</BlockSlot> <BlockSlot @name="secondary">
<BlockSlot @name="actions"> <PopoverSelect
{{#if (not-eq parent.Key '/') }} @position="right"
<a data-test-create href="{{href-to 'dc.kv.create' parent.Key}}" class="type-create">Create</a> @onchange={{action (mut sortBy) value='target.selected'}}
{{else}} @multiple={{false}}
<a data-test-create href="{{href-to 'dc.kv.root-create'}}" class="type-create">Create</a> as |components|>
{{/if}} <BlockSlot @name="selected">
</BlockSlot> <span>
<BlockSlot @name="content"> {{#let (from-entries (array
<ChangeableSet @dispatcher={{searchable 'kv' items}} @terms={{search}}> (array "Key:asc" "A to Z")
<BlockSlot @name="content" as |filtered|> (array "Key:desc" "Z to A")
<ConsulKvList (array "isFolder:desc" "Folders to Keys")
@items={{sort-by "isFolder:desc" "Key:asc" filtered}} (array "isFolder:asc" "Keys to Folders")
@parent={{parent}} ))
@ondelete={{refresh-route}} as |selectable|
> }}
<EmptyState @allowLogin={{true}}> {{get selectable sort}}
<BlockSlot @name="header"> {{/let}}
<h2> </span>
{{#if (gt items.length 0)}}
No K/V pairs found
{{else}}
Welcome to Key/Value
{{/if}}
</h2>
</BlockSlot> </BlockSlot>
<BlockSlot @name="body"> <BlockSlot @name="options">
<p> {{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#if (gt items.length 0)}} <Optgroup @label="Name">
No K/V pairs where found matching that search, or you may not have access to view the K/V pairs you are searching for. <Option @value="Key:asc" @selected={{eq "Key:asc" sort}}>A to Z</Option>
{{else}} <Option @value="Key:desc" @selected={{eq "Key:desc" sort}}>Z to A</Option>
You don't have any K/V pairs, or you may not have access to view K/V pairs yet. </Optgroup>
{{/if}} <Optgroup @label="Type">
</p> <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> </BlockSlot>
<BlockSlot @name="actions"> </PopoverSelect>
<li class="docs-link"> </BlockSlot>
<a href="{{env 'CONSUL_DOCS_URL'}}/agent/kv" rel="noopener noreferrer" target="_blank">Documentation on K/V</a> </SearchBar>
</li> {{/if}}
<li class="learn-link"> </BlockSlot>
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/getting-started/kv" rel="noopener noreferrer" target="_blank">Read the guide</a> <BlockSlot @name="actions">
</li> {{#if (not-eq parent.Key '/') }}
</BlockSlot> <a data-test-create href="{{href-to 'dc.kv.create' parent.Key}}" class="type-create">Create</a>
</EmptyState> {{else}}
</ConsulKvList> <a data-test-create href="{{href-to 'dc.kv.root-create'}}" class="type-create">Create</a>
</BlockSlot> {{/if}}
</ChangeableSet> </BlockSlot>
</BlockSlot> <BlockSlot @name="content">
</AppView> {{#let (sort-by (comparator 'kv' sort) items) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'kv' sorted}} @terms={{search}}>
<BlockSlot @name="content" as |filtered|>
<ConsulKvList
@items={{filtered}}
@parent={{parent}}
@ondelete={{refresh-route}}
>
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>
{{#if (gt items.length 0)}}
No K/V pairs found
{{else}}
Welcome to Key/Value
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
{{#if (gt items.length 0)}}
No K/V pairs where found matching that search, or you may not have access to view the K/V pairs you are searching for.
{{else}}
You don't have any K/V pairs, or you may not have access to view K/V pairs yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}/agent/kv" rel="noopener noreferrer" target="_blank">Documentation on K/V</a>
</li>
<li class="learn-link">
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/getting-started/kv" rel="noopener noreferrer" target="_blank">Read the guide</a>
</li>
</BlockSlot>
</EmptyState>
</ConsulKvList>
</BlockSlot>
</ChangeableSet>
{{/let}}
</BlockSlot>
</AppView>
{{/let}}

View File

@ -1,11 +1,5 @@
{{title 'Namespaces'}} {{title 'Namespaces'}}
{{#let (selectable-key-values {{#let (or sortBy "Name:asc") as |sort|}}
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
selected=sortBy
)
as |sort|
}}
<EventSource @src={{items}} /> <EventSource @src={{items}} />
<AppView @class="nspace list" @loading={{isLoading}}> <AppView @class="nspace list" @loading={{isLoading}}>
<BlockSlot @name="notification" as |status type subject|> <BlockSlot @name="notification" as |status type subject|>
@ -21,18 +15,44 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<SearchBar <SearchBar
@value={{search}} @value={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
@secondary="sort" class="with-sort"
@selected={{sort.selected}} >
@options={{sort.items}} <BlockSlot @name="secondary">
@onchange={{action (mut sortBy) value='target.selected.key'}} <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">
{{#let (sort-by (comparator 'nspace' sort.selected.key) items) as |sorted|}} {{#let (sort-by (comparator 'nspace' sort) items) as |sorted|}}
<ChangeableSet @dispatcher={{searchable 'nspace' sorted}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'nspace' sorted}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="set" as |filtered|>
<ConsulNspaceList <ConsulNspaceList

View File

@ -1,14 +1,6 @@
{{title 'Services'}} {{title 'Services'}}
<EventSource @src={{items}} /> <EventSource @src={{items}} />
{{#let (selectable-key-values {{#let (or sortBy "Name:asc") as |sort|}}
(array "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
(array "Status:asc" "Unhealthy to Healthy")
(array "Status:desc" "Healthy to Unhealthy")
selected=sortBy
)
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'}}
@ -24,15 +16,47 @@
<SearchBar <SearchBar
@value={{search}} @value={{search}}
@onsearch={{action (mut search) value="target.value"}} @onsearch={{action (mut search) value="target.value"}}
@secondary="sort" class="with-sort"
@selected={{sort.selected}} >
@options={{sort.items}} <BlockSlot @name="secondary">
@onchange={{action (mut sortBy) value='target.selected.key'}} <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>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="content">
{{#let (sort-by (comparator 'service' sort.selected.key) services) as |sorted|}} {{#let (sort-by (comparator 'service' sort) services) 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 |filtered|>
<ConsulServiceList @items={{filtered}} @proxies={{proxies}}/> <ConsulServiceList @items={{filtered}} @proxies={{proxies}}/>

View File

@ -3,38 +3,68 @@
<ErrorState @error={{api.error}} /> <ErrorState @error={{api.error}} />
</BlockSlot> </BlockSlot>
<BlockSlot @name="loaded"> <BlockSlot @name="loaded">
{{#let (filter-by "Action" "deny" api.data) as |denied|}} {{#let (or sortBy "Action:asc") as |sort|}}
{{#let (selectable-key-values
(array "" (concat "All (" api.data.length ")"))
(array "allow" (concat "Allow (" (sub api.data.length denied.length) ")"))
(array "deny" (concat "Deny (" denied.length ")"))
selected=filterBy
)
as |filter|
}}
<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 api.data.length 0) }}
<input type="checkbox" id="toolbar-toggle" /> <SearchBar
<SearchBar @value={{search}}
@value={{search}} @onsearch={{action (mut search) value="target.value"}}
@onsearch={{action (mut search) value="target.value"}} class="with-sort"
@selected={{filter.selected}} >
@options={{filter.items}} <BlockSlot @name="secondary">
@onchange={{action (mut filterBy) value='target.value'}} <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}} {{/if}}
<ChangeableSet {{#let (sort-by (comparator 'intention' sort) api.data) as |sorted|}}
@dispatcher={{ <ChangeableSet @dispatcher={{searchable 'intention' sorted}} @terms={{search}}>
searchable
'intention'
(if (eq filter.selected.key "") api.data (filter-by "Action" filter.selected.key api.data))
}}
@terms={{search}}
>
<BlockSlot @name="content" as |filtered|> <BlockSlot @name="content" as |filtered|>
<ConsulIntentionList <ConsulIntentionList
@items={{filtered}} @items={{filtered}}
@ -51,9 +81,9 @@
</ConsulIntentionList> </ConsulIntentionList>
</BlockSlot> </BlockSlot>
</ChangeableSet> </ChangeableSet>
{{/let}}
</div> </div>
</div> </div>
{{/let}} {{/let}}
{{/let}}
</BlockSlot> </BlockSlot>
</DataLoader> </DataLoader>

View File

@ -1,57 +1,57 @@
@setupApplicationTest @setupApplicationTest
Feature: dc / acls / roles / sorting Feature: dc / acls / roles / sorting
Scenario: Sorting Roles Scenario: Sorting Roles
Given 1 datacenter model with the value "dc-1" Given 1 datacenter model with the value "dc-1"
And 4 role models from yaml And 4 role models from yaml
--- ---
- Name: "system-A" - Name: "system-A"
CreateIndex: 3 CreateIndex: 3
- Name: "system-D" - Name: "system-D"
CreateIndex: 2 CreateIndex: 2
- Name: "system-C" - Name: "system-C"
CreateIndex: 1 CreateIndex: 1
- Name: "system-B" - Name: "system-B"
CreateIndex: 4 CreateIndex: 4
--- ---
When I visit the roles page for yaml When I visit the roles page for yaml
--- ---
dc: dc-1 dc: dc-1
--- ---
Then the url should be /dc-1/acls/roles Then the url should be /dc-1/acls/roles
Then I see 4 role models Then I see 4 role 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 roles vertically like yaml Then I see name on the roles 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 roles vertically like yaml Then I see name on the roles vertically like yaml
--- ---
- "system-A" - "system-A"
- "system-B" - "system-B"
- "system-C" - "system-C"
- "system-D" - "system-D"
--- ---
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.3.button on the sort
Then I see name on the roles vertically like yaml Then I see name on the roles vertically like yaml
--- ---
- "system-C" - "system-C"
- "system-D" - "system-D"
- "system-A" - "system-A"
- "system-B" - "system-B"
--- ---
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.2.button on the sort
Then I see name on the roles vertically like yaml Then I see name on the roles vertically like yaml
--- ---
- "system-B" - "system-B"
- "system-A" - "system-A"
- "system-D" - "system-D"
- "system-C" - "system-C"
--- ---

View File

@ -1,5 +1,5 @@
@setupApplicationTest @setupApplicationTest
Feature: dc / services / intentions: Intentions per service Feature: dc / services / show / intentions: Intentions per service
Background: Background:
Given 1 datacenter model with the value "dc1" Given 1 datacenter model with the value "dc1"
And 1 node models And 1 node models
@ -26,6 +26,6 @@ Feature: dc / services / intentions: Intentions per service
And I click actions on the intentions And I click actions on the intentions
And I click delete on the intentions And I click delete on the intentions
And I click confirmDelete on the intentions And I click confirmDelete on the intentions
Then a DELETE request was made to "/v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc1" Then a DELETE request was made to "/v1/connect/intentions/755b72bd-f5ab-4c92-90cc-bed0e7d8e9f0?dc=dc1"
And "[data-notification]" has the "notification-delete" class And "[data-notification]" has the "notification-delete" class
And "[data-notification]" has the "success" class And "[data-notification]" has the "success" class

View File

@ -40,7 +40,7 @@ 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.1.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-F - Service-F
@ -51,20 +51,20 @@ Feature: dc / services / sorting
- Service-A - Service-A
--- ---
When I click selected on the sort When I click selected on the sort
When I click options.0.button on the sort
Then I see name on the services vertically like yaml
---
- Service-A
- Service-B
- Service-C
- Service-D
- Service-E
- Service-F
---
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
Then I see name on the services vertically like yaml Then I see name on the services vertically like yaml
--- ---
- Service-A
- Service-B
- Service-C
- Service-D
- Service-E
- Service-F
---
When I click selected on the sort
When I click options.0.button on the sort
Then I see name on the services vertically like yaml
---
- Service-B - Service-B
- Service-C - Service-C
- Service-A - Service-A
@ -73,7 +73,7 @@ Feature: dc / services / sorting
- Service-E - Service-E
--- ---
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.1.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-E - Service-E