ui: Token listing redesign (#8117)

This commit is contained in:
John Cowen 2020-06-17 10:25:54 +01:00 committed by GitHub
parent 97342de262
commit 9a539f0340
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 895 additions and 820 deletions

View File

@ -0,0 +1,71 @@
<ul data-test-proxy-exposed-paths>
{{#each items as |path|}}
<li>
<div>
{{#let (concat address ':' path.Path) as |combinedAddress|}}
<p class="combined-address">
<span>
{{combinedAddress}}
</span>
<CopyButton
@value={{combinedAddress}}
@name="Address"
/>
</p>
{{/let}}
</div>
<div>
<ul>
{{#if path.Protocol}}
<li class="protocol">
<span>
<Tooltip>
Protocol
</Tooltip>
</span>
<span>
{{path.Protocol}}
</span>
</li>
{{/if}}
{{#if path.ListenerPort}}
<li class="port">
<span>
<Tooltip>
Port
</Tooltip>
</span>
<span>
listening on :{{path.ListenerPort}}
</span>
</li>
{{/if}}
{{#if path.LocalPathPort}}
<li class="port">
<span>
<Tooltip>
Port
</Tooltip>
</span>
<span>
local port :{{path.LocalPathPort}}
</span>
</li>
{{/if}}
{{#if path.Path}}
<li class="path">
<span>
<Tooltip>
Path
</Tooltip>
</span>
<span>
{{path.Path}}
</span>
</li>
{{/if}}
</ul>
</div>
</li>
{{/each}}
</ul>

View File

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

View File

@ -8,7 +8,7 @@ A presentational component for presenting Consul Metadata
| Argument/Attribute | Type | Default | Description | | Argument/Attribute | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `items` | `array` | | A an array of entries or `[key, value]` pairs as returned by `Object.entries()` | | `items` | `array` | | An array of entries or `[key, value]` pairs as returned by `Object.entries()` |
### Example ### Example

View File

@ -1,30 +1,32 @@
{{yield}}
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<ListCollection @items={{items}} class="consul-service-instance-list" as |item index|> <ListCollection @items={{items}} class="consul-service-instance-list" as |item index|>
<a href={{href-to routeName item.Service.Service item.Node.Node (or item.Service.ID item.Service.Service)}}> <BlockSlot @name="header">
{{item.Service.ID}} <a href={{href-to routeName item.Service.Service item.Node.Node (or item.Service.ID item.Service.Service)}}>
</a> {{item.Service.ID}}
<ul> </a>
<ConsulExternalSource @item={{item.Service}} as |ExternalSource|> </BlockSlot>
<li> <BlockSlot @name="details">
<ExternalSource /> <ul>
</li> <ConsulExternalSource @item={{item.Service}} as |ExternalSource|>
</ConsulExternalSource> <li>
<ExternalSource />
</li>
</ConsulExternalSource>
{{#let (reject-by 'ServiceID' '' item.Checks) as |checks|}} {{#let (reject-by 'ServiceID' '' item.Checks) as |checks|}}
{{#let (service/instance-checks checks) as |serviceCheck| }} {{#let (service/instance-checks checks) as |serviceCheck| }}
{{#if (eq serviceCheck.check 'empty') }} {{#if (eq serviceCheck.check 'empty') }}
<li class={{serviceCheck.check}}> <li class={{serviceCheck.check}}>
No service checks No service checks
</li> </li>
{{else}} {{else}}
{{#if (eq serviceCheck.count checks.length)}} {{#if (eq serviceCheck.count checks.length)}}
<li class={{serviceCheck.check}}> <li class={{serviceCheck.check}}>
All service checks {{serviceCheck.status}} All service checks {{serviceCheck.status}}
</li> </li>
{{else}} {{else}}
<li class={{serviceCheck.check}}> <li class={{serviceCheck.check}}>
{{serviceCheck.count}}/{{checks.length}} service checks {{serviceCheck.status}} {{serviceCheck.count}}/{{checks.length}} service checks {{serviceCheck.status}}
</li> </li>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/let}} {{/let}}
@ -32,9 +34,9 @@
{{#let (filter-by 'ServiceID' '' item.Checks) as |checks|}} {{#let (filter-by 'ServiceID' '' item.Checks) as |checks|}}
{{#let (service/instance-checks checks) as |nodeCheck| }} {{#let (service/instance-checks checks) as |nodeCheck| }}
{{#if (eq nodeCheck.check 'empty') }} {{#if (eq nodeCheck.check 'empty') }}
<li class={{nodeCheck.check}}> <li class={{nodeCheck.check}}>
No node checks No node checks
</li> </li>
{{else}} {{else}}
{{#if (eq nodeCheck.count checks.length)}} {{#if (eq nodeCheck.count checks.length)}}
<li class={{nodeCheck.check}}> <li class={{nodeCheck.check}}>
@ -49,27 +51,28 @@
{{/let}} {{/let}}
{{/let}} {{/let}}
{{#if (get proxies item.Service.ID)}} {{#if (get proxies item.Service.ID)}}
<li class="proxy"> <li class="proxy">
connected with proxy connected with proxy
</li> </li>
{{/if}} {{/if}}
{{#if (gt item.Node.Node.length 0)}} {{#if (gt item.Node.Node.length 0)}}
<li class="node"> <li class="node">
<a href={{href-to 'dc.nodes.show' item.Node.Node}}>{{item.Node.Node}}</a> <a href={{href-to 'dc.nodes.show' item.Node.Node}}>{{item.Node.Node}}</a>
</li>
{{/if}}
<li class="address" data-test-address>
{{#if (not-eq item.Service.Address '')}}
{{item.Service.Address}}:{{item.Service.Port}}
{{else}}
{{item.Node.Address}}:{{item.Service.Port}}
{{/if}}
</li>
<TagList @item={{item.Service}} as |Tags|>
<li>
<Tags />
</li> </li>
</TagList> {{/if}}
</ul> <li class="address" data-test-address>
{{#if (not-eq item.Service.Address '')}}
{{item.Service.Address}}:{{item.Service.Port}}
{{else}}
{{item.Node.Address}}:{{item.Service.Port}}
{{/if}}
</li>
<TagList @item={{item.Service}} as |Tags|>
<li>
<Tags />
</li>
</TagList>
</ul>
</BlockSlot>
</ListCollection> </ListCollection>
{{/if}} {{/if}}

View File

@ -1,20 +1,24 @@
{{yield}}
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<ListCollection @items={{items}} class="consul-service-list" as |item index|> <ListCollection @items={{items}} class="consul-service-list" as |item index|>
<li> <BlockSlot @name="header">
<span class={{service/health-checks item}}> <dl class={{service/health-checks item}}>
<Tooltip> <dt>
{{#if (eq 'critical' (service/health-checks item))}} Health
At least one health check on one instance is failing. </dt>
{{else if (eq 'warning' (service/health-checks item))}} <dd>
At least one health check on one instance has a warning. <Tooltip @position="top-start">
{{else if (eq 'passing' (service/health-checks item))}} {{#if (eq 'critical' (service/health-checks item))}}
All health checks are passing. At least one health check on one instance is failing.
{{else}} {{else if (eq 'warning' (service/health-checks item))}}
There are no health checks. At least one health check on one instance has a warning.
{{/if}} {{else if (eq 'passing' (service/health-checks item))}}
</Tooltip> All health checks are passing.
</span> {{else}}
There are no health checks.
{{/if}}
</Tooltip>
</dd>
</dl>
{{#if (eq item.Kind 'terminating-gateway')}} {{#if (eq item.Kind 'terminating-gateway')}}
<a data-test-service-name href={{href-to "dc.services.show.services" item.Name}}> <a data-test-service-name href={{href-to "dc.services.show.services" item.Name}}>
{{item.Name}} {{item.Name}}
@ -28,40 +32,42 @@
{{item.Name}} {{item.Name}}
</a> </a>
{{/if}} {{/if}}
</li> </BlockSlot>
<ul> <BlockSlot @name="details">
{{#if (and nspace (env 'CONSUL_NSPACES_ENABLED'))}} <ul>
{{#if (not-eq item.Namespace nspace)}} {{#if (and nspace (env 'CONSUL_NSPACES_ENABLED'))}}
<li class="nspace"> {{#if (not-eq item.Namespace nspace)}}
{{item.Namespace}} <li class="nspace">
{{item.Namespace}}
</li>
{{/if}}
{{/if}}
<ConsulKind @item={{item}} as |Kind|>
<li>
<Kind />
</li>
</ConsulKind>
<ConsulExternalSource @item={{item}} as |ExternalSource|>
<li>
<ExternalSource />
</li>
</ConsulExternalSource>
{{#if (not-eq item.InstanceCount 0)}}
<li>
{{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'Instance' without-count=true}}
</li> </li>
{{/if}} {{/if}}
{{/if}} {{#if (get proxies item.Name)}}
<ConsulKind @item={{item}} as |Kind|> <li data-test-proxy class="proxy">
<li> connected with proxy
<Kind />
</li> </li>
</ConsulKind> {{/if}}
<ConsulExternalSource @item={{item}} as |ExternalSource|> <TagList @item={{item}} as |Tags|>
<li> <li>
<ExternalSource /> <Tags />
</li> </li>
</ConsulExternalSource> </TagList>
{{#if (not-eq item.InstanceCount 0)}} </ul>
<li> </BlockSlot>
{{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'Instance' without-count=true}}
</li>
{{/if}}
{{#if (get proxies item.Name)}}
<li data-test-proxy class="proxy">
connected with proxy
</li>
{{/if}}
<TagList @item={{item}} as |Tags|>
<li>
<Tags />
</li>
</TagList>
</ul>
</ListCollection> </ListCollection>
{{/if}} {{/if}}

View File

@ -0,0 +1,32 @@
## ConsulTokenList
```
<ConsulTokenList
@items={{items}}
@token={{userToken}}
@onuse={{action 'use'}}
@ondelete={{action 'delete'}}
@onlogout={{action 'logout'}}
@onclone={{action 'clone'}}
/>
```
A presentational component for rendering Consul ACL tokens
### Arguments
| Argument/Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `array` | | An array of ACL tokens |
| `token` | `Token` | | A token object to use for comparision for current token, usually the users current token |
| `onuse` | `function` | | An action to execute when the `Use` action is clicked |
| `ondelete` | `function` | | An action to execute when the `Delete` action is clicked |
| `onlogout` | `function` | | An action to execute when the `Logout` action is clicked |
| `onclone` | `function` | | An action to execute when the `Clone/Duplicate` action is clicked |
### See
- [Component Source Code](./index.js)
- [TemplateSource Code](./index.hbs)
---

View File

@ -0,0 +1,157 @@
{{#if (gt items.length 0)}}
<ListCollection @items={{items}} class="consul-token-list" as |item index checked change|>
<BlockSlot @name="header">
{{#if (eq item.AccessorID token.AccessorID)}}
<dl rel="me">
<dd>
<Tooltip>
Your token
</Tooltip>
</dd>
</dl>
{{/if}}
<a href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>{{substr item.AccessorID -8}}</a>
</BlockSlot>
<BlockSlot @name="details">
<dl>
<dt>Scope</dt>
<dd>
{{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}}
<dl>
<dt>Identities</dt>
<dd>
{{#each (append (get policies 'identities')) as |item|}}
<span data-test-policy class={{policy/typeof item}}>Service Identity: {{item.Name}}</span>
{{/each}}
</dd>
</dl>
<dl>
<dt>Rules</dt>
<dd>
{{#if (token/is-legacy item) }}
Legacy tokens have embedded rules.
{{ else }}
{{#each (append (get policies 'policies') item.Roles) as |item|}}
<span data-test-policy class={{policy/typeof item}}>{{item.Name}}</span>
{{/each}}
{{/if}}
</dd>
</dl>
{{/let}}
<dl>
<dt>Description</dt>
<dd data-test-description>
{{or item.Description item.Name}}
</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>
</ListCollection>
{{/if}}

View File

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

View File

@ -0,0 +1,15 @@
export default (collection, clickable, attribute, text, deletable) => () => {
return collection('.consul-token-list li:not(:first-child)', {
id: attribute('data-test-token', '[data-test-token]'),
description: text('[data-test-description]'),
policy: text('[data-test-policy].policy', { multiple: true }),
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(),
});
};

View File

@ -0,0 +1,59 @@
<ul data-test-proxy-upstreams>
{{#each items as |item|}}
<li>
<div>
<p data-test-destination-name>
{{item.DestinationName}}
</p>
</div>
<div>
<ul>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (not-eq item.DestinationType 'prepared_query')}}
<li class="nspace">
<span>
<Tooltip>
Namespace
</Tooltip>
</span>
<span>
{{or item.DestinationNamespace 'default'}}
</span>
</li>
{{/if}}
{{/if}}
{{#if (and (not-eq item.Datacenter dc) (not-eq item.Datacenter ""))}}
<li class="datacenter">
<span>
<Tooltip>
Datacenter
</Tooltip>
</span>
<span>
{{item.Datacenter}}
</span>
</li>
{{/if}}
{{#if (gt item.LocalBindPort 0)}}
{{#let (concat (or item.LocalBindAddress '127.0.0.1') ':' item.LocalBindPort) as |combinedAddress|}}
<li class="port">
<span>
<Tooltip>
Address
</Tooltip>
</span>
<span>
<span>{{combinedAddress}}</span>
<CopyButton
@value={{combinedAddress}}
@name="Address"
/>
</span>
</li>
{{/let}}
{{/if}}
</ul>
</div>
</li>
{{/each}}
</ul>

View File

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

View File

@ -0,0 +1,69 @@
<ListCollection @items={{items}} class="consul-upstream-list" as |item index|>
<BlockSlot @name="header">
{{#if (service/exists item)}}
<dl class={{service/health-checks item}}>
<dt>
Health
</dt>
<dd>
<Tooltip @position="top-start">
{{#if (eq 'critical' (service/health-checks item))}}
At least one health check on one instance is failing.
{{else if (eq 'warning' (service/health-checks item))}}
At least one health check on one instance has a warning.
{{else if (eq 'passing' (service/health-checks item))}}
All health checks are passing.
{{else}}
There are no health checks.
{{/if}}
</Tooltip>
</dd>
</dl>
{{#if (and (env 'CONSUL_NSPACES_ENABLED') (not-eq item.Namespace nspace))}}
<a data-test-service-name href={{href-to 'nspace.dc.services.show' (concat '~' item.Namespace) dc item.Name }}>
{{item.Name}}
</a>
{{else}}
<a data-test-service-name href={{href-to 'dc.services.show' item.Name}}>
{{item.Name}}
</a>
{{/if}}
{{else}}
<p data-test-service-name>
{{item.Name}}
</p>
{{/if}}
</BlockSlot>
<BlockSlot @name="details">
<ul>
{{#if (and (env 'CONSUL_NSPACES_ENABLED') (not-eq item.Namespace nspace))}}
<li class="nspace">
<span>
<Tooltip>
Namespace
</Tooltip>
</span>
<span>
{{item.Namespace}}
</span>
</li>
{{/if}}
{{#if (not-eq item.GatewayConfig.ListenerPort 0)}}
<li class="port">
<span>
<Tooltip>
Port
</Tooltip>
</span>
<span>
<span>:{{item.GatewayConfig.ListenerPort}}</span>
<CopyButton
@value={{item.GatewayConfig.ListenerPort}}
@name="Port"
/>
</span>
</li>
{{/if}}
</ul>
</BlockSlot>
</ListCollection>

View File

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

View File

@ -0,0 +1,29 @@
export default {
id: 'copy-button',
initial: 'idle',
on: {
RESET: [
{
target: 'idle',
},
],
},
states: {
idle: {
on: {
SUCCESS: [
{
target: 'success',
},
],
ERROR: [
{
target: 'error',
},
],
},
},
success: {},
error: {},
},
};

View File

@ -1,17 +1,16 @@
<FeedbackDialog class="copy-button" @type="inline"> <StateChart @src={{chart}} as |State Guard Action dispatch state|>
<BlockSlot @name="action" as |success error|> <Ref @target={{this}} @name="dispatch" @value={{dispatch}} />
<Ref @target={{this}} @name="success" @value={{success}} /> <State @matches="success">
<Ref @target={{this}} @name="error" @value={{error}} /> <Tooltip @targetId={{guid}} @isShown={{true}} @position={{position}} @duration={{3000}} @oncomplete={{action dispatch 'RESET'}}>
<button id={{guid}} title={{concat "Copy " name " to the clipboard"}} ...attributes type="button" class="copy-btn" data-clipboard-text={{value}}>{{~yield~}}</button> <span role="alert">Copied {{name}}!</span>
</BlockSlot> </Tooltip>
<BlockSlot @name="success" as |transition|> </State>
<p class={{transition}}> <State @matches="error">
Copied {{name}}! <Tooltip role="alert" @targetId={{guid}} @isShown={{true}} @position={{position}} @duration={{3000}} @oncomplete={{action dispatch 'RESET'}}>
</p> <span role="alert">There was an problem!</span>
</BlockSlot> </Tooltip>
<BlockSlot @name="error" as |transition|> </State>
<p class={{transition}}> <div class="copy-button" id={{guid}}>
Sorry, something went wrong! <button title={{concat "Copy " name " to the clipboard"}} ...attributes type="button" class="copy-btn" data-clipboard-text={{value}}>{{~yield~}}</button>
</p> </div>
</BlockSlot> </StateChart>
</FeedbackDialog>

View File

@ -1,5 +1,6 @@
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 chart from './chart.xstate';
export default Component.extend({ export default Component.extend({
clipboard: service('clipboard/os'), clipboard: service('clipboard/os'),
@ -7,6 +8,7 @@ export default Component.extend({
tagName: '', tagName: '',
init: function() { init: function() {
this._super(...arguments); this._super(...arguments);
this.chart = chart;
this.guid = this.dom.guid(this); this.guid = this.dom.guid(this);
this._listeners = this.dom.listeners(); this._listeners = this.dom.listeners();
}, },
@ -16,14 +18,9 @@ export default Component.extend({
}, },
didInsertElement: function() { didInsertElement: function() {
this._super(...arguments); this._super(...arguments);
const component = this; this._listeners.add(this.clipboard.execute(`#${this.guid} button`), {
this._listeners.add(this.clipboard.execute(`#${this.guid}`), { success: () => this.dispatch('SUCCESS'),
success: function() { error: () => this.dispatch('ERROR'),
component.success(...arguments);
},
error: function() {
component.error(...arguments);
},
}); });
}, },
}); });

View File

@ -1,11 +0,0 @@
<div {{ref this '$feedback'}} class="with-feedback" ...attributes>
{{yield}}
{{#if (eq state 'success') }}
<YieldSlot @name="success" @params={{block-params transition}}>{{yield}}</YieldSlot>
{{else if (eq state 'error') }}
<YieldSlot @name="error" @params={{block-params transition}}>{{yield}}</YieldSlot>
{{/if}}
{{#if (or permanent (eq state 'ready')) }}
<YieldSlot @name="action" @params={{block-params success error}}>{{yield}}{{message}}</YieldSlot>
{{/if}}
</div>

View File

@ -1,51 +0,0 @@
import Component from '@ember/component';
import { set } from '@ember/object';
import { inject as service } from '@ember/service';
import SlotsMixin from 'block-slots';
const STATE_READY = 'ready';
const STATE_SUCCESS = 'success';
const STATE_ERROR = 'error';
export default Component.extend(SlotsMixin, {
wait: service('timeout'),
dom: service('dom'),
transition: '',
transitionClassName: 'feedback-dialog-out',
state: STATE_READY,
permanent: true,
tagName: '',
init: function() {
this._super(...arguments);
this.success = this._success.bind(this);
this.error = this._error.bind(this);
},
applyTransition: function() {
const wait = this.wait.execute;
const className = this.transitionClassName;
// TODO: Make 0 default in wait
wait(0)
.then(() => {
set(this, 'transition', className);
return wait(0);
})
.then(() => {
return new Promise(resolve => {
this.dom
.element(`.${className}`, this.$feedback)
.addEventListener('transitionend', resolve);
});
})
.then(() => {
set(this, 'transition', '');
set(this, 'state', STATE_READY);
});
},
_success: function() {
set(this, 'state', STATE_SUCCESS);
this.applyTransition();
},
_error: function() {
set(this, 'state', STATE_ERROR);
this.applyTransition();
},
});

View File

@ -1,15 +1,18 @@
<EmberNativeScrollable {{yield}}
@tagName="ul" <EmberNativeScrollable
@content-size={{_contentSize}} @tagName="ul"
@scroll-left={{_scrollLeft}} @content-size={{_contentSize}}
@scroll-top={{_scrollTop}} @scroll-left={{_scrollLeft}}
@scrollChange={{action "scrollChange"}} @scroll-top={{_scrollTop}}
@scrollChange={{action "scrollChange"}}
@clientSizeChange={{action "clientSizeChange"}} @clientSizeChange={{action "clientSizeChange"}}
> >
<li></li> <li></li>
{{~#each _cells as |cell|~}} {{~#each _cells as |cell|~}}
<li onclick={{action 'click'}} style={{{cell.style}}} class={{if (service/exists cell.item) 'linkable' }}> <li onclick={{action 'click'}} style={{{cell.style}}} class={{if (service/exists cell.item) 'linkable'}}>
{{yield cell.item cell.index }} <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>
</li> </li>
{{~/each~}} {{~/each~}}
</EmberNativeScrollable> </EmberNativeScrollable>

View File

@ -1,18 +1,21 @@
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { computed, get } from '@ember/object'; import { computed, get, set } from '@ember/object';
import Component from 'ember-collection/components/ember-collection'; import Component from 'ember-collection/components/ember-collection';
import PercentageColumns from 'ember-collection/layouts/percentage-columns'; import PercentageColumns from 'ember-collection/layouts/percentage-columns';
import style from 'ember-computed-style'; import style from 'ember-computed-style';
import Slotted from 'block-slots';
import WithResizing from 'consul-ui/mixins/with-resizing'; import WithResizing from 'consul-ui/mixins/with-resizing';
export default Component.extend(WithResizing, { const formatItemStyle = PercentageColumns.prototype.formatItemStyle;
export default Component.extend(Slotted, WithResizing, {
dom: service('dom'), dom: service('dom'),
tagName: 'div', tagName: 'div',
attributeBindings: ['style'], attributeBindings: ['style'],
height: 500, height: 500,
cellHeight: 73, cellHeight: 70,
style: style('getStyle'), style: style('getStyle'),
classNames: ['list-collection'], classNames: ['list-collection'],
checked: null,
init: function() { init: function() {
this._super(...arguments); this._super(...arguments);
this.columns = [100]; this.columns = [100];
@ -24,6 +27,14 @@ export default Component.extend(WithResizing, {
get(this, 'columns'), get(this, 'columns'),
get(this, 'cellHeight') get(this, 'cellHeight')
); );
const o = this;
this['cell-layout'].formatItemStyle = function(itemIndex) {
let style = formatItemStyle.apply(this, arguments);
if (o.checked === itemIndex) {
style = `${style};z-index: 1`;
}
return style;
};
}, },
getStyle: computed('height', function() { getStyle: computed('height', function() {
return { return {
@ -50,5 +61,31 @@ export default Component.extend(WithResizing, {
click: function(e) { click: function(e) {
return this.dom.clickFirstAnchor(e, '.list-collection > ul > li'); return this.dom.clickFirstAnchor(e, '.list-collection > ul > li');
}, },
change: function(index, e = {}) {
if (e.target.checked && index != get(this, 'checked')) {
set(this, 'checked', parseInt(index));
this.$row = this.dom.closest('li', e.target);
this.$row.style.zIndex = 1;
const $group = this.dom.sibling(e.target, 'div');
const groupRect = $group.getBoundingClientRect();
const groupBottom = groupRect.top + $group.clientHeight;
const $footer = this.dom.element('footer[role="contentinfo"]');
const footerRect = $footer.getBoundingClientRect();
const footerTop = footerRect.top;
if (groupBottom > footerTop) {
$group.classList.add('above');
} else {
$group.classList.remove('above');
}
} else {
const $group = this.dom.sibling(e.target, 'div');
$group.classList.remove('above');
set(this, 'checked', null);
this.$row.style.zIndex = null;
}
},
}, },
}); });

View File

@ -1,3 +1,11 @@
<EmberTooltip @popperContainer=".app-view" @side="top-start"> <EmberTooltip
@targetId={{targetId}}
@onHide={{oncomplete}}
@isShown={{isShown}}
@duration={{duration}}
@event={{if isShown 'none' (or event 'hover')}}
@popperContainer=".app-view"
@side={{or position 'top'}}
>
{{yield}} {{yield}}
</EmberTooltip> </EmberTooltip>

View File

@ -6,9 +6,4 @@ export default Controller.extend({
replace: true, replace: true,
}, },
}, },
actions: {
sendClone: function(item) {
this.send('clone', item);
},
},
}); });

View File

@ -0,0 +1,30 @@
import { helper } from '@ember/component/helper';
import { get } from '@ember/object';
const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001';
export default helper(function policyGroup([items] /*, hash*/) {
return items.reduce(
function(prev, item) {
let group;
switch (true) {
case get(item, 'ID') === MANAGEMENT_ID:
group = 'management';
break;
case get(item, 'template') !== '':
group = 'identities';
break;
default:
group = 'policies';
break;
}
prev[group].push(item);
return prev;
},
{
management: [],
identities: [],
policies: [],
}
);
});

View File

@ -10,6 +10,8 @@ export default Route.extend({
const dc = this.modelFor('dc').dc.Name; const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1); const nspace = this.modelFor('nspace').nspace.substr(1);
return hash({ return hash({
dc: dc,
nspace: nspace || 'default',
item: this.repo.findInstanceBySlug(params.id, params.node, params.name, dc, nspace), item: this.repo.findInstanceBySlug(params.id, params.node, params.name, dc, nspace),
}).then(model => { }).then(model => {
// this will not be run in a blocking loop, but this is ok as // this will not be run in a blocking loop, but this is ok as

View File

@ -13,10 +13,10 @@ export default Route.extend({
const dc = this.modelFor('dc').dc.Name; const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1); const nspace = this.modelFor('nspace').nspace.substr(1);
return hash({ return hash({
item: this.repo.findBySlug(params.name, dc, nspace),
urls: this.settings.findBySlug('urls'),
dc: dc, dc: dc,
nspace: nspace || 'default', nspace: nspace || 'default',
item: this.repo.findBySlug(params.name, dc, nspace),
urls: this.settings.findBySlug('urls'),
proxies: [], proxies: [],
}) })
.then(model => { .then(model => {

View File

@ -18,7 +18,7 @@
@extend %more-popover-menu-panel; @extend %more-popover-menu-panel;
} }
%more-popover-menu-panel:not(.above) { %more-popover-menu-panel:not(.above) {
top: 38px; top: 48px;
} }
%more-popover-menu-panel:not(.left) { %more-popover-menu-panel:not(.left) {
right: 10px; right: 10px;

View File

@ -56,24 +56,6 @@
padding: 50px; padding: 50px;
text-align: center; text-align: center;
} }
/* this should probably be its own component */
%app-view-content div > dl {
position: relative;
}
%app-view-content div > dl > dt {
position: absolute;
}
%app-view-content div > dl > dt {
width: 140px;
}
%app-view-content div > dl > dd {
padding-left: 140px;
}
%app-view-content div > dl > * {
min-height: 1em;
margin-bottom: 0.4em;
}
/* */
/* TODO: Think about an %app-form or similar */ /* TODO: Think about an %app-form or similar */
%app-view-content form:not(.filter-bar) fieldset { %app-view-content form:not(.filter-bar) fieldset {
padding-bottom: 0.3em; padding-bottom: 0.3em;

View File

@ -9,9 +9,14 @@
%composite-row.linkable, %composite-row.linkable,
.consul-gateway-service-list > ul > li:not(:first-child), .consul-gateway-service-list > ul > li:not(:first-child),
.consul-service-instance-list > ul > li:not(:first-child), .consul-service-instance-list > ul > li:not(:first-child),
.consul-service-list > ul > li:not(:first-child) { .consul-service-list > ul > li:not(:first-child),
.consul-token-list > ul > li:not(:first-child) {
@extend %with-composite-row-intent; @extend %with-composite-row-intent;
} }
/* TODO: the service list has a 1px offset */
.consul-service-list li > div:first-child > dl:first-child dd {
margin-top: 1px;
}
.proxy-exposed-paths tbody tr { .proxy-exposed-paths tbody tr {
cursor: default !important; cursor: default !important;
} }

View File

@ -5,11 +5,15 @@
%with-composite-row-intent:active { %with-composite-row-intent:active {
@extend %composite-row-intent; @extend %composite-row-intent;
} }
%composite-row > a, %composite-row > :first-child {
%composite-row > p,
%composite-row > li > * {
@extend %composite-row-header; @extend %composite-row-header;
} }
%composite-row > ul { %composite-row-header > dl:first-child {
@extend %composite-row-icon;
}
%composite-row > :nth-child(2) {
@extend %composite-row-detail; @extend %composite-row-detail;
} }
%composite-row > :nth-child(3) {
@extend %composite-row-actions;
}

View File

@ -1,66 +1,96 @@
%composite-row { %composite-row {
display: block; display: grid;
box-sizing: border-box; grid-template-columns: auto 50px;
padding: 12px; grid-template-rows: 50% 50%;
padding-right: 0;
border: 1px solid; grid-template-areas:
'header actions'
'detail actions';
padding-top: 10px;
padding-bottom: 10px;
/* whilst this isn't in the designs this makes our temporary rollover look better */
padding-left: 12px;
} }
%composite-row-header { %composite-row-header {
margin-bottom: 0 !important; grid-area: header;
} align-self: start;
%composite-row-intent {
border: 1px solid;
position: relative;
} }
%composite-row-detail { %composite-row-detail {
display: flex; grid-area: detail;
align-self: end;
}
%composite-row-detail:not(:last-child) {
overflow-x: hidden;
}
%composite-row-actions {
grid-area: actions;
justify-self: center;
align-self: center;
}
%composite-row-icon {
margin-right: 10px;
}
%composite-row-icon dt {
display: none;
}
%composite-row-icon dd::before {
font-size: 0.9em;
}
/* TODO Currently only here due to dl's in %form-row */
%composite-row dl {
margin: 0;
padding: 0;
}
%composite-row-detail,
%composite-row-detail ul,
%composite-row-detail dl,
%composite-row-header,
%composite-row-header dl {
display: inline-flex;
flex-wrap: nowrap; flex-wrap: nowrap;
} }
%composite-row-detail dt {
display: none;
}
%composite-row-header *,
%composite-row-detail * { %composite-row-detail * {
white-space: nowrap; white-space: nowrap;
flex-wrap: nowrap;
} }
%composite-row-detail > li { %composite-row-detail > dl,
%composite-row-detail > ul > li {
margin-right: 12px; margin-right: 12px;
} }
%composite-row-detail .node::before,
%composite-row-detail .tag-list::before {
position: relative;
}
%composite-row-detail .node::before { %composite-row-detail .node::before {
margin-top: 2px; top: 1px;
}
%composite-row-detail .tag-list::before {
top: 2px;
} }
//Health Checks // Copy Button
%composite-row > li > span.passing::before,
%composite-row > li > span.warning::before,
%composite-row > li > span.critical::before,
%composite-row > li > span.empty::before {
height: 18px;
width: 18px;
margin-top: 2px;
}
// Copy Button with Feedback
%composite-row .copy-button button { %composite-row .copy-button button {
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0 !important;
} }
%composite-row-detail .copy-button { %composite-row .copy-button {
margin-left: 4px; margin-left: 4px;
} }
%composite-row-header .copy-button { %composite-row .copy-button {
top: 3px;
}
%composite-row-header .copy-button,
%composite-row-detail .copy-button {
display: none;
}
%composite-row-header:hover .copy-button,
%composite-row-detail:hover .copy-button {
display: inline-flex; display: inline-flex;
} }
/* buttons need to be displayed in order for the tooltip */
// Tooltip /* to track them */
%composite-row-detail .feedback-dialog-out { %composite-row-header .copy-button button,
left: -12px; %composite-row-detail .copy-button button {
bottom: 12px; opacity: 0;
} }
%composite-row-detail .feedback-dialog-out::after { %composite-row-header:hover .copy-button button,
left: 18px; %composite-row-detail:hover .copy-button button {
opacity: 1;
} }

View File

@ -1,5 +1,6 @@
%composite-row { %composite-row {
list-style-type: none; list-style-type: none;
border: 1px solid;
border-top-color: $transparent; border-top-color: $transparent;
border-bottom-color: $gray-200; border-bottom-color: $gray-200;
border-right-color: $transparent; border-right-color: $transparent;
@ -14,28 +15,54 @@
%composite-row-header { %composite-row-header {
color: $black; color: $black;
} }
%composite-row-header * {
color: inherit;
}
%composite-row-detail { %composite-row-detail {
color: $gray-500; color: $gray-500;
} }
%composite-row-detail .policy::before {
@extend %with-file-fill-mask, %as-pseudo;
background-color: $gray-500;
margin-right: 3px;
}
%composite-row-detail .role::before {
@extend %with-user-plain-mask, %as-pseudo;
background-color: $gray-500;
margin-right: 3px;
}
%composite-row-detail .policy-management::before {
@extend %with-star-fill-mask, %as-pseudo;
background-color: var(--brand-600);
margin-right: 3px;
}
// Health Checks // Health Checks
%composite-row .passing::before { %composite-row-detail li.passing::before,
%composite-row-header .passing dd::before {
@extend %with-check-circle-fill-color-mask, %as-pseudo; @extend %with-check-circle-fill-color-mask, %as-pseudo;
background-color: $green-500; background-color: $green-500;
} }
%composite-row .warning::before { %composite-row-detail li.warning::before,
%composite-row-header .warning dd::before {
@extend %with-alert-triangle-color-mask, %as-pseudo; @extend %with-alert-triangle-color-mask, %as-pseudo;
background-color: $orange-500; background-color: $orange-500;
} }
%composite-row .critical::before { %composite-row-detail li.critical::before,
%composite-row-header .critical dd::before {
@extend %with-cancel-square-fill-color-mask, %as-pseudo; @extend %with-cancel-square-fill-color-mask, %as-pseudo;
background-color: $red-500; background-color: $red-500;
} }
%composite-row .empty::before { %composite-row-detail li.empty::before,
%composite-row-header .empty dd::before {
@extend %with-minus-square-fill-color-mask, %as-pseudo; @extend %with-minus-square-fill-color-mask, %as-pseudo;
background-color: $gray-500; background-color: $gray-500;
} }
%composite-row-header [rel='me'] dd::before {
@extend %with-check-circle-fill-mask, %as-pseudo;
background-color: $blue-500;
}
// Metadata // Metadata
%composite-row .node a { %composite-row .node a {
color: $gray-500; color: $gray-500;

View File

@ -0,0 +1,3 @@
.confirmation-alert {
@extend %confirmation-alert;
}

View File

@ -0,0 +1,4 @@
@import './definition-table/index';
.definition-table {
@extend %definition-table;
}

View File

@ -1 +1,2 @@
@import './skin';
@import './layout'; @import './layout';

View File

@ -0,0 +1,16 @@
%definition-table > dl {
position: relative;
}
%definition-table > dl > dt {
position: absolute;
}
%definition-table > dl > dt {
width: 140px;
}
%definition-table > dl > dd {
padding-left: 140px;
}
%definition-table > dl > * {
min-height: 1em;
margin-bottom: 0.4em;
}

View File

@ -0,0 +1,2 @@
%definition-table {
}

View File

@ -1,20 +0,0 @@
@import './feedback-dialog/index';
.with-feedback {
@extend %feedback-dialog-inline;
}
%feedback-dialog-inline .feedback-dialog-out {
@extend %blink-in-fade-out;
transition-delay: 3s;
}
@media #{$--lt-spacious-page-header} {
.actions .with-feedback p {
bottom: auto;
top: 0px;
}
.actions .with-feedback p::after {
bottom: auto;
top: -13px !important;
border-bottom-width: 18px;
border-top-width: 0;
}
}

View File

@ -1,12 +0,0 @@
%feedback-dialog-inline {
@extend %tooltip;
}
%feedback-dialog-inline p::after {
@extend %tooltip-tail;
top: auto !important;
bottom: -13px;
}
%feedback-dialog-inline p {
@extend %tooltip-bubble;
}

View File

@ -61,7 +61,7 @@
%healthcheck-output pre code { %healthcheck-output pre code {
word-wrap: break-word; word-wrap: break-word;
} }
%healthcheck-output .with-feedback { %healthcheck-output .copy-button {
position: absolute; position: absolute;
right: 0.5em; right: 0.5em;
top: 1em; top: 1em;

View File

@ -20,7 +20,6 @@
@import './flash-message'; @import './flash-message';
@import './code-editor'; @import './code-editor';
@import './confirmation-dialog'; @import './confirmation-dialog';
@import './feedback-dialog';
@import './modal-dialog'; @import './modal-dialog';
@import './auth-form'; @import './auth-form';
@import './auth-modal'; @import './auth-modal';
@ -35,6 +34,9 @@
@import './popover-select'; @import './popover-select';
@import './tooltip-panel'; @import './tooltip-panel';
@import './menu-panel'; @import './menu-panel';
@import './more-popover-menu';
@import './confirmation-alert';
@import './definition-table';
/**/ /**/

View File

@ -0,0 +1,18 @@
.more-popover-menu > [type='checkbox'] {
@extend %more-popover-menu;
}
%more-popover-menu-panel [type='checkbox']:checked ~ * {
/* this needs to autocalculate */
min-height: 143px;
max-height: 143px;
}
%more-popover-menu-panel [id$='logout']:checked ~ * {
/* this needs to autocalculate */
min-height: 183px;
max-height: 183px;
}
%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'] {
display: block;
}

View File

@ -21,9 +21,6 @@ td strong {
%pill.policy-management::before { %pill.policy-management::before {
@extend %with-star-icon; @extend %with-star-icon;
} }
%pill.policy-service-identity::before {
@extend %with-service-identity-icon;
}
%pill.role::before { %pill.role::before {
@extend %with-user-plain-icon; @extend %with-user-plain-icon;
opacity: 0.3; opacity: 0.3;
@ -33,16 +30,16 @@ td strong {
// All of this icon assigning stuff should probably go in the eventual // All of this icon assigning stuff should probably go in the eventual
// refactored /components/icons.scss file // refactored /components/icons.scss file
td.policy-service-identity a::after { span.policy-management a::after {
@extend %with-service-identity-icon, %as-pseudo;
margin-left: 3px;
}
td.policy-management a::after {
@extend %with-star-icon, %as-pseudo; @extend %with-star-icon, %as-pseudo;
margin-left: 3px; margin-left: 3px;
} }
span.policy-service-identity,
.consul-external-source, .consul-external-source,
.consul-kind { .consul-kind {
@extend %reduced-pill; @extend %reduced-pill;
} }
span.policy-service-identity::before {
width: 0;
}

View File

@ -4,21 +4,6 @@
%table-actions > [type='checkbox'] { %table-actions > [type='checkbox'] {
@extend %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-panel [id$='logout']:checked ~ * {
/* this needs to autocalculate */
min-height: 183px;
max-height: 183px;
}
%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'] {
display: block;
}
%table-actions .confirmation-alert { %table-actions .confirmation-alert {
@extend %confirmation-alert; @extend %confirmation-alert;
} }
@ -27,6 +12,9 @@
top: 8px; top: 8px;
right: 15px; right: 15px;
} }
%table-actions .menu-panel:not(.above) {
top: 38px !important;
}
/*TODO: Rename this to %app-view-brand-icon or similar */ /*TODO: Rename this to %app-view-brand-icon or similar */
%with-external-source-icon { %with-external-source-icon {

View File

@ -69,21 +69,12 @@ table.has-actions tr > *:nth-last-child(5):first-child ~ * {
} }
/*TODO: trs only live in tables, get rid of table */ /*TODO: trs only live in tables, get rid of table */
html.template-service.template-list main table tr {
@extend %services-row;
}
html.template-nspace.has-acls.template-list main table tr { html.template-nspace.has-acls.template-list main table tr {
@extend %with-acls-nspaces-row; @extend %with-acls-nspaces-row;
} }
html.template-nspace:not(.has-acls).template-list main table tr { html.template-nspace:not(.has-acls).template-list main table tr {
@extend %nspaces-row; @extend %nspaces-row;
} }
html.template-service.template-show #instances table tr {
@extend %instances-row;
}
html.template-token.template-list main table tr {
@extend %tokens-row;
}
html.template-role.template-list main table tr { html.template-role.template-list main table tr {
@extend %roles-row; @extend %roles-row;
} }
@ -93,66 +84,18 @@ html.template-role.template-edit [role='dialog'] table tr,
html.template-role.template-edit main table.token-list tr { html.template-role.template-edit main table.token-list tr {
@extend %tokens-minimal-row; @extend %tokens-minimal-row;
} }
html.template-token.template-list main table tr td.me,
html.template-token.template-list main table tr td.me ~ td,
html.template-token.template-list main table tr th {
@extend %tokens-your-row;
}
html.template-node.template-show main table.sessions tr { html.template-node.template-show main table.sessions tr {
@extend %node-sessions-row; @extend %node-sessions-row;
} }
html.template-instance.template-show main table.exposedpaths tr {
@extend %instance-paths-row;
}
// this will get auto calculated later in tabular-collection.js // this will get auto calculated later in tabular-collection.js
// keeping this here for reference // keeping this here for reference
// %services-row > * { // %services-row > * {
// (100% / 2) - (160px / 2) // (100% / 2) - (160px / 2)
// width: calc(50% - 160px); // width: calc(50% - 160px);
// } // }
%services-row > *:nth-child(2) { %tokens-minimal-row > *:not(last-child) {
width: 100px;
}
%services-row > * {
width: auto;
}
%instances-row > * {
width: calc(100% / 5);
}
// instance-paths are for exposed paths
// we make the columns that need as much space as possible
// as wide as possible so 50% each minus enough room
// for the 3 port columns - we probably need a max of 55px
// for each port column so 55 * 3 = 165
// so column 1 and 5 are 50% - 165 each
// the 3 remaining columns split the 165 thats left between them
%instance-paths-row > *:nth-child(1),
%instance-paths-row > *:nth-child(5) {
width: calc(50% - 165px) !important;
}
%instance-paths-row > *:nth-child(2),
%instance-paths-row > *:nth-child(3),
%instance-paths-row > *:nth-child(4) {
width: 110px !important;
}
%tokens-row > *:first-child,
%tokens-minimal-row > *:not(last-child),
%tokens-row > *:nth-child(2),
%tokens-your-row:nth-last-child(2) {
width: 120px; width: 120px;
} }
%tokens-row > *:nth-child(3) {
width: calc(30% - 150px);
}
%tokens-row > *:nth-child(4) {
width: calc(70% - 150px);
}
%tokens-your-row:nth-child(4) {
width: calc(70% - 270px) !important;
}
%tokens-row > *:last-child {
@extend %table-actions;
}
%tokens-minimal-row > *:last-child { %tokens-minimal-row > *:last-child {
width: calc(100% - 240px) !important; width: calc(100% - 240px) !important;
} }
@ -204,19 +147,3 @@ html.template-instance.template-show main table.exposedpaths tr {
display: none; display: none;
} }
} }
@media #{$--lt-medium-table} {
/* Token > Policies */
/* Token > Your Token */
html.template-token.template-list tr > :nth-child(2),
html.template-token.template-list tr > :nth-child(4),
html.template-token.template-list tr th:nth-child(5),
html.template-token.template-list main table tr td.me ~ td:nth-of-type(5) {
display: none;
}
html.template-service.template-show #instances tr > :nth-child(3) {
display: none;
}
%instances-row > * {
width: calc(100% / 4);
}
}

View File

@ -1,3 +1,6 @@
%tag-list {
white-space: nowrap;
}
%tag-list dt { %tag-list dt {
display: none; display: none;
} }
@ -11,10 +14,11 @@
} }
%tag-list dd { %tag-list dd {
display: inline-flex; display: inline-flex;
flex-wrap: wrap;
padding-left: 0px; padding-left: 0px;
} }
%tag-list dd > *:not(:last-child)::after { %tag-list dd > *:not(:last-child)::after {
content: ', '; content: ',';
white-space: pre; margin-right: 0.3em;
display: inline; display: inline;
} }

View File

@ -117,6 +117,10 @@ pre code,
font-size: $typo-size-450; font-size: $typo-size-450;
font-weight: $typo-weight-medium; font-weight: $typo-weight-medium;
} }
%composite-row-header *:not(button) {
font-size: inherit;
font-weight: inherit;
}
/**/ /**/
/* TODO: We have nothing else with a 500 */ /* TODO: We have nothing else with a 500 */

View File

@ -4,17 +4,6 @@
cursor: pointer; cursor: pointer;
float: right; float: right;
} }
%token-yours {
color: $blue-500;
}
%token-yours::before {
@extend %with-check-circle-fill-mask, %as-pseudo;
background-color: $blue-500;
margin-right: 5px;
}
.me ~ :nth-last-child(2) {
@extend %token-yours;
}
.template-token.template-edit main dd { .template-token.template-edit main dd {
display: flex; display: flex;
} }

View File

@ -66,15 +66,15 @@
<p class="notice info"><strong>Update.</strong> We have upgraded our ACL system by allowing you to create reusable policies which you can then apply to tokens. Don't worry, even though this token was written in the old style, it is still valid. However, we do recommend upgrading your old tokens to the new style. Learn how in our <a href="{{env 'CONSUL_DOCS_URL'}}/guides/acl-migrate-tokens.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p> <p class="notice info"><strong>Update.</strong> We have upgraded our ACL system by allowing you to create reusable policies which you can then apply to tokens. Don't worry, even though this token was written in the old style, it is still valid. However, we do recommend upgrading your old tokens to the new style. Learn how in our <a href="{{env 'CONSUL_DOCS_URL'}}/guides/acl-migrate-tokens.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p>
{{/if}} {{/if}}
{{#if (not create) }} {{#if (not create) }}
<div> <div class="definition-table">
<dl> <dl>
<dt>AccessorID</dt> <dt>AccessorID</dt>
<dd> <dd>
<CopyButton @value={{item.AccessorID}} @name="AccessorID" /> {{item.AccessorID}} <CopyButton @value={{item.AccessorID}} @name="AccessorID" @position="top-start" /> {{item.AccessorID}}
</dd> </dd>
<dt>Token</dt> <dt>Token</dt>
<dd> <dd>
<CopyButton @value={{item.SecretID}} @name="Token" /> <SecretButton>{{item.SecretID}}</SecretButton> <CopyButton @value={{item.SecretID}} @name="Token" @position="top-start" /> <SecretButton>{{item.SecretID}}</SecretButton>
</dd> </dd>
{{#if (and (not (token/is-legacy item)) (not create))}} {{#if (and (not (token/is-legacy item)) (not create))}}
<dt>Scope</dt> <dt>Scope</dt>

View File

@ -43,151 +43,36 @@
{{/if}} {{/if}}
<ChangeableSet @dispatcher={{searchable 'token' items}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'token' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="set" as |filtered|>
<TabularCollection @items={{sort-by "CreateTime:desc" filtered}} as |item index|> <ConsulTokenList
<BlockSlot @name="header"> @items={{sort-by "CreateTime:desc" filtered}}
<th>Accessor ID</th> @token={{token}}
<th>Scope</th> @onuse={{action send 'use'}}
<th>Description</th> @ondelete={{action send 'delete'}}
<th>Roles &amp; Policies</th> @onlogout={{action send 'logout'}}
<th>&nbsp;</th> @onclone={{action send 'clone'}}
</BlockSlot> />
<BlockSlot @name="row">
<td data-test-token="{{item.AccessorID}}" class={{if (eq item.AccessorID token.AccessorID) 'me' }}>
<a href={{href-to 'dc.acls.tokens.edit' item.AccessorID}}>{{substr item.AccessorID -8}}</a>
</td>
<td>
{{if item.Local 'local' 'global' }}
</td>
<td data-test-description>
<p>{{default item.Description item.Name}}</p>
</td>
<td colspan={{if (not-eq item.AccessorID token.AccessorID) '2' }}>
{{#if (token/is-legacy item) }}
<p>Legacy tokens have embedded rules.</p>
{{ else }}
{{#each (append item.Policies item.Roles) as |item|}}
<strong data-test-policy class={{policy/typeof item}}>{{item.Name}}</strong>
{{/each}}
{{/if}}
</td>
{{#if (eq item.AccessorID token.AccessorID)}}
<td>Your token</td>
{{/if}}
</BlockSlot>
<BlockSlot @name="actions" as |index change checked|>
<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 'sendClone' 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 send 'logout' 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 send 'use' 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 send 'delete' item}}>Delete</button>
</li>
<li>
<label for={{concat confirm 'delete'}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
{{/unless}}
</BlockSlot>
</PopoverMenu>
</BlockSlot>
</TabularCollection>
</BlockSlot> </BlockSlot>
<BlockSlot @name="empty"> <BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}> <EmptyState @allowLogin={{true}}>
<BlockSlot @name="header"> <BlockSlot @name="header">
<h2> <h2>
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
No tokens found No tokens found
{{else}} {{else}}
Welcome to ACL Tokens Welcome to ACL Tokens
{{/if}} {{/if}}
</h2> </h2>
</BlockSlot> </BlockSlot>
<BlockSlot @name="body"> <BlockSlot @name="body">
<p> <p>
{{#if (gt items.length 0)}} {{#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. No tokens where found matching that search, or you may not have access to view the tokens you are searching for.
{{else}} {{else}}
There don't seem to be any tokens, or you may not have access to view tokens yet. There don't seem to be any tokens, or you may not have access to view tokens yet.
{{/if}} {{/if}}
</p> </p>
</BlockSlot> </BlockSlot>
</EmptyState> </EmptyState>
</BlockSlot> </BlockSlot>
</ChangeableSet> </ChangeableSet>
</BlockSlot> </BlockSlot>

View File

@ -34,7 +34,7 @@
{{/if}} {{/if}}
{{partial 'dc/kv/form'}} {{partial 'dc/kv/form'}}
{{#if session}} {{#if session}}
<div data-test-session={{session.ID}}> <div class="definition-table" data-test-session={{session.ID}}>
<h2> <h2>
<a href="{{env 'CONSUL_DOCS_URL'}}/internals/sessions.html#session-design" rel="help noopener noreferrer" target="_blank">Lock Session</a> <a href="{{env 'CONSUL_DOCS_URL'}}/internals/sessions.html#session-design" rel="help noopener noreferrer" target="_blank">Lock Session</a>
</h2> </h2>

View File

@ -2,64 +2,8 @@
<div role="tabpanel"> <div role="tabpanel">
{{#if (gt proxy.Proxy.Upstreams.length 0)}} {{#if (gt proxy.Proxy.Upstreams.length 0)}}
<section class="proxy-upstreams"> <section class="proxy-upstreams">
<h3> Upstreams</h3> <h3>Upstreams</h3>
<ul data-test-proxy-upstreams> <ConsulUpstreamInstanceList @items={{proxy.Proxy.Upstreams}} @dc={{dc}} @nspace={{nspace}} />
{{#let proxy.Datacenter as |proxyDatacenter|}}
{{#each proxy.Proxy.Upstreams as |item|}}
<li>
<p data-test-destination-name>
{{item.DestinationName}}
</p>
<ul>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (not-eq item.DestinationType 'prepared_query')}}
<li class="nspace">
<span>
<Tooltip>
Namespace
</Tooltip>
</span>
<span>
{{or item.DestinationNamespace 'default'}}
</span>
</li>
{{/if}}
{{/if}}
{{#if (and (not-eq item.Datacenter proxyDatacenter) (not-eq item.Datacenter ""))}}
<li class="datacenter">
<span>
<Tooltip>
Datacenter
</Tooltip>
</span>
<span>
{{item.Datacenter}}
</span>
</li>
{{/if}}
{{#if (gt item.LocalBindPort 0)}}
{{#let (concat (or item.LocalBindAddress '127.0.0.1') ':' item.LocalBindPort) as |combinedAddress| }}
<li class="port">
<span>
<Tooltip>
Address
</Tooltip>
</span>
<span>
<span>{{combinedAddress}}</span>
<CopyButton
@value={{combinedAddress}}
@name="Address"
/>
</span>
</li>
{{/let}}
{{/if}}
</ul>
</li>
{{/each}}
{{/let}}
</ul>
</section> </section>
{{/if}} {{/if}}
{{#if (gt proxy.Proxy.Expose.Paths.length 0)}} {{#if (gt proxy.Proxy.Expose.Paths.length 0)}}
@ -68,73 +12,7 @@
<p> <p>
The following list shows individual HTTP paths exposed through Envoy for external services like Prometheus. Read more about this in our documentation. The following list shows individual HTTP paths exposed through Envoy for external services like Prometheus. Read more about this in our documentation.
</p> </p>
<ul data-test-proxy-exposed-paths> <ConsulExposedPathList @items={{proxy.Proxy.Expose.Paths}} @address={{item.Address}} />
{{#each proxy.Proxy.Expose.Paths as |path|}}
<li>
{{#let (concat item.Address ':' path.Path) as |combinedAddress| }}
<p class="combined-address">
<span>
{{combinedAddress}}
</span>
<CopyButton
@value={{combinedAddress}}
@name="Address"
/>
</p>
{{/let}}
<ul>
{{#if path.Protocol}}
<li class="protocol">
<span>
<Tooltip>
Protocol
</Tooltip>
</span>
<span>
{{path.Protocol}}
</span>
</li>
{{/if}}
{{#if path.ListenerPort}}
<li class="port">
<span>
<Tooltip>
Port
</Tooltip>
</span>
<span>
listening on :{{path.ListenerPort}}
</span>
</li>
{{/if}}
{{#if path.LocalPathPort}}
<li class="port">
<span>
<Tooltip>
Port
</Tooltip>
</span>
<span>
local port :{{path.LocalPathPort}}
</span>
</li>
{{/if}}
{{#if path.Path}}
<li class="path">
<span>
<Tooltip>
Path
</Tooltip>
</span>
<span>
{{path.Path}}
</span>
</li>
{{/if}}
</ul>
</li>
{{/each}}
</ul>
</section> </section>
{{/if}} {{/if}}
{{#if (or (gt proxy.ServiceChecks.length 0) (gt proxy.NodeChecks.length 0))}} {{#if (or (gt proxy.ServiceChecks.length 0) (gt proxy.NodeChecks.length 0))}}

View File

@ -6,7 +6,7 @@
The following services may receive traffic from external services through this gateway. Learn more about configuring gateways in our 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.html" target="_blank" rel="noopener noreferrer">step-by-step guide</a>. <a href="{{env 'CONSUL_DOCS_URL'}}/connect/terminating_gateway.html" target="_blank" rel="noopener noreferrer">step-by-step guide</a>.
</p> </p>
<ConsulServiceList @routeName="dc.services.show" @items={{gatewayServices}} @nspace={{nspace}} /> <ConsulServiceList @items={{gatewayServices}} @nspace={{nspace}} />
</section> </section>
{{else}} {{else}}
<p> <p>

View File

@ -5,72 +5,7 @@
<p> <p>
Upstreams are services that may receive traffic from this gateway. Learn more about configuring gateways in our <a href="{{env 'CONSUL_DOCS_URL'}}/connect/ingress_gateway.html" target="_blank" rel="noopener noreferrer">documentation</a>. Upstreams are services that may receive traffic from this gateway. Learn more about configuring gateways in our <a href="{{env 'CONSUL_DOCS_URL'}}/connect/ingress_gateway.html" target="_blank" rel="noopener noreferrer">documentation</a>.
</p> </p>
{{#let item.Service.Namespace as |nspace|}} <ConsulUpstreamList @items={{gatewayServices}} @dc={{dc}} @nspace={{nspace}} />
<ListCollection @items={{gatewayServices}} class="consul-upstream-list" as |item index|>
{{#if (service/exists item)}}
{{#let item.Namespace as |gatewayNspace|}}
<li>
<span class={{service/health-checks item}}>
<Tooltip>
{{#if (eq 'critical' (service/health-checks item))}}
At least one health check on one instance is failing.
{{else if (eq 'warning' (service/health-checks item))}}
At least one health check on one instance has a warning.
{{else if (eq 'passing' (service/health-checks item))}}
All health checks are passing.
{{else}}
There are no health checks.
{{/if}}
</Tooltip>
</span>
{{#if (and (env 'CONSUL_NSPACES_ENABLED') (not-eq gatewayNspace nspace))}}
<a data-test-service-name href={{href-to 'nspace.dc.services.show' (concat '~' gatewayNspace) dc item.Name }}>
{{item.Name}}
</a>
{{else}}
<a data-test-service-name href={{href-to 'dc.services.show' item.Name}}>
{{item.Name}}
</a>
{{/if}}
</li>
{{/let}}
{{else}}
<p data-test-service-name>
{{item.Name}}
</p>
{{/if}}
<ul>
{{#if (and (env 'CONSUL_NSPACES_ENABLED') (not-eq item.Namespace nspace))}}
<li class="nspace">
<span>
<Tooltip>
Namespace
</Tooltip>
</span>
<span>
{{item.Namespace}}
</span>
</li>
{{/if}}
{{#if (not-eq item.GatewayConfig.ListenerPort 0)}}
<li class="port">
<span>
<Tooltip>
Port
</Tooltip>
</span>
<span>
<span>:{{item.GatewayConfig.ListenerPort}}</span>
<CopyButton
@value={{item.GatewayConfig.ListenerPort}}
@name="Port"
/>
</span>
</li>
{{/if}}
</ul>
</ListCollection>
{{/let}}
</section> </section>
{{else}} {{else}}
<p> <p>

View File

@ -1,4 +1,5 @@
@setupApplicationTest @setupApplicationTest
@ignore
Feature: components / copy-button Feature: components / copy-button
Background: Background:
Given 1 datacenter model with the value "dc-1" Given 1 datacenter model with the value "dc-1"
@ -22,4 +23,4 @@ Feature: components / copy-button
--- ---
Then the url should be /dc-1/nodes/node-0/health-checks Then the url should be /dc-1/nodes/node-0/health-checks
When I click ".healthcheck-output:nth-child(1) button.copy-btn" When I click ".healthcheck-output:nth-child(1) button.copy-btn"
Then I see the text "Copied output!" in ".healthcheck-output:nth-child(1) p.feedback-dialog-out" Then I copied "The output"

View File

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

View File

@ -1,30 +0,0 @@
import { module, skip, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | feedback dialog', function(hooks) {
setupRenderingTest(hooks);
skip("it doesn't render anything when used inline");
test('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
await render(hbs`{{feedback-dialog}}`);
assert.dom('*').hasText('');
// Template block usage:
await render(hbs`
{{#feedback-dialog}}
{{#block-slot name='success'}}
{{/block-slot}}
{{#block-slot name='error'}}
{{/block-slot}}
{{/feedback-dialog}}
`);
assert.dom('*').hasText('');
});
});

View File

@ -34,6 +34,7 @@ import policySelectorFactory from 'consul-ui/components/policy-selector/pageobje
import roleFormFactory from 'consul-ui/components/role-form/pageobject'; import roleFormFactory from 'consul-ui/components/role-form/pageobject';
import roleSelectorFactory from 'consul-ui/components/role-selector/pageobject'; import roleSelectorFactory from 'consul-ui/components/role-selector/pageobject';
import tokenListFactory from 'consul-ui/components/token-list/pageobject'; import tokenListFactory from 'consul-ui/components/token-list/pageobject';
import consulTokenListFactory from 'consul-ui/components/consul-token-list/pageobject';
import consulIntentionListFactory from 'consul-ui/components/consul-intention-list/pageobject'; import consulIntentionListFactory from 'consul-ui/components/consul-intention-list/pageobject';
// pages // pages
@ -86,6 +87,7 @@ const policySelector = policySelectorFactory(clickable, deletable, collection, a
const roleForm = roleFormFactory(submitable, cancelable, policySelector); const roleForm = roleFormFactory(submitable, cancelable, policySelector);
const roleSelector = roleSelectorFactory(clickable, deletable, collection, alias, roleForm); const roleSelector = roleSelectorFactory(clickable, deletable, collection, alias, roleForm);
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable); const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable);
const consulTokenList = consulTokenListFactory(collection, clickable, attribute, text, deletable);
const page = pageFactory(clickable, attribute, is, authForm); const page = pageFactory(clickable, attribute, is, authForm);
@ -131,19 +133,7 @@ export default {
), ),
// TODO: This needs a policyList // TODO: This needs a policyList
role: create(role(visitable, submitable, deletable, cancelable, policySelector, tokenList)), role: create(role(visitable, submitable, deletable, cancelable, policySelector, tokenList)),
tokens: create( tokens: create(tokens(visitable, creatable, text, consulTokenList, freetextFilter)),
tokens(
visitable,
submitable,
deletable,
creatable,
clickable,
attribute,
collection,
text,
freetextFilter
)
),
token: create( token: create(
token(visitable, submitable, deletable, cancelable, clickable, policySelector, roleSelector) token(visitable, submitable, deletable, cancelable, clickable, policySelector, roleSelector)
), ),

View File

@ -1,34 +1,9 @@
export default function( export default function(visitable, creatable, text, tokens, filter) {
visitable, return {
submitable, visit: visitable('/:dc/acls/tokens'),
deletable, update: text('[data-test-notification-update]'),
creatable, tokens: tokens(),
clickable, filter: filter(),
attribute, ...creatable(),
collection, };
text,
filter
) {
return submitable(
creatable({
visit: visitable('/:dc/acls/tokens'),
update: text('[data-test-notification-update]'),
tokens: collection(
'[data-test-tabular-row]',
deletable({
id: attribute('data-test-token', '[data-test-token]'),
description: text('[data-test-description]'),
policy: text('[data-test-policy].policy', { multiple: true }),
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]'),
})
),
filter: filter(),
})
);
} }