ui: Redesigns for the token/policy/roles listings pages (#8144)

This commit is contained in:
John Cowen 2020-06-23 10:12:04 +01:00 committed by GitHub
parent c5d0216939
commit ed8d148502
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 665 additions and 470 deletions

View File

@ -0,0 +1,3 @@
<li class="dangerous">
<button tabindex="-1" type="button" onclick={{action onclick}}>{{yield}}</button>
</li>

View File

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

View File

@ -0,0 +1,22 @@
{{yield}}
<div class="confirmation-alert" ...attributes>
<div>
<header>
<YieldSlot @name="header">{{yield}}</YieldSlot>
</header>
<YieldSlot @name="body">{{yield}}</YieldSlot>
</div>
<ul>
<YieldSlot @name="confirm" @params={{
block-params (component 'confirmation-alert/action'
onclick=(action onclick)
)
}}
>
{{yield}}
</YieldSlot>
<li>
<label for={{name}}>Cancel</label>
</li>
</ul>
</div>

View File

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

View File

@ -0,0 +1,24 @@
## ConsulPolicyList
```
<ConsulPolicyList
@items={{items}}
@ondelete={{action 'delete'}}
/>
```
A presentational component for rendering Consul ACL policies
### Arguments
| Argument/Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `array` | | An array of ACL policies |
| `ondelete` | `function` | | An action to execute when the `Delete` action is clicked |
### See
- [Component Source Code](./index.js)
- [TemplateSource Code](./index.hbs)
---

View File

@ -0,0 +1,67 @@
{{#if (gt items.length 0)}}
<ListCollection @items={{items}} class="consul-policy-list" as |item|>
<BlockSlot @name="header">
{{#if (eq (policy/typeof item) 'policy-management')}}
<dl class="policy-management">
<dd>
<Tooltip @position="top-start">
Global Management Policy
</Tooltip>
</dd>
</dl>
{{/if}}
<a data-test-policy={{item.Name}} href={{href-to 'dc.acls.policies.edit' item.ID}} class={{if (eq (policy/typeof item) 'policy-management') 'is-management'}}>{{item.Name}}</a>
</BlockSlot>
<BlockSlot @name="details">
<dl class="datacenter">
<dt>
<Tooltip @position="top-start">Datacenters</Tooltip>
</dt>
<dd>
{{join ', ' (policy/datacenters item)}}
</dd>
</dl>
<dl class="description">
<dt>Description</dt>
<dd data-test-description>
{{item.Description}}
</dd>
</dl>
</BlockSlot>
<BlockSlot @name="actions" as |Actions|>
<Actions as |Action|>
<Action data-test-edit-action @href={{href-to 'dc.acls.policies.edit' item.ID}}>
<BlockSlot @name="label">
{{#if (eq (policy/typeof item) 'policy-management')}}
View
{{else}}
Edit
{{/if}}
</BlockSlot>
</Action>
{{#if (not-eq (policy/typeof item) 'policy-management')}}
<Action data-test-delete-action @onclick={{action ondelete item}} class="dangerous">
<BlockSlot @name="label">
Delete
</BlockSlot>
<BlockSlot @name="confirmation" as |Confirmation|>
<Confirmation class="warning">
<BlockSlot @name="header">
Confirm delete
</BlockSlot>
<BlockSlot @name="body">
<p>
Are you sure you want to delete this policy?
</p>
</BlockSlot>
<BlockSlot @name="confirm" as |Confirm|>
<Confirm>Delete</Confirm>
</BlockSlot>
</Confirmation>
</BlockSlot>
</Action>
{{/if}}
</Actions>
</BlockSlot>
</ListCollection>
{{/if}}

View File

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

View File

@ -0,0 +1,8 @@
export default (collection, clickable, attribute, text, actions) => () => {
return collection('.consul-policy-list li:not(:first-child)', {
name: attribute('data-test-policy', '[data-test-policy]'),
description: text('[data-test-description]'),
policy: clickable('a'),
...actions(['edit', 'delete']),
});
};

View File

@ -0,0 +1,24 @@
## ConsulRoleList
```
<ConsulRoleList
@items={{items}}
@ondelete={{action 'delete'}}
/>
```
A presentational component for rendering Consul ACL roles
### Arguments
| Argument/Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `array` | | An array of ACL roles |
| `ondelete` | `function` | | An action to execute when the `Delete` action is clicked |
### See
- [Component Source Code](./index.js)
- [TemplateSource Code](./index.hbs)
---

View File

@ -0,0 +1,45 @@
{{#if (gt items.length 0)}}
<ListCollection @items={{items}} class="consul-role-list" as |item|>
<BlockSlot @name="header">
<a data-test-role={{item.Name}} href={{href-to 'dc.acls.roles.edit' item.ID}}>{{item.Name}}</a>
</BlockSlot>
<BlockSlot @name="details">
<ConsulTokenRulesetList @item={{item}} />
<dl>
<dt>Description</dt>
<dd data-test-description>
{{item.Description}}
</dd>
</dl>
</BlockSlot>
<BlockSlot @name="actions" as |Actions|>
<Actions as |Action|>
<Action data-test-edit-action @href={{href-to 'dc.acls.roles.edit' item.ID}}>
<BlockSlot @name="label">
Edit
</BlockSlot>
</Action>
<Action data-test-delete-action @onclick={{action ondelete item}} class="dangerous">
<BlockSlot @name="label">
Delete
</BlockSlot>
<BlockSlot @name="confirmation" as |Confirmation|>
<Confirmation class="warning">
<BlockSlot @name="header">
Confirm delete
</BlockSlot>
<BlockSlot @name="body">
<p>
Are you sure you want to delete this role?
</p>
</BlockSlot>
<BlockSlot @name="confirm" as |Confirm|>
<Confirm>Delete</Confirm>
</BlockSlot>
</Confirmation>
</BlockSlot>
</Action>
</Actions>
</BlockSlot>
</ListCollection>
{{/if}}

View File

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

View File

@ -0,0 +1,9 @@
export default (collection, clickable, attribute, text, actions) => () => {
return collection('.consul-role-list li:not(:first-child)', {
name: attribute('data-test-role', '[data-test-role]'),
description: text('[data-test-description]'),
policy: text('[data-test-policy].policy', { multiple: true }),
serviceIdentity: text('[data-test-policy].policy-service-identity', { multiple: true }),
...actions(['edit', 'delete']),
});
};

View File

@ -1,10 +1,10 @@
{{#if (gt items.length 0)}}
<ListCollection @items={{items}} class="consul-token-list" as |item index checked change|>
<ListCollection @items={{items}} class="consul-token-list" as |item|>
<BlockSlot @name="header">
{{#if (eq item.AccessorID token.AccessorID)}}
<dl rel="me">
<dd>
<Tooltip>
<Tooltip @position="top-start">
Your token
</Tooltip>
</dd>
@ -19,50 +19,7 @@
{{if item.Local 'local' 'global' }}
</dd>
</dl>
{{#let (policy/group item.Policies) as |policies|}}
{{#let (get policies 'management') as |management|}}
{{#if (gt management.length 0)}}
<dl>
<dt>
Management
</dt>
<dd>
{{#each (get policies 'management') as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{item.Name}}</span>
{{/each}}
</dd>
</dl>
{{/if}}
{{/let}}
{{#let (get policies 'identities') as |identities|}}
{{#if (gt identities.length 0)}}
<dl>
<dt>Identities</dt>
<dd>
{{#each identities as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{if (eq item.template 'service-identity') 'Service' 'Node'}} Identity: {{item.Name}}</span>
{{/each}}
</dd>
</dl>
{{/if}}
{{/let}}
{{#let (append (get policies 'policies') item.Roles) as |policies|}}
{{#if (gt policies.length 0)}}
<dl>
<dt>Rules</dt>
<dd>
{{#if (token/is-legacy item) }}
Legacy tokens have embedded rules.
{{ else }}
{{#each policies as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{item.Name}}</span>
{{/each}}
{{/if}}
</dd>
</dl>
{{/if}}
{{/let}}
{{/let}}
<ConsulTokenRulesetList @item={{item}} />
<dl>
<dt>Description</dt>
<dd data-test-description>
@ -70,98 +27,86 @@
</dd>
</dl>
</BlockSlot>
<BlockSlot @name="actions">
<div class="more-popover-menu">
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}} @submenus={{array "logout" "use" "delete"}}>
<BlockSlot @name="trigger">
More
</BlockSlot>
<BlockSlot @name="menu" as |confirm send keypressClick|>
<li role="none">
<a data-test-edit role="menuitem" tabindex="-1" href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>Edit</a>
</li>
{{#if (not (token/is-legacy item))}}
<li role="none">
<button role="menuitem" tabindex="-1" type="button" data-test-clone {{action onclone item}}>Duplicate</button>
</li>
{{/if}}
{{#if (eq item.AccessorID token.AccessorID) }}
<li role="none" class="dangerous">
<label for={{concat confirm 'logout'}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-logout>Log out</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm logout
</header>
<p>
Are you sure you want to stop using this ACL token? This will log you out.
</p>
</div>
<ul>
<li class="dangerous">
<button tabindex="-1" type="button" onclick={{action onlogout item}}>Logout</button>
</li>
<li>
<label for={{concat confirm 'logout'}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
{{else}}
<li role="none">
<label for={{concat confirm 'use'}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-use>Use</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm use
</header>
<p>
Are you sure you want to use this ACL token?
</p>
</div>
<ul>
<li class="dangerous">
<button data-test-confirm-use tabindex="-1" type="button" onclick={{action onuse item}}>Use</button>
</li>
<li>
<label for={{concat confirm 'use'}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
{{/if}}
{{#unless (or (token/is-anonymous item) (eq item.AccessorID token.AccessorID)) }}
<li role="none" class="dangerous">
<label for={{concat confirm 'delete'}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm Delete
</header>
<p>
Are you sure you want to delete this token?
</p>
</div>
<ul>
<li class="dangerous">
<button tabindex="-1" type="button" class="type-delete" onclick={{action ondelete item}}>Delete</button>
</li>
<li>
<label for={{concat confirm 'delete'}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
{{/unless}}
</BlockSlot>
</PopoverMenu>
</div>
<BlockSlot @name="actions" as |Actions|>
<Actions as |Action|>
<Action data-test-edit-action @href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>
<BlockSlot @name="label">
Edit
</BlockSlot>
</Action>
{{#if (not (token/is-legacy item))}}
<Action data-test-clone-action @onclick={{action onclone item}}>
<BlockSlot @name="label">
Duplicate
</BlockSlot>
</Action>
{{/if}}
{{#if (eq item.AccessorID token.AccessorID)}}
<Action data-test-logout-action class="dangerous" @onclick={{action onclone item}}>
<BlockSlot @name="label">
Logout
</BlockSlot>
<BlockSlot @name="confirmation" as |Confirmation|>
<Confirmation class="warning">
<BlockSlot @name="header">
Confirm logout
</BlockSlot>
<BlockSlot @name="body">
<p>
Are you sure you want to stop using this ACL token? This will log you out.
</p>
</BlockSlot>
<BlockSlot @name="confirm" as |Confirm|>
<Confirm>Logout</Confirm>
</BlockSlot>
</Confirmation>
</BlockSlot>
</Action>
{{else}}
<Action data-test-use-action @onclick={{action onuse item}}>
<BlockSlot @name="label">
Use
</BlockSlot>
<BlockSlot @name="confirmation" as |Confirmation|>
<Confirmation class="warning">
<BlockSlot @name="header">
Confirm use
</BlockSlot>
<BlockSlot @name="body">
<p>
Are you sure you want to use this ACL token?
</p>
</BlockSlot>
<BlockSlot @name="confirm" as |Confirm|>
<Confirm>Use</Confirm>
</BlockSlot>
</Confirmation>
</BlockSlot>
</Action>
{{/if}}
{{#if (not (or (token/is-anonymous item) (eq item.AccessorID token.AccessorID)))}}
<Action data-test-delete-action @onclick={{action ondelete item}} class="dangerous">
<BlockSlot @name="label">
Delete
</BlockSlot>
<BlockSlot @name="confirmation" as |Confirmation|>
<Confirmation class="warning">
<BlockSlot @name="header">
Confirm delete
</BlockSlot>
<BlockSlot @name="body">
<p>
Are you sure you want to delete this token?
</p>
</BlockSlot>
<BlockSlot @name="confirm" as |Confirm|>
<Confirm>Delete</Confirm>
</BlockSlot>
</Confirmation>
</BlockSlot>
</Action>
{{/if}}
</Actions>
</BlockSlot>
</ListCollection>
{{/if}}

View File

@ -1,4 +1,4 @@
export default (collection, clickable, attribute, text, deletable) => () => {
export default (collection, clickable, attribute, text, actions) => () => {
return collection('.consul-token-list li:not(:first-child)', {
id: attribute('data-test-token', '[data-test-token]'),
description: text('[data-test-description]'),
@ -6,10 +6,6 @@ export default (collection, clickable, attribute, text, deletable) => () => {
role: text('[data-test-policy].role', { multiple: true }),
serviceIdentity: text('[data-test-policy].policy-service-identity', { multiple: true }),
token: clickable('a'),
actions: clickable('label'),
use: clickable('[data-test-use]'),
confirmUse: clickable('[data-test-confirm-use]'),
clone: clickable('[data-test-clone]'),
...deletable(),
...actions(['edit', 'delete', 'use', 'logout', 'clone']),
});
};

View File

@ -0,0 +1,22 @@
## ConsulTokenRulesetList
```
<ConsulTokenRulesetList
@item={{item}}
/>
```
A presentational component for rendering Consul ACL token 'rulesets'. Rulesets are the various 'rule-type' things that belong to a token such as policies, identities and roles, and in the case of legacy tokens, the old style string based rules property.
### Arguments
| Argument/Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `item` | `array` | | An ACL token |
### See
- [Component Source Code](./index.js)
- [TemplateSource Code](./index.hbs)
---

View File

@ -0,0 +1,49 @@
{{#let (policy/group item.Policies) as |policies|}}
{{#let (get policies 'management') as |management|}}
{{#if (gt management.length 0)}}
<dl>
<dt>
Management
</dt>
<dd>
{{#each (get policies 'management') as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{item.Name}}</span>
{{/each}}
</dd>
</dl>
{{/if}}
{{/let}}
{{#let (get policies 'identities') as |identities|}}
{{#if (gt identities.length 0)}}
<dl>
<dt>Identities</dt>
<dd>
{{#each identities as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{item.Name}}</span>
{{/each}}
</dd>
</dl>
{{/if}}
{{/let}}
{{#if (token/is-legacy item) }}
<dl>
<dt>Rules</dt>
<dd>
Legacy tokens have embedded rules.
</dd>
</dl>
{{else}}
{{#let (append (get policies 'policies') (or item.Roles (array))) as |policies|}}
{{#if (gt policies.length 0)}}
<dl>
<dt>Rules</dt>
<dd>
{{#each policies as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{item.Name}}</span>
{{/each}}
</dd>
</dl>
{{/if}}
{{/let}}
{{/if}}
{{/let}}

View File

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

View File

@ -11,9 +11,17 @@
<li></li>
{{~#each _cells as |cell|~}}
<li onclick={{action 'click'}} style={{{cell.style}}} class={{if (service/exists cell.item) 'linkable'}}>
<YieldSlot @name="header"><div>{{yield cell.item cell.index checked (action "change")}}</div></YieldSlot>
<YieldSlot @name="details"><div>{{yield cell.item cell.index checked (action "change")}}</div></YieldSlot>
<YieldSlot @name="actions"><div>{{yield cell.item cell.index checked (action "change")}}</div></YieldSlot>
<YieldSlot @name="header"><div>{{yield cell.item cell.index}}</div></YieldSlot>
<YieldSlot @name="details"><div>{{yield cell.item cell.index}}</div></YieldSlot>
<YieldSlot @name="actions"
@params={{
block-params (component 'more-popover-menu' expanded=(if (eq checked cell.index) true false) onchange=(action "change" cell.index))
}}
>
<div>
{{yield cell.item cell.index}}
</div>
</YieldSlot>
</li>
{{~/each~}}
</EmberNativeScrollable>

View File

@ -1,15 +1,21 @@
{{yield}}
{{#let (hash
change=(action "change")
) as |api|}}
<div class="menu-panel {{position}}">
<YieldSlot @name="controls">
{{yield}}
{{yield api}}
</YieldSlot>
{{#yield-slot name="header"}}
<div>{{yield}}</div>
<div>
{{yield api}}
</div>
{{else}}
{{/yield-slot}}
<ul role="menu" ...attributes>
<YieldSlot @name="menu">
{{yield}}
{{yield api}}
</YieldSlot>
</ul>
</div>
</div>
{{/let}}

View File

@ -1,7 +1,26 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import Slotted from 'block-slots';
export default Component.extend(Slotted, {
tagName: '',
dom: service('dom'),
actions: {
change: function(e) {
const id = e.target.getAttribute('id');
const $trigger = this.dom.element(`[for='${id}']`);
const $panel = this.dom.element('[role=menu]', $trigger.parentElement);
const $menuPanel = this.dom.closest('.menu-panel', $panel);
if (e.target.checked) {
$panel.style.display = 'block';
const height = $panel.offsetHeight + 2;
$menuPanel.style.maxHeight = $menuPanel.style.minHeight = `${height}px`;
} else {
$panel.style.display = null;
$menuPanel.style.maxHeight = null;
$menuPanel.style.minHeight = '0';
}
},
},
});

View File

@ -0,0 +1,28 @@
{{yield}}
<li role="none" ...attributes>
{{#if hasConfirmation}}
<label for={{concat menu.confirm guid}} role="menuitem" tabindex="-1" onkeypress={{menu.keypressClick}}>
<YieldSlot @name="label">{{yield}}</YieldSlot>
</label>
<div role="menu">
<YieldSlot @name="confirmation" @params={{
block-params (component 'confirmation-alert'
onclick=(action onclick)
name=(concat menu.confirm guid)
)
}}>{{yield}}</YieldSlot>
</div>
{{else if href}}
<a role="menuitem" tabindex="-1" href={{href}}>
<YieldSlot @name="label">
{{yield}}
</YieldSlot>
</a>
{{else}}
<button role="menuitem" tabindex="-1" type="button" onclick={{action this.onclick}}>
<YieldSlot @name="label">
{{yield}}
</YieldSlot>
</button>
{{/if}}
</li>

View File

@ -0,0 +1,26 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { set } from '@ember/object';
import Slotted from 'block-slots';
export default Component.extend(Slotted, {
tagName: '',
dom: service('dom'),
init: function() {
this._super(...arguments);
this.guid = this.dom.guid(this);
},
didInsertElement: function() {
this._super(...arguments);
this.menu.addSubmenu(this.guid);
},
didDestroyElement: function() {
this._super(...arguments);
this.menu.removeSubmenu(this.guid);
},
willRender: function() {
this._super(...arguments);
set(this, 'hasConfirmation', this._isRegistered('confirmation'));
},
});

View File

@ -0,0 +1,15 @@
<div class="more-popover-menu">
<PopoverMenu @expanded={{expanded}} @onchange={{action onchange}} @keyboardAccess={{false}} as |api|>
<BlockSlot @name="trigger">
More
</BlockSlot>
<BlockSlot @name="menu" as |confirm send keypressClick|>
{{yield (component 'more-popover-menu/action' menu=(hash
addSubmenu=api.addSubmenu
removeSubmenu=api.removeSubmenu
confirm=confirm
keypressClick=keypressClick
))}}
</BlockSlot>
</PopoverMenu>
</div>

View File

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

View File

@ -0,0 +1,17 @@
export default (clickable, confirmation) => (actions, scope) => {
return actions.reduce(
(prev, item) => {
const itemScope = `[data-test-${item}-action]`;
return {
...prev,
[item]: clickable(`${itemScope} [role='menuitem']`),
[`confirm${item.charAt(0).toUpperCase()}${item.substr(1)}`]: clickable(
`${itemScope} [role='menu'] button`
),
};
},
{
actions: clickable('label'),
}
);
};

View File

@ -1,4 +1,4 @@
{{yield (concat 'popover-menu-' guid)}}
{{yield}}
<AriaMenu @keyboardAccess={{keyboardAccess}} as |change keypress ariaLabelledBy ariaControls ariaExpanded keypressClick|>
<ToggleButton
@checked={{if keyboardAccess ariaExpanded expanded}}
@ -7,25 +7,35 @@
<Ref @target={{this}} @name="toggle" @value={{api}} />
<button type="button" aria-haspopup="menu" onkeydown={{keypress}} onclick={{this.toggle.click}} id={{ariaLabelledBy}} aria-controls={{ariaControls}}>
<YieldSlot @name="trigger">
{{yield}}
{{yield (hash
addSubmenu=(action 'addSubmenu')
removeSubmenu=(action 'removeSubmenu')
)}}
</YieldSlot>
</button>
</ToggleButton>
<MenuPanel @position={{position}} id={{ariaControls}} aria-labelledby={{ariaLabelledBy}} aria-expanded={{ariaExpanded}}>
<MenuPanel @position={{position}} id={{ariaControls}} aria-labelledby={{ariaLabelledBy}} aria-expanded={{ariaExpanded}} as |api|>
<BlockSlot @name="controls">
<input type="checkbox" id={{concat 'popover-menu-' guid '-'}} />
{{#each submenus as |sub|}}
<input type="checkbox" id={{concat 'popover-menu-' guid '-' sub}} />
{{/each}}
{{#each submenus as |sub|}}
<input type="checkbox" id={{concat 'popover-menu-' guid '-' sub}} onchange={{api.change}} />
{{/each}}
</BlockSlot>
{{#if hasHeader}}
<BlockSlot @name="header">
{{#yield-slot name="header"}}{{yield}}{{else}}{{/yield-slot}}
{{yield (hash
addSubmenu=(action 'addSubmenu')
removeSubmenu=(action 'removeSubmenu')
)}}
{{#yield-slot name="header"}}{{else}}{{/yield-slot}}
</BlockSlot>
{{/if}}
<BlockSlot @name="menu">
<YieldSlot @name="menu" @params={{block-params (concat "popover-menu-" guid "-") send keypressClick this.toggle.click}}>
{{yield}}
{{yield (hash
addSubmenu=(action 'addSubmenu')
removeSubmenu=(action 'removeSubmenu')
)}}
</YieldSlot>
</BlockSlot>
</MenuPanel>

View File

@ -16,11 +16,22 @@ export default Component.extend(Slotted, {
init: function() {
this._super(...arguments);
this.guid = this.dom.guid(this);
this.submenus = [];
},
willRender: function() {
set(this, 'hasHeader', this._isRegistered('header'));
},
actions: {
addSubmenu: function(name) {
set(this, 'submenus', this.submenus.concat(name));
},
removeSubmenu: function(name) {
const pos = this.submenus.indexOf(name);
if (pos !== -1) {
this.submenus.splice(pos, 1);
set(this, 'submenus', this.submenus);
}
},
change: function(e) {
if (!e.target.checked) {
[...this.dom.elements(`[id^=popover-menu-${this.guid}]`)].forEach(function($item) {

View File

@ -3,13 +3,16 @@ import { get } from '@ember/object';
const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001';
export function typeOf(params, hash) {
const item = params[0];
const template = get(item, 'template');
switch (true) {
case typeof template === 'undefined':
return 'role';
case template === 'service-identity':
return 'policy-service-identity';
case template === 'node-identity':
return 'policy-node-identity';
case get(item, 'ID') === MANAGEMENT_ID:
return 'policy-management';
case typeof get(item, 'template') === 'undefined':
return 'role';
case get(item, 'template') !== '':
return 'policy-service-identity';
default:
return 'policy';
}

View File

@ -6,6 +6,7 @@
background-color: $yellow-050;
border-top-left-radius: $decor-radius-200;
border-top-right-radius: $decor-radius-200;
cursor: default;
}
%confirmation-alert > ul {
list-style: none;

View File

@ -4,15 +4,16 @@
%menu-panel [type='checkbox'] {
display: none;
}
%menu-panel [type='checkbox'] ~ * {
transition: transform 150ms, min-height 150ms, max-height 150ms;
%menu-panel {
overflow: hidden;
transition: min-height 150ms, max-height 150ms;
min-height: 0;
}
%menu-panel [type='checkbox'] ~ * {
transition: transform 150ms;
}
%menu-panel [type='checkbox']:checked ~ * {
transform: translateX(calc(-100% - 10px));
/* this needs to autocalculate */
/* or be hardcoded */
/* min-height: 143px; */
}
%menu-panel [role='menuitem'] {
display: flex;
@ -27,7 +28,19 @@
position: absolute;
top: 0;
left: calc(100% + 10px);
display: none;
}
/* TODO: once everything is using ListCollection */
/* this can go */
%menu-panel [type='checkbox']:checked ~ * {
/* this needs to autocalculate */
min-height: 143px;
max-height: 143px;
}
%menu-panel [id$='-']:first-child:checked ~ ul label[for$='-'] + [role='menu'] {
display: block;
}
/**/
%menu-panel dl {
padding: 0.9em 1em;
}

View File

@ -1,18 +1,17 @@
@import './skin';
@import './layout';
%with-popover-menu > input {
%with-popover-menu > [type='checkbox'] {
@extend %popover-menu;
}
%popover-menu {
@extend %display-toggle-siblings;
}
%popover-menu + label + div {
@extend %popover-menu-panel;
}
%popover-menu + label > * {
@extend %toggle-button;
}
%more-popover-menu-panel,
%popover-menu + label + div {
@extend %popover-menu-panel;
}
%popover-menu-panel {
@extend %menu-panel;
}

View File

@ -1,56 +1,12 @@
%with-popover-menu {
position: relative;
}
%with-popover-menu > [type='checkbox'] {
@extend %popover-menu;
}
%more-popover-menu {
@extend %display-toggle-siblings;
}
%more-popover-menu + label > * {
@extend %toggle-button;
}
%more-popover-menu-panel {
overflow: hidden;
width: 192px;
}
%more-popover-menu + label + div {
@extend %more-popover-menu-panel;
}
%more-popover-menu-panel:not(.above) {
top: 48px;
}
%more-popover-menu-panel:not(.left) {
right: 10px;
}
%more-popover-menu-panel li [role='menu'] {
display: none;
}
%more-popover-menu-panel [id$='-']:first-child:checked ~ ul label[for$='-'] + [role='menu'] {
display: block;
}
%popover-menu {
@extend %display-toggle-siblings;
}
%popover-menu + label > * {
@extend %toggle-button;
}
%popover-menu-panel {
@extend %menu-panel;
width: 192px;
}
%popover-menu + label + div {
@extend %popover-menu-panel;
}
%popover-menu-panel:not(.above) {
top: 38px;
}
%popover-menu-panel:not(.left) {
right: 10px;
}
%popover-menu-panel li [role='menu'] {
display: none;
}
%popover-menu-panel [id$='-']:first-child:checked ~ ul label[for$='-'] + [role='menu'] {
display: block;
right: 5px;
}

View File

@ -4,13 +4,3 @@
height: 16px;
margin-left: 16px;
}
%more-popover-menu + label > *::after {
@extend %with-more-horizontal-icon, %as-pseudo;
opacity: 0.7;
width: 16px;
height: 16px;
}
%more-popover-menu + label > * {
font-size: 0;
background-color: $transparent;
}

View File

@ -10,28 +10,23 @@
.consul-gateway-service-list > ul > li:not(:first-child),
.consul-service-instance-list > ul > li:not(:first-child),
.consul-service-list > ul > li:not(:first-child),
.consul-token-list > ul > li:not(:first-child) {
.consul-token-list > ul > li:not(:first-child),
.consul-policy-list > ul > li:not(:first-child),
.consul-role-list > ul > li:not(:first-child) {
@extend %with-composite-row-intent;
}
/*TODO: This hides the icons-less dt's in the below lists as */
/* they don't have tooltips */
.consul-token-list > ul > li:not(:first-child) dt {
.consul-token-list > ul > li:not(:first-child) dt,
.consul-policy-list > ul li:not(:first-child) dl:not(.datacenter) dt,
.consul-role-list > ul > li:not(:first-child) dt {
display: none;
}
/* TODO: the service list has a 1px offset */
.consul-policy-list dl.datacenter dt,
.consul-service-list li > div:first-child > dl:first-child dd {
margin-top: 1px;
}
.proxy-exposed-paths tbody tr {
cursor: default !important;
}
.proxy-exposed-paths tbody tr:hover {
box-shadow: none !important;
}
.proxy-exposed-paths tbody tr .combined-address button:hover {
// In this case we do not need a background on the icon
background-color: $transparent !important;
}
.proxy-exposed-paths > ul,
.proxy-upstreams > ul {
border-top: 1px solid $gray-200;

View File

@ -33,9 +33,12 @@
margin-right: 3px;
}
%composite-row-detail .policy-management::before {
margin-right: 3px;
}
%composite-row-detail .policy-management::before,
%composite-row-header .policy-management dd::before {
@extend %with-star-fill-mask, %as-pseudo;
background-color: var(--brand-600);
margin-right: 3px;
}
// Health Checks
%composite-row-detail li.passing::before,

View File

@ -1,18 +1,24 @@
.more-popover-menu > [type='checkbox'] {
.more-popover-menu {
@extend %more-popover-menu;
}
%more-popover-menu-panel [type='checkbox']:checked ~ * {
/* this needs to autocalculate */
min-height: 143px;
max-height: 143px;
%more-popover-menu {
@extend %with-popover-menu;
}
%more-popover-menu-panel [id$='logout']:checked ~ * {
/* this needs to autocalculate */
min-height: 183px;
max-height: 183px;
%more-popover-menu > [type='checkbox'] + label {
@extend %more-popover-menu-trigger;
}
%more-popover-menu-panel [id$='delete']:checked ~ ul label[for$='delete'] + [role='menu'],
%more-popover-menu-panel [id$='logout']:checked ~ ul label[for$='logout'] + [role='menu'],
%more-popover-menu-panel [id$='use']:checked ~ ul label[for$='use'] + [role='menu'] {
/* This gives the trigger a slightly larger invisible hit area */
%more-popover-menu-trigger {
padding: 7px;
display: block;
}
%more-popover-menu-trigger > * {
font-size: 0;
background-color: $transparent;
padding: 0;
}
%more-popover-menu-trigger > *::after {
@extend %with-more-horizontal-mask, %as-pseudo;
background-color: $gray-900;
margin: 0;
}

View File

@ -2,44 +2,21 @@ td strong {
@extend %pill;
margin-right: 3px;
}
// For the moment pills with classes are iconed ones
%pill:not([class]) {
@extend %frame-gray-900;
}
%pill[class] {
padding-left: 0;
margin-right: 16px;
}
%pill[class]::before {
@extend %as-pseudo;
margin-right: 3px;
}
%pill.policy::before {
@extend %with-file-fill-icon;
opacity: 0.3;
}
%pill.policy-management::before {
@extend %with-star-icon;
}
%pill.role::before {
@extend %with-user-plain-icon;
opacity: 0.3;
}
// TODO: These are related to the pill icons, but also to the tables
// All of this icon assigning stuff should probably go in the eventual
// refactored /components/icons.scss file
span.policy-management a::after {
@extend %with-star-icon, %as-pseudo;
margin-left: 3px;
}
span.policy-service-identity,
span.policy-node-identity,
.consul-external-source,
.consul-kind {
@extend %reduced-pill;
}
span.policy-service-identity::before {
width: 0;
span.policy-service-identity::before,
span.policy-node-identity::before {
display: inline-block;
width: auto;
}
span.policy-node-identity::before {
content: 'Node Identity: ';
}
span.policy-service-identity::before {
content: 'Service Identity: ';
}

View File

@ -1,19 +1,13 @@
%main-content table {
@extend %table;
}
%table-actions > [type='checkbox'] {
%table-actions {
@extend %more-popover-menu;
}
%table-actions .confirmation-alert {
@extend %confirmation-alert;
overflow: visible;
}
%table-actions > [type='checkbox'] + label {
position: absolute;
top: 8px;
right: 15px;
}
%table-actions .menu-panel:not(.above) {
top: 38px !important;
right: 5px;
}
/*TODO: Rename this to %app-view-brand-icon or similar */

View File

@ -1,5 +1,5 @@
<p class="notice policy-management"><strong>Management</strong> This global-management token is built into Consul's policy system. You can apply this special policy to tokens for full access. This policy is not editable or removeable, but can be ignored by not applying it to any tokens. Learn more in our <a href="{{env 'CONSUL_DOCS_URL'}}/guides/acl.html#builtin-policies" target="_blank" rel="noopener noreferrer">documentation</a>.</p>
<div>
<div class="definition-table">
<dl>
<dt>Name</dt>
<dd>{{item.Name}}</dd>

View File

@ -40,66 +40,10 @@
{{/if}}
<ChangeableSet @dispatcher={{searchable 'policy' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection @items={{sort-by "CreateIndex:desc" "Name:asc" filtered}} as |item index|>
<BlockSlot @name="header">
<th>Name</th>
<th>Datacenters</th>
<th>Description</th>
</BlockSlot>
<BlockSlot @name="row">
<td data-test-policy="{{item.Name}}">
<a href={{href-to 'dc.acls.policies.edit' item.ID}} class={{if (eq (policy/typeof item) 'policy-management') 'is-management'}}>{{item.Name}}</a>
</td>
<td>
{{join ', ' (policy/datacenters item)}}
</td>
<td data-test-description>
<p>{{item.Description}}</p>
</td>
</BlockSlot>
<BlockSlot @name="actions" as |index change checked|>
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}>
<BlockSlot @name="trigger">
More
</BlockSlot>
<BlockSlot @name="menu" as |confirm send keypressClick|>
{{#if (eq (policy/typeof item) 'policy-management')}}
<li role="none">
<a role="menuitem" tabindex="-1" data-test-edit href={{href-to 'dc.acls.policies.edit' item.ID}}>View</a>
</li>
{{else}}
<li role="none">
<a role="menuitem" tabindex="-1" data-test-edit href={{href-to 'dc.acls.policies.edit' item.ID}}>Edit</a>
</li>
<li role="none" class="dangerous">
<label for={{confirm}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm Delete
</header>
<p>
Are you sure you want to delete this policy?
</p>
</div>
<ul>
<li class="dangerous">
<button tabindex="-1" type="button" class="type-delete" onclick={{action send 'delete' item}}>Delete</button>
</li>
<li>
<label for={{confirm}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
{{/if}}
</BlockSlot>
</PopoverMenu>
</BlockSlot>
</TabularCollection>
<ConsulPolicyList
@items={{sort-by "CreateTime:desc" "Name:asc" filtered}}
@ondelete={{action send 'delete'}}
/>
</BlockSlot>
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>

View File

@ -40,61 +40,10 @@
{{/if}}
<ChangeableSet @dispatcher={{searchable 'role' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection @items={{sort-by "CreateIndex:desc" "Name:asc" filtered}} as |item index|>
<BlockSlot @name="header">
<th>Name</th>
<th>Description</th>
<th>Policies</th>
</BlockSlot>
<BlockSlot @name="row">
<td data-test-role="{{item.Name}}">
<a href={{href-to 'dc.acls.roles.edit' item.ID}}>{{item.Name}}</a>
</td>
<td data-test-description>
<p>{{item.Description}}</p>
</td>
<td>
{{#each item.Policies as |item|}}
<strong data-test-policy class={{policy/typeof item}}>{{item.Name}}</strong>
{{/each}}
</td>
</BlockSlot>
<BlockSlot @name="actions" as |index change checked|>
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}>
<BlockSlot @name="trigger">
More
</BlockSlot>
<BlockSlot @name="menu" as |confirm send keypressClick|>
<li role="none">
<a role="menuitem" tabindex="-1" href={{href-to 'dc.acls.roles.edit' item.ID}}>Edit</a>
</li>
<li role="none" class="dangerous">
<label for={{confirm}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm Delete
</header>
<p>
Are you sure you want to delete this role?
</p>
</div>
<ul>
<li class="dangerous">
<button tabindex="-1" type="button" class="type-delete" onclick={{action send 'delete' item}}>Delete</button>
</li>
<li>
<label for={{confirm}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
</BlockSlot>
</PopoverMenu>
</BlockSlot>
</TabularCollection>
<ConsulRoleList
@items={{sort-by "CreateTime:desc" "Name:asc" filtered}}
@ondelete={{action send 'delete'}}
/>
</BlockSlot>
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>

View File

@ -77,7 +77,7 @@ Feature: dc / acls / tokens / index: ACL Token List
s: Si-Search
---
And I see 1 token model
And I see 1 token model with the serviceIdentity "Service Identity: Si-Search"
And I see 1 token model with the serviceIdentity "Si-Search"
Scenario: I see the legacy message if I have one legacy token
Given 1 datacenter model with the value "dc-1"
And 3 token models from yaml

View File

@ -33,8 +33,13 @@ import policyFormFactory from 'consul-ui/components/policy-form/pageobject';
import policySelectorFactory from 'consul-ui/components/policy-selector/pageobject';
import roleFormFactory from 'consul-ui/components/role-form/pageobject';
import roleSelectorFactory from 'consul-ui/components/role-selector/pageobject';
import morePopoverMenuFactory from 'consul-ui/components/more-popover-menu/pageobject';
import tokenListFactory from 'consul-ui/components/token-list/pageobject';
import consulTokenListFactory from 'consul-ui/components/consul-token-list/pageobject';
import consulRoleListFactory from 'consul-ui/components/consul-role-list/pageobject';
import consulPolicyListFactory from 'consul-ui/components/consul-policy-list/pageobject';
import consulIntentionListFactory from 'consul-ui/components/consul-intention-list/pageobject';
// pages
@ -86,8 +91,31 @@ const policyForm = policyFormFactory(submitable, cancelable, radiogroup, text);
const policySelector = policySelectorFactory(clickable, deletable, collection, alias, policyForm);
const roleForm = roleFormFactory(submitable, cancelable, policySelector);
const roleSelector = roleSelectorFactory(clickable, deletable, collection, alias, roleForm);
const morePopoverMenu = morePopoverMenuFactory(clickable);
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable);
const consulTokenList = consulTokenListFactory(collection, clickable, attribute, text, deletable);
const consulTokenList = consulTokenListFactory(
collection,
clickable,
attribute,
text,
morePopoverMenu
);
const consulRoleList = consulRoleListFactory(
collection,
clickable,
attribute,
text,
morePopoverMenu
);
const consulPolicyList = consulPolicyListFactory(
collection,
clickable,
attribute,
text,
morePopoverMenu
);
const page = pageFactory(clickable, attribute, is, authForm);
@ -115,22 +143,9 @@ export default {
kv: create(kv(visitable, attribute, submitable, deletable, cancelable, clickable)),
acls: create(acls(visitable, deletable, creatable, clickable, attribute, collection, aclFilter)),
acl: create(acl(visitable, submitable, deletable, cancelable, clickable)),
policies: create(
policies(
visitable,
deletable,
creatable,
clickable,
attribute,
collection,
text,
freetextFilter
)
),
policies: create(policies(visitable, creatable, consulPolicyList, freetextFilter)),
policy: create(policy(visitable, submitable, deletable, cancelable, clickable, tokenList)),
roles: create(
roles(visitable, deletable, creatable, clickable, attribute, collection, text, freetextFilter)
),
roles: create(roles(visitable, creatable, consulRoleList, freetextFilter)),
// TODO: This needs a policyList
role: create(role(visitable, submitable, deletable, cancelable, policySelector, tokenList)),
tokens: create(tokens(visitable, creatable, text, consulTokenList, freetextFilter)),

View File

@ -1,24 +1,7 @@
export default function(
visitable,
deletable,
creatable,
clickable,
attribute,
collection,
text,
filter
) {
export default function(visitable, creatable, policies, filter) {
return creatable({
visit: visitable('/:dc/acls/policies'),
policies: collection(
'[data-test-tabular-row]',
deletable({
name: attribute('data-test-policy', '[data-test-policy]'),
description: text('[data-test-description]'),
policy: clickable('a'),
actions: clickable('label'),
})
),
policies: policies(),
filter: filter(),
});
}

View File

@ -1,25 +1,8 @@
export default function(
visitable,
deletable,
creatable,
clickable,
attribute,
collection,
text,
filter
) {
return creatable({
export default function(visitable, creatable, roles, filter) {
return {
visit: visitable('/:dc/acls/roles'),
roles: collection(
'[data-test-tabular-row]',
deletable({
name: attribute('data-test-role', '[data-test-role]'),
description: text('[data-test-description]'),
policy: text('[data-test-policy].policy', { multiple: true }),
serviceIdentity: text('[data-test-policy].policy-service-identity', { multiple: true }),
actions: clickable('label'),
})
),
roles: roles(),
filter: filter(),
});
...creatable(),
};
}