ui: L7 intentions improvements (#8851)

* Disable source as well as destination on editing

* Various visual/textual amends

* Make errors only appear once you've interacted with a field

* Move tests that involve selecting menus to a create form

* Revert fieldsets and checkboxes
This commit is contained in:
John Cowen 2020-10-08 16:02:31 +01:00 committed by GitHub
parent 09fbe303b2
commit b20f77748d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 292 additions and 120 deletions

View file

@ -9,6 +9,7 @@
<label data-test-source-element class="type-select{{if item.error.SourceName ' has-error'}}">
<span>Source Service</span>
<PowerSelectWithCreate
@disabled={{not create}}
@options={{services}}
@searchField="Name"
@selected={{SourceName}}
@ -23,14 +24,16 @@
{{service.Name}}
{{/if}}
</PowerSelectWithCreate>
{{#if create}}
<em>Search for an existing service, or enter any Service name.</em>
{{/if}}
</label>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
<label data-test-source-nspace class="type-select{{if item.error.SourceNS ' has-error'}}">
<span>Source Namespace</span>
<PowerSelectWithCreate
@disabled={{not create}}
@options={{nspaces}}
@searchField="Name"
@selected={{SourceNS}}
@searchPlaceholder="Type namespace name"
@buildSuggestion={{action "createNewLabel" "Use a Consul Namespace called '{{term}}'"}}
@ -43,9 +46,9 @@
{{nspace.Name}}
{{/if}}
</PowerSelectWithCreate>
{{#if create}}
<em>Search for an existing namespace, or enter any Namespace name.</em>
{{/if}}
{{#if create}}
<em>Search for an existing namespace, or enter any Namespace name.</em>
{{/if}}
</label>
{{/if}}
</fieldset>
@ -69,9 +72,9 @@
{{service.Name}}
{{/if}}
</PowerSelectWithCreate>
{{#if create}}
<em>Search for an existing service, or enter any Service name.</em>
{{/if}}
{{#if create}}
<em>Search for an existing service, or enter any Service name.</em>
{{/if}}
</label>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
<label data-test-destination-nspace class="type-select{{if item.error.DestinationNS ' has-error'}}">

View file

@ -1,2 +1,11 @@
%consul-intention-fieldsets {
.consul-intention-fieldsets {
.value-allow > :last-child::before {
@extend %with-arrow-right-color-icon, %as-pseudo;
}
.value-deny > :last-child::before {
@extend %with-deny-color-icon, %as-pseudo;
}
.value- > :last-child::before {
@extend %with-layers-mask, %as-pseudo;
}
}

View file

@ -2,6 +2,10 @@
...attributes
class="consul-intention-permission-form"
>
<FormGroup
@name={{name}}
as |group|>
{{yield (hash
submit=(action 'submit' changeset)
reset=(action 'reset' changeset)
@ -35,35 +39,45 @@
<h2>Path</h2>
</header>
<div>
<label class="type-select">
<span>Path Type</span>
<PowerSelect
@options={{pathTypes}}
@selected={{pathType}}
@onChange={{action 'change' 'HTTP.PathType' changeset}} as |Type|>
{{get pathLabels Type}}
</PowerSelect>
</label>
<group.Element
@name="PathType"
@type="select"
as |el|>
<el.Label>
Path type
</el.Label>
<PowerSelect
@options={{pathTypes}}
@selected={{pathType}}
@onChange={{action 'change' 'HTTP.PathType' changeset}} as |Type|>
{{get pathLabels Type}}
</PowerSelect>
</group.Element>
{{#if shouldShowPathField}}
<label class="type-text{{if changeset.error.HTTP.Path ' has-error'}}">
<span>{{get pathLabels pathType}}</span>
<input
type="text"
name="Path"
value={{changeset-get changeset 'HTTP.Path'}}
oninput={{action 'change' 'HTTP.Path' changeset}}
/>
{{#if changeset.error.HTTP.Path}}
<strong>
{{#if (eq (changeset-get changeset 'HTTP.PathType') 'PathRegex')}}
Path Regex should not be blank
{{else}}
Path should begin with a '/'
{{/if}}
</strong>
{{/if}}
</label>
<group.Element
@name="Path"
@error={{changeset-get changeset 'error.HTTP.Path'}}
as |el|>
<el.Label>
{{get pathLabels pathType}}
</el.Label>
<el.Text
@value={{changeset-get changeset 'HTTP.Path'}}
oninput={{action 'change' 'HTTP.Path' changeset}}
/>
<State @state={{el.state}} @matches="error">
<el.Error>
{{#if (eq (changeset-get changeset 'HTTP.Path') 'Regex')}}
Path Regex should not be blank
{{else}}
Path should begin with a '/'
{{/if}}
</el.Error>
</State>
</group.Element>
{{/if}}
</div>
</fieldset>
@ -71,15 +85,17 @@
<h2>Methods</h2>
<div class="type-toggle">
<span>All methods are applied by default unless specified</span>
<label class="type-checkbox">
<input
type="checkbox"
name="{{name}}[allMethods]"
<group.Element
@name="allMethods"
as |el|>
<el.Checkbox
checked={{if allMethods 'checked'}}
onchange={{action 'change' 'allMethods' changeset}}
/>
<span>All methods</span>
</label>
<el.Label>
All Methods
</el.Label>
</group.Element>
</div>
{{#if shouldShowMethods}}
@ -102,6 +118,7 @@
<fieldset>
<h2>Headers</h2>
<ConsulIntentionPermissionHeaderList
@items={{changeset-get changeset 'HTTP.Header'}}
@ondelete={{action 'delete' 'HTTP.Header' changeset}}
@ -121,7 +138,7 @@
disabled={{if (not this.headerForm.isDirty) 'disabled'}}
onclick={{action this.headerForm.submit}}
>
Add another header
Add{{#if (gt (get (changeset-get changeset 'HTTP.Header') 'length') 0)}} another{{/if}} header
</button>
<button
type="button"
@ -132,4 +149,5 @@
</button>
</fieldset>
</FormGroup>
</div>

View file

@ -2,5 +2,15 @@
h2 {
border-top: 1px solid $blue-500;
}
button.type-submit {
@extend %frame-blue-300;
}
button.type-submit:hover:not(:disabled),
button.type-submit:focus:not(:disabled) {
@extend %frame-blue-500;
}
button.type-submit:disabled {
@extend %frame-blue-200;
}
}

View file

@ -2,56 +2,69 @@
...attributes
class="consul-intention-permission-header-form"
>
{{yield (hash
submit=(action 'submit' changeset)
reset=(action 'reset' changeset)
<FormGroup
@name={{name}}
as |group|>
isDirty=(and changeset.isValid changeset.isDirty)
changeset=changeset
)}}
{{yield (hash
submit=(action 'submit' changeset)
reset=(action 'reset' changeset)
<fieldset>
<div>
isDirty=(and changeset.isValid changeset.isDirty)
changeset=changeset
)}}
<label class="type-select">
<span>Header Type</span>
<div>
<fieldset>
<div>
<group.Element
@name="HeaderType"
@type="select"
as |el|>
<el.Label>Header type</el.Label>
<PowerSelect
@options={{headerTypes}}
@selected={{headerType}}
@onChange={{action 'change' 'HeaderType' changeset}} as |Type|>
{{get headerLabels Type}}
</PowerSelect>
</div>
</label>
<label class="type-text{{if changeset.error.Name ' has-error'}}">
<span>Header name</span>
<input
type="text"
name={{concat name '[Name]'}}
value={{changeset-get changeset 'Name'}}
oninput={{action 'change' 'Name' changeset}}
/>
{{#if changeset.error.Name}}
<strong>{{changeset.error.Name.validation}}</strong>
{{/if}}
</label>
</group.Element>
<group.Element
@name="Name"
@error={{changeset-get changeset 'error.Name'}}
as |el|>
<el.Label>Header name</el.Label>
<el.Text
@value={{changeset-get changeset 'Name'}}
oninput={{action 'change' 'Name' changeset}}
/>
<State @state={{el.state}} @matches="error">
<el.Error>
{{changeset-get changeset 'error.Name.validation'}}
</el.Error>
</State>
</group.Element>
{{#if shouldShowValueField}}
<label class="type-text{{if changeset.error.Value ' has-error'}}">
<span>Header {{lowercase (get headerLabels headerType)}}</span>
<input
type="text"
name="Value"
value={{changeset-get changeset 'Value'}}
oninput={{action 'change' 'Value' changeset}}
/>
{{#if changeset.error.Value}}
<strong>{{changeset.error.Value.validation}}</strong>
{{/if}}
</label>
<group.Element
@name="Value"
@error={{changeset-get changeset 'error.Value'}}
as |el|>
<el.Label>Header {{lowercase (get headerLabels headerType)}}</el.Label>
<el.Text
@value={{changeset-get changeset 'Value'}}
oninput={{action 'change' 'Value' changeset}}
/>
<State @state={{el.state}} @matches="error">
<el.Error>
{{changeset-get changeset 'error.Value.validation'}}
</el.Error>
</State>
</group.Element>
{{/if}}
</div>
</fieldset>
</div>
</fieldset>
</FormGroup>
</div>

View file

@ -3,7 +3,6 @@
class="consul-intention-permission-list{{if (not onclick) ' readonly'}}"
@scroll="native"
@items={{items}}
@cellHeight={{42}}
as |item|>
<BlockSlot @name="details">
<div onclick={{action (optional onclick) item}}>

View file

@ -1,24 +1,5 @@
@import './skin';
@import './layout';
%list-row-200 {
@extend %list-row;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
%list-row-200 .detail {
grid-row-start: header !important;
grid-row-end: detail !important;
align-self: center !important;
}
%list-row-200 .popover-menu > [type="checkbox"] + label {
padding: 0;
}
%list-row-200 .popover-menu > [type="checkbox"] + label + div:not(.above) {
top: 30px;
}
%list-row-200 dd {
@extend %p2;
}
.consul-intention-permission-list > ul > li {
@extend %list-row-200;
}

View file

@ -0,0 +1,8 @@
<input
{{did-insert (optional @didinsert)}}
{{on 'change' (optional @onchange)}}
type="checkbox"
name={{@name}}
value={{@value}}
...attributes
/>

View file

@ -0,0 +1,6 @@
<strong
role="alert"
...attributes
>
{{yield}}
</strong>

View file

@ -0,0 +1,33 @@
{{#let (hash
Element=(component 'form-group/element' group=@group name=@name)
Text=(component 'form-group/element/text' didinsert=(action this.connect) name=this.name oninput=(action (mut this.touched) true))
Checkbox=(component 'form-group/element/checkbox' didinsert=(action this.connect) name=this.name onchange=(action (mut this.touched) true))
Radio=(component 'form-group/element/radio' didinsert=(action this.connect) name=this.name onchange=(action (mut this.touched) true))
Label=(component 'form-group/element/label')
Error=(component 'form-group/element/error')
state=state
)
as |el|}}
{{#if (contains this.type (array 'radiogroup' 'checkbox-group' 'checkboxgroup'))}}
<div
data-property={{this.prop}}
class="type-{{this.type}}{{if (state-matches state 'error') ' has-error'}}"
...attributes
>
{{yield el}}
</div>
{{else}}
<label
data-property={{this.prop}}
class="type-{{this.type}}{{if (state-matches state 'error') ' has-error'}}"
...attributes
>
{{yield el}}
</label>
{{/if}}
{{/let}}

View file

@ -0,0 +1,37 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class Element extends Component {
@tracked el;
@tracked touched = false;
get type() {
if (typeof this.el !== 'undefined') {
return this.el.dataset.type || this.el.getAttribute('type') || this.el.getAttribute('role');
}
return this.args.type;
}
get name() {
if (typeof this.args.group !== 'undefined') {
return `${this.args.group.name}[${this.args.name}]`;
} else {
return this.args.name;
}
}
get prop() {
return `${this.args.name.toLowerCase().replaceAll('.', '-')}`;
}
get state() {
const error = this.touched && this.args.error;
return {
matches: name => name === 'error' && error,
};
}
@action
connect($el) {
this.el = $el;
}
}

View file

@ -0,0 +1,6 @@
<span
class="form-elements-label label"
...attributes
>
{{yield}}
</span>

View file

@ -0,0 +1,8 @@
<input
{{did-insert (optional @didinsert)}}
{{on 'change' (optional @onchange)}}
type="radio"
name={{@name}}
value={{@value}}
...attributes
/>

View file

@ -0,0 +1,8 @@
<input
{{did-insert (optional @didinsert)}}
{{on 'input' (optional @oninput)}}
type="text"
name={{@name}}
value={{@value}}
...attributes
/>

View file

@ -0,0 +1,3 @@
{{yield (hash
Element=(component 'form-group/element' group=this)
)}}

View file

@ -0,0 +1,7 @@
import Component from '@glimmer/component';
export default class FormGroup extends Component {
get name() {
return this.args.name;
}
}

View file

@ -107,6 +107,18 @@
border-color: $green-800;
color: $white;
}
%frame-blue-200 {
@extend %frame-border-000;
background-color: $white;
border-color: $blue-300;
color: $blue-300;
}
%frame-blue-300 {
@extend %frame-border-000;
background-color: $white;
border-color: $blue-500;
color: $blue-500;
}
%frame-blue-500 {
@extend %frame-border-000;
background-color: $blue-050;

View file

@ -30,3 +30,23 @@
%list-row-detail > span {
margin-right: 18px;
}
%list-row-200 {
@extend %list-row;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
%list-row-200 .detail {
grid-row-start: header !important;
grid-row-end: detail !important;
align-self: center !important;
padding: 5px 0;
}
%list-row-200 .popover-menu > [type='checkbox'] + label {
padding: 0;
}
%list-row-200 .popover-menu > [type='checkbox'] + label + div:not(.above) {
top: 30px;
}
%list-row-200 dd {
@extend %p2;
}

View file

@ -3,3 +3,4 @@
@import 'routes/dc/nodes/index';
@import 'routes/dc/kv/index';
@import 'routes/dc/acls/index';
@import 'routes/dc/intentions/index';

View file

@ -0,0 +1,3 @@
html[data-route^='dc.intentions.edit'] .definition-table {
margin-bottom: 1em;
}

View file

@ -16,17 +16,11 @@ Feature: dc / intentions / filtered-select: Intention Service Select Dropdowns
- Name: service-3
Kind: connect-proxy
---
And 1 intention model from yaml
---
SourceName: 'service-0'
DestinationName: 'service-1'
---
When I visit the intention page for yaml
---
dc: datacenter
intention: intention
---
Then the url should be /datacenter/intentions/intention
Then the url should be /datacenter/intentions/create
And I click "[data-test-[Name]-element] .ember-power-select-trigger"
Then I see the text "* (All Services)" in ".ember-power-select-option:nth-last-child(3)"
Then I see the text "service-0" in ".ember-power-select-option:nth-last-child(2)"
@ -35,7 +29,7 @@ Feature: dc / intentions / filtered-select: Intention Service Select Dropdowns
---------------
| Name |
| source |
#| destination |
| destination |
---------------
Scenario: Opening the [Name] dropdown with 2 services with the same name from different nspaces
Given 1 datacenter model with the value "datacenter"
@ -47,17 +41,11 @@ Feature: dc / intentions / filtered-select: Intention Service Select Dropdowns
Namespace: nspace
Kind: ~
---
And 1 intention model from yaml
---
SourceName: 'service-0'
DestinationName: 'service-0'
---
When I visit the intention page for yaml
---
dc: datacenter
intention: intention
---
Then the url should be /datacenter/intentions/intention
Then the url should be /datacenter/intentions/create
And I click "[data-test-[Name]-element] .ember-power-select-trigger"
Then I see the text "* (All Services)" in ".ember-power-select-option:nth-last-child(2)"
Then I see the text "service-0" in ".ember-power-select-option:last-child"
@ -65,5 +53,5 @@ Feature: dc / intentions / filtered-select: Intention Service Select Dropdowns
---------------
| Name |
| source |
#| destination |
| destination |
---------------

View file

@ -8,9 +8,8 @@ Feature: dc / intentions / form-select: Intention Service Select Dropdowns
When I visit the intention page for yaml
---
dc: datacenter
intention: intention
---
Then the url should be /datacenter/intentions/intention
Then the url should be /datacenter/intentions/create
And I click "[data-test-[Name]-element] .ember-power-select-trigger"
And I type "something" into ".ember-power-select-search-input"
And I click ".ember-power-select-option:first-child"
@ -19,5 +18,5 @@ Feature: dc / intentions / form-select: Intention Service Select Dropdowns
---------------
| Name |
| source |
# | destination |
| destination |
---------------