ui: Namespaces Redesign (#8336)

* ui: Add new consul-nspace-list component

* ui: Use new consul-nspace-list component

* Fix up other components to use linkable list-collection action

* ui: Remove some dead CSS
This commit is contained in:
John Cowen 2020-07-20 18:12:34 +01:00 committed by GitHub
parent ac629cd51e
commit 59585f71a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 176 additions and 206 deletions

View File

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

View File

@ -0,0 +1,55 @@
{{#if (gt items.length 0)}}
<ListCollection @items={{items}} @linkable={{action "isLinkable"}} class="consul-nspace-list" as |item|>
<BlockSlot @name="header">
{{#if item.DeletedAt}}
<p>
Deleting {{item.Name}}...
</p>
{{else}}
<a href={{href-to 'dc.nspaces.edit' item.Name}}>{{item.Name}}</a>
{{/if}}
</BlockSlot>
<BlockSlot @name="details">
<dl>
<dt>Description</dt>
<dd data-test-description>
{{item.Description}}
</dd>
</dl>
{{#if (env 'CONSUL_ACLS_ENABLED')}}
<ConsulTokenRulesetList @item={{item}} />
{{/if}}
</BlockSlot>
<BlockSlot @name="actions" as |Actions|>
<Actions as |Action|>
<Action data-test-edit-action @href={{href-to 'dc.nspaces.edit' item.Name}}>
<BlockSlot @name="label">
Edit
</BlockSlot>
</Action>
{{#if (not-eq item.Name 'default') }}
<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 namespace?
</p>
</BlockSlot>
<BlockSlot @name="confirm" as |Confirm|>
<Confirm>Delete</Confirm>
</BlockSlot>
</Confirmation>
</BlockSlot>
</Action>
{{/if}}
</Actions>
</BlockSlot>
</ListCollection>
{{/if}}

View File

@ -0,0 +1,10 @@
import Component from '@ember/component';
export default Component.extend({
tagName: '',
actions: {
isLinkable: function(item) {
return !item.DeletedAt;
},
},
});

View File

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

View File

@ -1,5 +1,5 @@
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}
<ListCollection @items={{items}} class="consul-service-list" as |item index|> <ListCollection @items={{items}} @linkable={{action "isLinkable"}} class="consul-service-list" as |item index|>
<BlockSlot @name="header"> <BlockSlot @name="header">
{{#let (get proxies item.Name) as |proxy|}} {{#let (get proxies item.Name) as |proxy|}}
{{#let (service/health-checks item proxy) as |health|}} {{#let (service/health-checks item proxy) as |health|}}
@ -23,6 +23,7 @@
</dl> </dl>
{{/let}} {{/let}}
{{/let}} {{/let}}
{{#if (gt item.InstanceCount 0)}}
{{#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}}
@ -36,6 +37,11 @@
{{item.Name}} {{item.Name}}
</a> </a>
{{/if}} {{/if}}
{{else}}
<p data-test-service-name>
{{item.Name}}
</p>
{{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="details"> <BlockSlot @name="details">
{{#if (and nspace (env 'CONSUL_NSPACES_ENABLED'))}} {{#if (and nspace (env 'CONSUL_NSPACES_ENABLED'))}}

View File

@ -1,5 +1,11 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { get } from '@ember/object';
export default Component.extend({ export default Component.extend({
tagName: '', tagName: '',
actions: {
isLinkable: function(item) {
return get(item, 'InstanceCount') > 0;
},
},
}); });

View File

@ -1,4 +1,4 @@
{{#let (policy/group item.Policies) as |policies|}} {{#let (policy/group (or item.Policies item.ACLs.PolicyDefaults (array))) as |policies|}}
{{#let (get policies 'management') as |management|}} {{#let (get policies 'management') as |management|}}
{{#if (gt management.length 0)}} {{#if (gt management.length 0)}}
<dl> <dl>
@ -33,7 +33,7 @@
</dd> </dd>
</dl> </dl>
{{else}} {{else}}
{{#let (append (get policies 'policies') (or item.Roles (array))) as |policies|}} {{#let (append (get policies 'policies') (or item.Roles item.ACLs.RoleDefaults (array))) as |policies|}}
{{#if (gt policies.length 0)}} {{#if (gt policies.length 0)}}
<dl> <dl>
<dt>Rules</dt> <dt>Rules</dt>

View File

@ -1,6 +1,6 @@
<ListCollection @items={{items}} class="consul-upstream-list" as |item index|> <ListCollection @items={{items}} @linkable={{action "isLinkable"}} class="consul-upstream-list" as |item index|>
<BlockSlot @name="header"> <BlockSlot @name="header">
{{#if (service/exists item)}} {{#if (gt item.InstanceCount 0)}}
<dl class={{service/health-checks item}}> <dl class={{service/health-checks item}}>
<dt> <dt>
Health Health

View File

@ -1,5 +1,11 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { get } from '@ember/object';
export default Component.extend({ export default Component.extend({
tagName: '', tagName: '',
actions: {
isLinkable: function(item) {
return get(item, 'InstanceCount') > 0;
},
},
}); });

View File

@ -10,7 +10,13 @@
> >
<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
(compute (action (or linkable (noop)) cell.item))
'linkable'
}}
>
<YieldSlot @name="header"><div>{{yield cell.item cell.index}}</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="details"><div>{{yield cell.item cell.index}}</div></YieldSlot>
<YieldSlot @name="actions" <YieldSlot @name="actions"

View File

@ -1,11 +0,0 @@
import { helper } from '@ember/component/helper';
export function serviceExists([item], hash) {
if (typeof item.InstanceCount === 'undefined') {
return false;
}
return item.InstanceCount > 0;
}
export default helper(serviceExists);

View File

@ -7,9 +7,7 @@
} }
/* hoverable rows */ /* hoverable rows */
%composite-row.linkable, %composite-row.linkable,
.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-token-list > ul > li:not(:first-child), .consul-token-list > ul > li:not(:first-child),
.consul-policy-list > ul > li:not(:first-child), .consul-policy-list > ul > li:not(:first-child),
.consul-role-list > ul > li:not(:first-child) { .consul-role-list > ul > li:not(:first-child) {
@ -17,6 +15,7 @@
} }
/*TODO: This hides the icons-less dt's in the below lists as */ /*TODO: This hides the icons-less dt's in the below lists as */
/* they don't have tooltips */ /* they don't have tooltips */
.consul-nspace-list > ul > li:not(:first-child) dt,
.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-policy-list > ul li:not(:first-child) dl:not(.datacenter) dt,
.consul-role-list > ul > li:not(:first-child) dt { .consul-role-list > ul > li:not(:first-child) dt {

View File

@ -23,16 +23,6 @@
--consul-icon: #{$consul-logo-color-svg}; --consul-icon: #{$consul-logo-color-svg};
--aws-icon: #{$aws-logo-color-svg}; --aws-icon: #{$aws-logo-color-svg};
} }
html.template-node.template-show #services td:first-child a span {
@extend %with-external-source-icon;
float: left;
margin-right: 10px;
margin-top: 2px;
}
/* This nudges the th in for the external source icons */
html.template-node.template-show #services th:first-child {
text-indent: 28px;
}
td.folder::before { td.folder::before {
@extend %with-folder-outline-mask, %as-pseudo; @extend %with-folder-outline-mask, %as-pseudo;
@ -88,22 +78,11 @@ th span em {
} }
/**/ /**/
/* ideally these would be in route css files, but left here as they */
/* accomplish the same thing (hide non-essential columns for tables) */
@media #{$--lt-medium-table} {
/* Policy > Datacenters */
html.template-policy.template-list tr > :nth-child(2) {
display: none;
}
}
@media #{$--lt-wide-table} { @media #{$--lt-wide-table} {
/* hide actions on narrow screens, you can always click in do everything from there */ /* hide actions on narrow screens, you can always click in do everything from there */
tr > .actions { tr > .actions {
display: none; display: none;
} }
html.template-node.template-show #services tr > :last-child {
display: none;
}
html.template-node.template-show #lock-sessions tr > :not(:first-child):not(:last-child) { html.template-node.template-show #lock-sessions tr > :not(:first-child):not(:last-child) {
display: none; display: none;
} }

View File

@ -69,15 +69,6 @@ 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-nspace.has-acls.template-list main table tr {
@extend %with-acls-nspaces-row;
}
html.template-nspace:not(.has-acls).template-list main table tr {
@extend %nspaces-row;
}
html.template-role.template-list main table tr {
@extend %roles-row;
}
html.template-policy.template-edit [role='dialog'] table tr, html.template-policy.template-edit [role='dialog'] table tr,
html.template-policy.template-edit main table tr, html.template-policy.template-edit main table tr,
html.template-role.template-edit [role='dialog'] table tr, html.template-role.template-edit [role='dialog'] table tr,
@ -100,23 +91,6 @@ html.template-node.template-show main table.sessions tr {
width: calc(100% - 240px) !important; width: calc(100% - 240px) !important;
} }
%roles-row > *:nth-child(1),
%roles-row > *:nth-child(2),
%with-acls-nspaces-row > *:nth-child(1),
%with-acls-nspaces-row > *:nth-child(2) {
width: calc(22% - 20px) !important;
}
%with-acls-nspaces-row > *:nth-child(3),
%roles-row > *:nth-child(3) {
width: calc(56% - 20px) !important;
}
%nspaces-row > *:nth-child(1) {
width: 30%;
}
%nspaces-row > *:nth-child(2) {
width: calc(70% - 60px);
}
@media #{$--horizontal-session-list} { @media #{$--horizontal-session-list} {
%node-sessions-row > * { %node-sessions-row > * {
// (100% / 7) - (300px / 6) - (120px / 6) // (100% / 7) - (300px / 6) - (120px / 6)

View File

@ -21,106 +21,40 @@
/> />
{{/if}} {{/if}}
<ChangeableSet @dispatcher={{searchable 'nspace' items}} @terms={{search}}> <ChangeableSet @dispatcher={{searchable 'nspace' items}} @terms={{search}}>
<BlockSlot @name="set" as |filtered|> <BlockSlot @name="content" as |filtered|>
<TabularCollection @items={{filtered}} as |item index|> <ConsulNspaceList
<BlockSlot @name="header"> @items={{filtered}}
<th>Name</th> @ondelete={{queue (action send 'delete')}}
<th>Description</th> >
{{#if (env 'CONSUL_ACLS_ENABLED')}} <EmptyState @allowLogin={{true}}>
<th>Roles &amp; Policies</th> <BlockSlot @name="header">
{{/if}} <h2>
</BlockSlot> {{#if (gt items.length 0)}}
<BlockSlot @name="row"> No namespaces found
{{#if item.DeletedAt}} {{else}}
<td class="no-actions" colspan="3"> Welcome to Namespaces
<p> {{/if}}
Deleting {{item.Name}}... </h2>
</p> </BlockSlot>
</td> <BlockSlot @name="body">
{{else}} <p>
<td data-test-namespace={{item.Name}}> {{#if (gt items.length 0)}}
<a href={{href-to 'dc.nspaces.edit' item.Name}}>{{item.Name}}</a> No namespaces where found matching that search, or you may not have access to view the namespaces you are searching for.
</td> {{else}}
<td data-test-description> There don't seem to be any namespaces, or you may not have access to view namespaces yet.
<p>{{item.Description}}</p> {{/if}}
</td> </p>
{{#if (env 'CONSUL_ACLS_ENABLED')}} </BlockSlot>
<td> <BlockSlot @name="actions">
{{#each (compact (append item.ACLs.PolicyDefaults item.ACLs.RoleDefaults)) as |item|}} <li class="docs-link">
<strong data-test-policy class={{policy/typeof item}}>{{item.Name}}</strong> <a href="{{env 'CONSUL_DOCS_URL'}}/commands/namespace" rel="noopener noreferrer" target="_blank">Documentation on namespaces</a>
{{/each}} </li>
</td> <li class="learn-link">
{{/if}} <a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/namespaces/secure-namespaces" rel="noopener noreferrer" target="_blank">Read the guide</a>
{{/if}} </li>
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions" as |index change checked|> </EmptyState>
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}> </ConsulNspaceList>
<BlockSlot @name="trigger">
More
</BlockSlot>
<BlockSlot @name="menu" as |confirm send keypressClick clickTrigger|>
<li role="none">
<a data-test-edit role="menuitem" tabindex="-1" href={{href-to 'dc.nspaces.edit' item.Name}}>Edit</a>
</li>
{{#if (not-eq item.Name 'default') }}
<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 key?
</p>
</div>
<ul>
<li class="dangerous">
<button tabindex="-1" type="button" class="type-delete" onclick={{queue (action send 'delete' item) (queue clickTrigger)}}>Delete</button>
</li>
<li>
<label for={{confirm}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
{{/if}}
</BlockSlot>
</PopoverMenu>
</BlockSlot>
</TabularCollection>
</BlockSlot>
<BlockSlot @name="empty">
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>
{{#if (gt items.length 0)}}
No namespaces found
{{else}}
Welcome to Namespaces
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
{{#if (gt items.length 0)}}
No namespaces where found matching that search, or you may not have access to view the namespaces you are searching for.
{{else}}
There don't seem to be any namespaces, or you may not have access to view namespaces yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}/commands/namespace" rel="noopener noreferrer" target="_blank">Documentation on namespaces</a>
</li>
<li class="learn-link">
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/namespaces/secure-namespaces" rel="noopener noreferrer" target="_blank">Read the guide</a>
</li>
</BlockSlot>
</EmptyState>
</BlockSlot> </BlockSlot>
</ChangeableSet> </ChangeableSet>
</BlockSlot> </BlockSlot>

View File

@ -5,7 +5,10 @@
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" target="_blank" rel="noopener noreferrer">step-by-step guide</a>. <a href="{{env 'CONSUL_DOCS_URL'}}/connect/terminating-gateway" target="_blank" rel="noopener noreferrer">step-by-step guide</a>.
</p> </p>
<ConsulServiceList @items={{gatewayServices}} @nspace={{nspace}} /> <ConsulServiceList
@items={{gatewayServices}}
@nspace={{nspace}}
/>
{{else}} {{else}}
<EmptyState> <EmptyState>
<BlockSlot @name="body"> <BlockSlot @name="body">

View File

@ -1,17 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Helper | service/exists', function(hooks) {
setupRenderingTest(hooks);
// Replace this with your real tests.
test('it renders', async function(assert) {
this.set('inputValue', { InstanceCount: 3 });
await render(hbs`{{service/exists inputValue}}`);
assert.equal(this.element.textContent.trim(), 'true');
});
});

View File

@ -41,6 +41,7 @@ import consulTokenListFactory from 'consul-ui/components/consul-token-list/pageo
import consulRoleListFactory from 'consul-ui/components/consul-role-list/pageobject'; import consulRoleListFactory from 'consul-ui/components/consul-role-list/pageobject';
import consulPolicyListFactory from 'consul-ui/components/consul-policy-list/pageobject'; import consulPolicyListFactory from 'consul-ui/components/consul-policy-list/pageobject';
import consulIntentionListFactory from 'consul-ui/components/consul-intention-list/pageobject'; import consulIntentionListFactory from 'consul-ui/components/consul-intention-list/pageobject';
import consulNspaceListFactory from 'consul-ui/components/consul-nspace-list/pageobject';
import consulKvListFactory from 'consul-ui/components/consul-kv-list/pageobject'; import consulKvListFactory from 'consul-ui/components/consul-kv-list/pageobject';
// pages // pages
@ -97,6 +98,13 @@ const morePopoverMenu = morePopoverMenuFactory(clickable);
const popoverSelect = popoverSelectFactory(clickable, collection); const popoverSelect = popoverSelectFactory(clickable, collection);
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable); const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable);
const consulNspaceList = consulNspaceListFactory(
collection,
clickable,
attribute,
text,
morePopoverMenu
);
const consulKvList = consulKvListFactory(collection, clickable, attribute, deletable); const consulKvList = consulKvListFactory(collection, clickable, attribute, deletable);
const consulTokenList = consulTokenListFactory( const consulTokenList = consulTokenListFactory(
collection, collection,
@ -166,9 +174,7 @@ export default {
), ),
intentions: create(intentions(visitable, creatable, consulIntentionList, intentionFilter)), intentions: create(intentions(visitable, creatable, consulIntentionList, intentionFilter)),
intention: create(intention(visitable, submitable, deletable, cancelable)), intention: create(intention(visitable, submitable, deletable, cancelable)),
nspaces: create( nspaces: create(nspaces(visitable, creatable, consulNspaceList, freetextFilter)),
nspaces(visitable, deletable, creatable, clickable, attribute, collection, text, freetextFilter)
),
nspace: create( nspace: create(
nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector) nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector)
), ),

View File

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