ui: Moves intentions listing and form into components (#7549)

Whilst we tried to do this with the smallest amount of changes possible,
our acceptance tests for trying to submit a blank form started failing
due to usage of `destroyRecord`, its seems that the correct way to
achieve the same thing is to use `rollbackAttributes` instead. We
changed that here and the tests pass once again. Furture work related to
this will involve change the rest of the UI where we use `destroyRecord`
to achieve the same thing, to use `rollbackAttributes` instead
pull/7344/head
John Cowen 2020-04-01 09:55:49 +01:00 committed by John Cowen
parent 80960c9d54
commit c20cff9bf5
16 changed files with 255 additions and 204 deletions

View File

@ -1,12 +1,12 @@
<form>
<form onsubmit={{action 'submit' _item}}>
<fieldset>
<div role="group">
<fieldset>
<h2>Source</h2>
<label data-test-source-element class="type-text{{if item.error.SourceName ' has-error'}}">
<label data-test-source-element class="type-text{{if _item.error.SourceName ' has-error'}}">
<span>Source Service</span>
<PowerSelectWithCreate
@options={{services}}
@options={{_services}}
@searchField="Name"
@selected={{SourceName}}
@searchPlaceholder="Type service name"
@ -23,10 +23,10 @@
<em>Search for an existing service, write in a future one, or write in any Service URI.</em>
</label>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
<label data-test-source-nspace class="type-text{{if item.error.SourceNS ' has-error'}}">
<label data-test-source-nspace class="type-text{{if _item.error.SourceNS ' has-error'}}">
<span>Source Namespace</span>
<PowerSelectWithCreate
@options={{nspaces}}
@options={{_nspaces}}
@searchField="Name"
@selected={{SourceNS}}
@searchPlaceholder="Type namespace name"
@ -46,10 +46,10 @@
</fieldset>
<fieldset>
<h2>Destination</h2>
<label data-test-destination-element class="type-text{{if item.error.DestinationName ' has-error'}}">
<label data-test-destination-element class="type-text{{if _item.error.DestinationName ' has-error'}}">
<span>Destination Service</span>
<PowerSelectWithCreate
@options={{services}}
@options={{_services}}
@searchField="Name"
@selected={{DestinationName}}
@searchPlaceholder="Type service name"
@ -66,10 +66,10 @@
<em>Search for an existing service or write in a future one.</em>
</label>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
<label data-test-destination-nspace class="type-text{{if item.error.DestinationNS ' has-error'}}">
<label data-test-destination-nspace class="type-text{{if _item.error.DestinationNS ' has-error'}}">
<span>Destination Namespace</span>
<PowerSelectWithCreate
@options={{nspaces}}
@options={{_nspaces}}
@searchField="Name"
@selected={{DestinationNS}}
@searchPlaceholder="Type namespace name"
@ -88,30 +88,30 @@
{{/if}}
</fieldset>
</div>
<div role="radiogroup" class={{if item.error.Action ' has-error'}}>
<div role="radiogroup" class={{if _item.error.Action ' has-error'}}>
{{#each (array 'allow' 'deny') as |intent|}}
<label>
<span>{{ capitalize intent }}</span>
<input type="radio" name="Action" value="{{intent}}" checked={{if (eq item.Action intent) 'checked'}} onchange={{ action 'change' }}/>
<input type="radio" name="Action" value="{{intent}}" checked={{if (eq _item.Action intent) 'checked'}} onchange={{ action 'change' }}/>
</label>
{{/each}}
</div>
<label class="type-text{{if item.error.Description ' has-error'}}">
<label class="type-text{{if _item.error.Description ' has-error'}}">
<span>Description (Optional)</span>
<input type="text" name="Description" value="{{item.Description}}" placeholder="Description (Optional)" onchange={{action 'change'}} />
<input type="text" name="Description" value="{{_item.Description}}" placeholder="Description (Optional)" onchange={{action 'change'}} />
</label>
</fieldset>
<div>
{{#if create }}
<button type="submit" {{ action "create" item}} disabled={{if (or item.isPristine item.isInvalid) 'disabled'}}>Save</button>
{{#if _item.isNew }}
<button type="submit" disabled={{if (or _item.isPristine _item.isInvalid) 'disabled'}}>Save</button>
{{ else }}
<button type="submit" {{ action "update" item}} disabled={{if item.isInvalid 'disabled'}}>Save</button>
<button type="submit" disabled={{if _item.isInvalid 'disabled'}}>Save</button>
{{/if}}
<button type="reset" {{ action "cancel" item}}>Cancel</button>
{{# if (and item.ID (not-eq item.ID 'anonymous')) }}
<button type="reset" onclick={{action oncancel _item}}>Cancel</button>
{{# if (and _item.ID (not-eq _item.ID 'anonymous')) }}
<ConfirmationDialog @message="Are you sure you want to delete this Intention?">
<BlockSlot @name="action" as |confirm|>
<button data-test-delete type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
<button data-test-delete type="button" class="type-delete" {{action confirm ondelete _item}}>Delete</button>
</BlockSlot>
<BlockSlot @name="dialog" as |execute cancel message|>
<DeleteConfirmation @message={{message}} @execute={{execute}} @cancel={{cancel}} />

View File

@ -0,0 +1,113 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { setProperties, set, get } from '@ember/object';
import { assert } from '@ember/debug';
export default Component.extend({
tagName: '',
dom: service('dom'),
builder: service('form'),
init: function() {
this._super(...arguments);
this.form = this.builder.form('intention');
},
didReceiveAttrs: function() {
this._super(...arguments);
if (this.item && this.services && this.nspaces) {
let services = this.services || [];
let nspaces = this.nspaces || [];
let source = services.findBy('Name', this.item.SourceName);
if (!source) {
source = { Name: this.item.SourceName };
services = [source].concat(services);
}
let destination = services.findBy('Name', this.item.DestinationName);
if (!destination) {
destination = { Name: this.item.DestinationName };
services = [destination].concat(services);
}
let sourceNS = nspaces.findBy('Name', this.item.SourceNS);
if (!sourceNS) {
sourceNS = { Name: this.item.SourceNS };
nspaces = [sourceNS].concat(nspaces);
}
let destinationNS = this.nspaces.findBy('Name', this.item.DestinationNS);
if (!destinationNS) {
destinationNS = { Name: this.item.DestinationNS };
nspaces = [destinationNS].concat(nspaces);
}
// TODO: Use this.{item,services} when we have this.args
setProperties(this, {
_item: this.form.setData(this.item).getData(),
_services: services,
_nspaces: nspaces,
SourceName: source,
DestinationName: destination,
SourceNS: sourceNS,
DestinationNS: destinationNS,
});
} else {
assert('@item, @services and @nspaces are required arguments', false);
}
},
actions: {
createNewLabel: function(template, term) {
return template.replace(/{{term}}/g, term);
},
isUnique: function(term) {
return !this._services.findBy('Name', term);
},
submit: function(item, e) {
e.preventDefault();
this.onsubmit(...arguments);
},
change: function(e, value, item) {
const event = this.dom.normalizeEvent(e, value);
const form = this.form;
const target = event.target;
let name, selected, match;
switch (target.name) {
case 'SourceName':
case 'DestinationName':
case 'SourceNS':
case 'DestinationNS':
name = selected = target.value;
// Names can be selected Service EmberObjects or typed in strings
// if its not a string, use the `Name` from the Service EmberObject
if (typeof name !== 'string') {
name = get(target.value, 'Name');
}
// mutate the value with the string name
// which will be handled by the form
target.value = name;
// these are 'non-form' variables so not on `item`
// these variables also exist in the template so we know
// the current selection
// basically the difference between
// `item.DestinationName` and just `DestinationName`
// see if the name is already in the list
match = this._services.filterBy('Name', name);
if (match.length === 0) {
// if its not make a new 'fake' Service that doesn't exist yet
// and add it to the possible services to make an intention between
selected = { Name: name };
switch (target.name) {
case 'SourceName':
case 'DestinationName':
set(this, '_services', [selected].concat(this._services.toArray()));
break;
case 'SourceNS':
case 'DestinationNS':
set(this, '_nspaces', [selected].concat(this._nspaces.toArray()));
break;
}
}
set(this, target.name, selected);
break;
}
form.handleEvent(event);
},
},
});

View File

@ -0,0 +1,73 @@
<TabularCollection class="consul-intention-list" @items={{items}} as |item index|>
<BlockSlot @name="header">
<th>Source</th>
<th>&nbsp;</th>
<th>Destination</th>
<th>Precedence</th>
</BlockSlot>
<BlockSlot @name="row">
<td class="source" data-test-intention={{item.ID}}>
<a href={{href-to 'dc.intentions.edit' item.ID}} data-test-intention-source={{item.SourceName}}>
{{#if (eq item.SourceName '*') }}
All Services (*)
{{else}}
{{item.SourceName}}
{{/if}}
{{! TODO: slugify }}
<em class={{concat 'nspace-' (or item.SourceNS 'default')}}>{{or item.SourceNS 'default'}}</em>
</a>
</td>
<td class="intent-{{item.Action}}" data-test-intention-action="{{item.Action}}">
<strong>{{item.Action}}</strong>
</td>
<td class="destination" data-test-intention-destination="{{item.DestinationName}}">
<span>
{{#if (eq item.DestinationName '*') }}
All Services (*)
{{else}}
{{item.DestinationName}}
{{/if}}
{{! TODO: slugify }}
<em class={{concat 'nspace-' (or item.DestinationNS 'default')}}>{{or item.DestinationNS 'default'}}</em>
</span>
</td>
<td class="precedence">
{{item.Precedence}}
</td>
</BlockSlot>
<BlockSlot @name="actions" as |index change checked|>
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}>
<BlockSlot @name="trigger">
More
</BlockSlot>
<BlockSlot @name="menu" as |confirm send keypressClick change|>
<li role="none">
<a role="menuitem" tabindex="-1" href={{href-to 'dc.intentions.edit' item.ID}}>Edit</a>
</li>
<li role="none" class="dangerous">
<label for={{confirm}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm Delete
</header>
<p>
Are you sure you want to delete this intention?
</p>
</div>
<ul>
<li class="dangerous">
<button tabindex="-1" type="button" class="type-delete" onclick={{queue (action change) (action ondelete item)}}>Delete</button>
</li>
<li>
<label for={{confirm}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
</BlockSlot>
</PopoverMenu>
</BlockSlot>
</TabularCollection>

View File

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

View File

@ -1,100 +1,8 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
export default Controller.extend({
dom: service('dom'),
builder: service('form'),
init: function() {
this._super(...arguments);
this.form = this.builder.form('intention');
},
setProperties: function(model) {
let source = model.services.findBy('Name', model.item.SourceName);
if (!source) {
source = { Name: model.item.SourceName };
model.services = [source].concat(model.services);
}
let destination = model.services.findBy('Name', model.item.DestinationName);
if (!destination) {
destination = { Name: model.item.DestinationName };
model.services = [destination].concat(model.services);
}
let sourceNS = model.nspaces.findBy('Name', model.item.SourceNS);
if (!sourceNS) {
sourceNS = { Name: model.item.SourceNS };
model.nspaces = [sourceNS].concat(model.nspaces);
}
let destinationNS = model.nspaces.findBy('Name', model.item.DestinationNS);
if (!destinationNS) {
destinationNS = { Name: model.item.DestinationNS };
model.nspaces = [destinationNS].concat(model.nspaces);
}
this._super({
...model,
...{
item: this.form.setData(model.item).getData(),
SourceName: source,
DestinationName: destination,
SourceNS: sourceNS,
DestinationNS: destinationNS,
},
});
},
actions: {
createNewLabel: function(template, term) {
return template.replace(/{{term}}/g, term);
},
isUnique: function(term) {
return !this.services.findBy('Name', term);
},
change: function(e, value, item) {
const event = this.dom.normalizeEvent(e, value);
const form = this.form;
const target = event.target;
let name, selected, match;
switch (target.name) {
case 'SourceName':
case 'DestinationName':
case 'SourceNS':
case 'DestinationNS':
name = selected = target.value;
// Names can be selected Service EmberObjects or typed in strings
// if its not a string, use the `Name` from the Service EmberObject
if (typeof name !== 'string') {
name = get(target.value, 'Name');
}
// mutate the value with the string name
// which will be handled by the form
target.value = name;
// these are 'non-form' variables so not on `item`
// these variables also exist in the template so we know
// the current selection
// basically the difference between
// `item.DestinationName` and just `DestinationName`
// see if the name is already in the list
match = this.services.filterBy('Name', name);
if (match.length === 0) {
// if its not make a new 'fake' Service that doesn't exist yet
// and add it to the possible services to make an intention between
selected = { Name: name };
switch (target.name) {
case 'SourceName':
case 'DestinationName':
set(this, 'services', [selected].concat(this.services.toArray()));
break;
case 'SourceNS':
case 'DestinationNS':
set(this, 'nspaces', [selected].concat(this.nspaces.toArray()));
break;
}
}
set(this, target.name, selected);
break;
}
form.handleEvent(event);
route: function() {
this.send(...arguments);
},
},
});

View File

@ -47,4 +47,9 @@ export default Controller.extend(WithSearching, WithFiltering, WithEventSource,
filter: function(item, { s = '', currentFilter = '' }) {
return currentFilter === '' || get(item, 'Action') === currentFilter;
},
actions: {
route: function() {
this.send(...arguments);
},
},
});

View File

@ -42,7 +42,7 @@ export default Route.extend(WithIntentionActions, {
},
deactivate: function() {
if (get(this.item, 'isNew')) {
this.item.destroyRecord();
this.item.rollbackAttributes();
}
},
});

View File

@ -0,0 +1,9 @@
@import './consul-intention-list/index';
.consul-intention-list {
@extend %consul-intention-list;
}
@media #{$--lt-wide-table} {
%consul-intention-list tr > :nth-last-child(2) {
display: none;
}
}

View File

@ -0,0 +1,5 @@
@import './skin';
@import './layout';
%consul-intention-list td.destination {
@extend %tbody-th;
}

View File

@ -0,0 +1,10 @@
%consul-intention-list td strong {
visibility: hidden;
}
%consul-intention-list td.intent-allow strong::before {
@extend %with-arrow-right-color-icon, %as-pseudo;
background-size: 24px;
}
%consul-intention-list td.intent-deny strong::before {
@extend %with-deny-color-icon, %as-pseudo;
}

View File

@ -25,6 +25,7 @@
@import './notice';
@import './sort-control';
@import './discovery-chain';
@import './consul-intention-list';
@import './tabular-details';
@import './tabular-collection';

View File

@ -82,9 +82,6 @@ th span em {
tr > .actions {
display: none;
}
html.template-intention.template-list tr > :nth-last-child(2) {
display: none;
}
html.template-service.template-list tr > :last-child {
display: none;
}

View File

@ -1,13 +0,0 @@
html.template-intention.template-list td strong {
visibility: hidden;
}
html.template-intention.template-list td.intent-allow strong::before {
@extend %with-arrow-right-color-icon, %as-pseudo;
background-size: 24px;
}
html.template-intention.template-list td.intent-deny strong::before {
@extend %with-deny-color-icon, %as-pseudo;
}
html.template-intention.template-list td.destination {
@extend %tbody-th;
}

View File

@ -44,6 +44,13 @@
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
{{ partial 'dc/intentions/form'}}
<ConsulIntentionForm
@item={{item}}
@services={{services}}
@nspaces={{nspaces}}
@ondelete={{action "route" "delete"}}
@onsubmit={{action "route" (if item.isNew "create" "update")}}
@oncancel={{action "route" "cancel"}}
/>
</BlockSlot>
</AppView>

View File

@ -20,79 +20,10 @@
<BlockSlot @name="content">
<ChangeableSet @dispatcher={{searchable}}>
<BlockSlot @name="set" as |filtered|>
<TabularCollection @route="dc.intentions.edit" @key="SourceName" @items={{filtered}} as |item index|>
<BlockSlot @name="header">
<th>Source</th>
<th>&nbsp;</th>
<th>Destination</th>
<th>Precedence</th>
</BlockSlot>
<BlockSlot @name="row">
<td class="source" data-test-intention="{{item.ID}}">
<a href={{href-to 'dc.intentions.edit' item.ID}} data-test-intention-source="{{item.SourceName}}">
{{#if (eq item.SourceName '*') }}
All Services (*)
{{else}}
{{item.SourceName}}
{{/if}}
{{! TODO: slugify }}
<em class={{concat 'nspace-' (or item.SourceNS 'default')}}>{{or item.SourceNS 'default'}}</em>
</a>
</td>
<td class="intent-{{item.Action}}" data-test-intention-action="{{item.Action}}">
<strong>{{item.Action}}</strong>
</td>
<td class="destination" data-test-intention-destination="{{item.DestinationName}}">
<span>
{{#if (eq item.DestinationName '*') }}
All Services (*)
{{else}}
{{item.DestinationName}}
{{/if}}
{{! TODO: slugify }}
<em class={{concat 'nspace-' (or item.DestinationNS 'default')}}>{{or item.DestinationNS 'default'}}</em>
</span>
</td>
<td class="precedence">
{{item.Precedence}}
</td>
</BlockSlot>
<BlockSlot @name="actions" as |index change checked|>
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}>
<BlockSlot @name="trigger">
More
</BlockSlot>
<BlockSlot @name="menu" as |confirm send keypressClick|>
<li role="none">
<a role="menuitem" tabindex="-1" href={{href-to 'dc.intentions.edit' item.ID}}>Edit</a>
</li>
<li role="none" class="dangerous">
<label for={{confirm}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm Delete
</header>
<p>
Are you sure you want to delete this intention?
</p>
</div>
<ul>
<li class="dangerous">
<button tabindex="-1" type="button" class="type-delete" onclick={{action send 'delete' item}}>Delete</button>
</li>
<li>
<label for={{confirm}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
</BlockSlot>
</PopoverMenu>
</BlockSlot>
</TabularCollection>
<ConsulIntentionList
@items={{filtered}}
@ondelete={{action "route" "delete"}}
/>
</BlockSlot>
<BlockSlot @name="empty">
<p>