ui: Intention "Action change" warning modal (#9108)
* ui: Add a warning dialog if you go to remove permissions from an intention * ui: Move modal styles next to component, add warning style * ui: Move back to using the input name for a selector * ui: Fixup negative "isn't" step so its optional * Add warning modal to pageobject * Fixup test for whether to show the warning modal or not * Intention change action warning acceptence test * Add a null/undefined Action
This commit is contained in:
parent
137f7d0a92
commit
1b042943e9
|
@ -1,3 +1,7 @@
|
|||
<div
|
||||
class="consul-intention"
|
||||
...attributes
|
||||
>
|
||||
<DataForm
|
||||
@type="intention"
|
||||
@dc={{@dc}}
|
||||
|
@ -27,9 +31,46 @@ as |api|>
|
|||
</BlockSlot>
|
||||
|
||||
<BlockSlot @name="form">
|
||||
|
||||
{{#let api.data as |item|}}
|
||||
{{#if item.IsEditable}}
|
||||
|
||||
{{#if this.warn}}
|
||||
{{#let (changeset-get item 'Action') as |newAction|}}
|
||||
<ModalDialog
|
||||
class="consul-intention-action-warn-modal warning"
|
||||
data-test-action-warning
|
||||
@onclose={{action (mut this.warn) false}}
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<h2>Set intention to {{newAction}}?</h2>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
When you change this Intention to {{newAction}}, you will remove all the L7 policy permissions currently saved to this Intention. Are you sure you want to set it to {{newAction}}?
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions" as |close|>
|
||||
<button
|
||||
data-test-action-warning-confirm
|
||||
type="button"
|
||||
class="dangerous"
|
||||
{{on 'click' api.submit}}
|
||||
>
|
||||
Set to {{capitalize newAction}}
|
||||
</button>
|
||||
<button
|
||||
data-test-action-warning-cancel
|
||||
type="button"
|
||||
class="type-cancel"
|
||||
onclick={{close}}
|
||||
>
|
||||
No, Cancel
|
||||
</button>
|
||||
</BlockSlot>
|
||||
</ModalDialog>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
|
||||
<DataSource
|
||||
@src={{concat '/' @nspace '/' @dc '/services'}}
|
||||
@onchange={{action this.createServices item}}
|
||||
|
@ -43,7 +84,9 @@ as |api|>
|
|||
{{#if (and api.isCreate this.isManagedByCRDs)}}
|
||||
<Consul::Intention::Notice::CustomResource @type="warning" />
|
||||
{{/if}}
|
||||
<form onsubmit={{action api.submit}}>
|
||||
<form
|
||||
{{on 'submit' (fn this.submit item api.submit)}}
|
||||
>
|
||||
<Consul::Intention::Form::Fieldsets
|
||||
@nspaces={{this.nspaces}}
|
||||
@services={{this.services}}
|
||||
|
@ -127,3 +170,4 @@ as |api|>
|
|||
|
||||
</BlockSlot>
|
||||
</DataForm>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { action } from '@ember/object';
|
|||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ConsulIntentionForm extends Component {
|
||||
|
||||
@tracked services;
|
||||
@tracked SourceName;
|
||||
@tracked DestinationName;
|
||||
|
@ -15,6 +14,8 @@ export default class ConsulIntentionForm extends Component {
|
|||
|
||||
@tracked isManagedByCRDs;
|
||||
|
||||
@tracked warn = false;
|
||||
|
||||
@service('repository/intention') repo;
|
||||
|
||||
constructor(owner, args) {
|
||||
|
@ -23,7 +24,7 @@ export default class ConsulIntentionForm extends Component {
|
|||
}
|
||||
|
||||
ondelete() {
|
||||
if(this.args.ondelete) {
|
||||
if (this.args.ondelete) {
|
||||
this.args.ondelete(...arguments);
|
||||
} else {
|
||||
this.onsubmit(...arguments);
|
||||
|
@ -31,7 +32,7 @@ export default class ConsulIntentionForm extends Component {
|
|||
}
|
||||
|
||||
oncancel() {
|
||||
if(this.args.oncancel) {
|
||||
if (this.args.oncancel) {
|
||||
this.args.oncancel(...arguments);
|
||||
} else {
|
||||
this.onsubmit(...arguments);
|
||||
|
@ -39,7 +40,7 @@ export default class ConsulIntentionForm extends Component {
|
|||
}
|
||||
|
||||
onsubmit() {
|
||||
if(this.args.onsubmit) {
|
||||
if (this.args.onsubmit) {
|
||||
this.args.onsubmit(...arguments);
|
||||
}
|
||||
}
|
||||
|
@ -48,9 +49,19 @@ export default class ConsulIntentionForm extends Component {
|
|||
updateCRDManagement() {
|
||||
this.isManagedByCRDs = this.repo.isManagedByCRDs();
|
||||
}
|
||||
|
||||
@action
|
||||
createServices (item, e) {
|
||||
submit(item, submit, e) {
|
||||
e.preventDefault();
|
||||
// if the action of the intention has changed and its non-empty then warn
|
||||
// the user
|
||||
if (typeof item.change.Action !== 'undefined' && typeof item.data.Action === 'undefined') {
|
||||
this.warn = true;
|
||||
} else {
|
||||
submit();
|
||||
}
|
||||
}
|
||||
@action
|
||||
createServices(item, e) {
|
||||
// Services in the menus should:
|
||||
// 1. Be unique (they potentially could be duplicated due to services from different namespaces)
|
||||
// 2. Only include services that shold have intentions
|
||||
|
@ -59,9 +70,7 @@ export default class ConsulIntentionForm extends Component {
|
|||
let items = e.data
|
||||
.uniqBy('Name')
|
||||
.toArray()
|
||||
.filter(
|
||||
item => !['connect-proxy', 'mesh-gateway', 'terminating-gateway'].includes(item.Kind)
|
||||
)
|
||||
.filter(item => !['connect-proxy', 'mesh-gateway', 'terminating-gateway'].includes(item.Kind))
|
||||
.sort((a, b) => a.Name.localeCompare(b.Name));
|
||||
items = [{ Name: '*' }].concat(items);
|
||||
let source = items.findBy('Name', item.SourceName);
|
||||
|
@ -80,7 +89,7 @@ export default class ConsulIntentionForm extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
createNspaces (item, e) {
|
||||
createNspaces(item, e) {
|
||||
// Nspaces in the menus should:
|
||||
// 1. Include an 'All Namespaces' option
|
||||
// 2. Include the current SourceNS and DestinationNS incase they don't exist yet
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
.consul-intention-action-warn-modal {
|
||||
.modal-dialog-window {
|
||||
max-width: 450px;
|
||||
}
|
||||
.modal-dialog-body p {
|
||||
font-size: $typo-size-600;
|
||||
}
|
||||
button.dangerous {
|
||||
@extend %dangerous-button;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
@import './search-bar';
|
||||
@import './list';
|
||||
@import './form';
|
||||
@import './form/fieldsets';
|
||||
@import './permission/list';
|
||||
@import './permission/form';
|
||||
|
|
|
@ -1,28 +1,44 @@
|
|||
{{on-window 'resize' (action "resize") }}
|
||||
<Portal @target="modal">
|
||||
{{yield}}
|
||||
<div {{ref this 'modal'}} ...attributes>
|
||||
<input id={{name}} type="radio" name="modal" data-checked="{{checked}}" checked={{checked}} onchange={{action 'change'}} />
|
||||
<div role="dialog" aria-modal="true">
|
||||
<div
|
||||
class="modal-dialog"
|
||||
{{ref this 'modal'}}
|
||||
...attributes
|
||||
>
|
||||
<input
|
||||
class="modal-dialog-control"
|
||||
id={{name}}
|
||||
type="radio"
|
||||
name="modal"
|
||||
data-checked="{{checked}}"
|
||||
checked={{checked}}
|
||||
onchange={{action 'change'}}
|
||||
/>
|
||||
<div
|
||||
class="modal-dialog-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<label for="modal_close"></label>
|
||||
<div>
|
||||
<div>
|
||||
<header>
|
||||
<label for="modal_close">Close</label>
|
||||
<div class="modal-dialog-window">
|
||||
<header class="modal-dialog-header">
|
||||
<label for="modal_close"></label>
|
||||
<YieldSlot @name="header">
|
||||
{{yield (hash
|
||||
close=(action "close")
|
||||
)}}
|
||||
</YieldSlot>
|
||||
</header>
|
||||
<div>
|
||||
<div class="modal-dialog-body">
|
||||
<YieldSlot @name="body">
|
||||
{{yield (hash
|
||||
close=(action "close")
|
||||
)}}
|
||||
</YieldSlot>
|
||||
</div>
|
||||
<footer>
|
||||
<footer class="modal-dialog-footer">
|
||||
<YieldSlot @name="actions" @params={{block-params (action "close")}}>
|
||||
{{yield (hash
|
||||
close=(action "close")
|
||||
|
|
|
@ -19,7 +19,7 @@ export default Component.extend(Slotted, {
|
|||
set(this, 'checked', true);
|
||||
if (this.height === null) {
|
||||
if (this.element) {
|
||||
const dialogPanel = this.dom.element('[role="dialog"] > div > div', this.modal);
|
||||
const dialogPanel = this.dom.element('.modal-dialog-window', this.modal);
|
||||
const rect = dialogPanel.getBoundingClientRect();
|
||||
set(this, 'dialog', dialogPanel);
|
||||
set(this, 'height', rect.height);
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
||||
.modal-dialog-modal {
|
||||
@extend %modal-dialog;
|
||||
}
|
||||
input[name='modal'] {
|
||||
@extend %modal-control;
|
||||
}
|
||||
html.template-with-modal {
|
||||
@extend %with-modal;
|
||||
}
|
||||
%modal-dialog table {
|
||||
min-height: 149px;
|
||||
}
|
|
@ -70,6 +70,7 @@
|
|||
%modal-window > header [for='modal_close'] {
|
||||
float: right;
|
||||
text-indent: -9000px;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: -3px;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
.modal-dialog.warning header {
|
||||
background-color: $yellow-050;
|
||||
border-color: $yellow-500;
|
||||
color: $yellow-800;
|
||||
}
|
||||
.modal-dialog.warning header > *:not(label) {
|
||||
font-size: $typo-size-500;
|
||||
font-weight: $typo-weight-semibold;
|
||||
}
|
||||
.modal-dialog.warning header::before {
|
||||
@extend %with-alert-triangle-mask, %as-pseudo;
|
||||
color: $yellow-500;
|
||||
float: left;
|
||||
margin-top: 2px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
%modal-dialog > label {
|
||||
background-color: rgba($white, 0.9);
|
||||
}
|
||||
%modal-window {
|
||||
box-shadow: $decor-elevation-800;
|
||||
}
|
||||
%modal-window {
|
||||
/*%frame-gray-000*/
|
||||
background-color: $white;
|
||||
}
|
||||
%modal-window > footer,
|
||||
%modal-window > header {
|
||||
@extend %frame-gray-800;
|
||||
}
|
||||
|
||||
.modal-dialog-body,
|
||||
%modal-window > footer,
|
||||
%modal-window > header {
|
||||
border-color: $gray-300;
|
||||
}
|
||||
.modal-dialog-body {
|
||||
border-style: solid;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
%modal-window > footer,
|
||||
%modal-window > header {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
%modal-window > header [for='modal_close'] {
|
||||
@extend %with-cancel-plain-icon;
|
||||
cursor: pointer;
|
||||
border: $decor-border-100;
|
||||
/*%frame-gray-050??*/
|
||||
background-color: $gray-050;
|
||||
border-color: $gray-300;
|
||||
border-radius: $decor-radius-100;
|
||||
}
|
|
@ -2,6 +2,7 @@ import Serializer from './application';
|
|||
import { inject as service } from '@ember/service';
|
||||
import { get } from '@ember/object';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/intention';
|
||||
import removeNull from 'consul-ui/utils/remove-null';
|
||||
|
||||
export default Serializer.extend({
|
||||
primaryKey: PRIMARY_KEY,
|
||||
|
@ -28,7 +29,7 @@ export default Serializer.extend({
|
|||
respond((headers, body) => {
|
||||
return cb(
|
||||
headers,
|
||||
body.map(item => this.ensureID(item))
|
||||
body.map(item => this.ensureID(removeNull(item)))
|
||||
);
|
||||
}),
|
||||
query
|
||||
|
@ -38,7 +39,7 @@ export default Serializer.extend({
|
|||
return this._super(
|
||||
cb =>
|
||||
respond((headers, body) => {
|
||||
body = this.ensureID(body);
|
||||
body = this.ensureID(removeNull(body));
|
||||
return cb(headers, body);
|
||||
}),
|
||||
query
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
@import './form-elements/index';
|
||||
@import './inline-alert/index';
|
||||
@import './menu-panel/index';
|
||||
@import './modal-dialog/index';
|
||||
@import './pill/index';
|
||||
@import './popover-menu/index';
|
||||
@import './radio-group/index';
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
|
@ -1,32 +0,0 @@
|
|||
%modal-dialog > label {
|
||||
background-color: rgba($white, 0.9);
|
||||
}
|
||||
%modal-window {
|
||||
box-shadow: $decor-elevation-800;
|
||||
}
|
||||
%modal-window {
|
||||
/*%frame-gray-000*/
|
||||
background-color: $white;
|
||||
border: $decor-border-100;
|
||||
border-color: $gray-300;
|
||||
}
|
||||
%modal-window > footer,
|
||||
%modal-window > header {
|
||||
@extend %frame-gray-800;
|
||||
}
|
||||
%modal-window > footer {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
%modal-window > header {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
%modal-window > header [for='modal_close'] {
|
||||
@extend %with-cancel-plain-icon;
|
||||
cursor: pointer;
|
||||
border: $decor-border-100;
|
||||
/*%frame-gray-050??*/
|
||||
background-color: $gray-050;
|
||||
border-color: $gray-300;
|
||||
border-radius: $decor-radius-100;
|
||||
}
|
|
@ -26,7 +26,6 @@
|
|||
@import './components/flash-message';
|
||||
@import './components/code-editor';
|
||||
@import './components/confirmation-dialog';
|
||||
@import './components/modal-dialog';
|
||||
@import './components/auth-form';
|
||||
@import './components/auth-modal';
|
||||
@import './components/oidc-select';
|
||||
|
@ -55,6 +54,7 @@
|
|||
/**/
|
||||
|
||||
@import 'consul-ui/components/notice';
|
||||
@import 'consul-ui/components/modal-dialog';
|
||||
|
||||
@import 'consul-ui/components/consul/exposed-path/list';
|
||||
@import 'consul-ui/components/consul/external-source';
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
%auth-modal footer {
|
||||
border: 0;
|
||||
border-top: 0;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 20px;
|
||||
margin: 0px 26px;
|
||||
padding-left: 42px;
|
||||
padding-right: 42px;
|
||||
}
|
||||
%auth-modal footer {
|
||||
background-color: $transparent;
|
||||
|
|
|
@ -4,6 +4,12 @@ label span {
|
|||
.has-error {
|
||||
@extend %form-element-error;
|
||||
}
|
||||
// TODO: float right here is too specific, this is currently used just for the role/policy selectors
|
||||
label.type-dialog {
|
||||
@extend %anchor;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
||||
.type-toggle {
|
||||
@extend %form-element, %sliding-toggle;
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
[role='dialog'] {
|
||||
@extend %modal-dialog;
|
||||
}
|
||||
input[name='modal'] {
|
||||
@extend %modal-control;
|
||||
}
|
||||
html.template-with-modal {
|
||||
@extend %with-modal;
|
||||
}
|
||||
%modal-dialog table {
|
||||
min-height: 149px;
|
||||
}
|
||||
// TODO: float right here is too specific, this is currently used just for the role/policy selectors
|
||||
label.type-dialog {
|
||||
@extend %anchor;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / intentions / permissions / warn: Intention Permission Warn
|
||||
Scenario:
|
||||
Given 1 datacenter model with the value "datacenter"
|
||||
And 1 intention model from yaml
|
||||
---
|
||||
SourceNS: default
|
||||
SourceName: web
|
||||
DestinationNS: default
|
||||
DestinationName: db
|
||||
Action: ~
|
||||
Permissions:
|
||||
- Action: allow
|
||||
HTTP:
|
||||
PathExact: /path
|
||||
---
|
||||
When I visit the intention page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
intention: intention-id
|
||||
---
|
||||
Then the url should be /datacenter/intentions/intention-id
|
||||
And I click "[value='deny']"
|
||||
And I submit
|
||||
And the warning object is present
|
||||
And I click the warning.cancel object
|
||||
And the warning object isn't present
|
||||
And I submit
|
||||
And the warning object is present
|
||||
And I click the warning.confirm object
|
||||
Then a PUT request was made to "/v1/connect/intentions/exact?source=default%2Fweb&destination=default%2Fdb&dc=datacenter" from yaml
|
|
@ -0,0 +1,10 @@
|
|||
import steps from '../../../steps';
|
||||
|
||||
// step definitions that are shared between features should be moved to the
|
||||
// tests/acceptance/steps/steps.js file
|
||||
|
||||
export default function(assert) {
|
||||
return steps(assert).then('I should find a file', function() {
|
||||
assert.ok(true, this.step);
|
||||
});
|
||||
}
|
|
@ -94,7 +94,13 @@ const morePopoverMenu = morePopoverMenuFactory(clickable);
|
|||
const popoverSelect = popoverSelectFactory(clickable, collection);
|
||||
const emptyState = emptyStateFactory(isPresent);
|
||||
|
||||
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, isPresent, deletable);
|
||||
const consulIntentionList = consulIntentionListFactory(
|
||||
collection,
|
||||
clickable,
|
||||
attribute,
|
||||
isPresent,
|
||||
deletable
|
||||
);
|
||||
const consulNspaceList = consulNspaceListFactory(
|
||||
collection,
|
||||
clickable,
|
||||
|
@ -176,6 +182,7 @@ export default {
|
|||
intention(
|
||||
visitable,
|
||||
clickable,
|
||||
isPresent,
|
||||
submitable,
|
||||
deletable,
|
||||
cancelable,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export default function(
|
||||
visitable,
|
||||
clickable,
|
||||
isPresent,
|
||||
submitable,
|
||||
deletable,
|
||||
cancelable,
|
||||
|
@ -18,6 +19,19 @@ export default function(
|
|||
form: permissionsForm(),
|
||||
list: permissionsList(),
|
||||
},
|
||||
warning: {
|
||||
scope: '[data-test-action-warning]',
|
||||
resetScope: true,
|
||||
present: isPresent(),
|
||||
confirm: {
|
||||
scope: '[data-test-action-warning-confirm]',
|
||||
click: clickable(),
|
||||
},
|
||||
cancel: {
|
||||
scope: '[data-test-action-warning-cancel]',
|
||||
click: clickable(),
|
||||
},
|
||||
},
|
||||
...submitable(),
|
||||
...cancelable(),
|
||||
...deletable(),
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function(scenario, assert, find, currentPage, $) {
|
|||
setTimeout(() => next());
|
||||
}
|
||||
)
|
||||
.then(`the $pageObject object is(n't) $state`, function(element, negative, state, next) {
|
||||
.then(`the $pageObject object is(n't)? $state`, function(element, negative, state, next) {
|
||||
assert[negative ? 'notOk' : 'ok'](element[state]);
|
||||
setTimeout(() => next());
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue