UI kmip scope delete and role form (#7169)
* always use ?force for kmip scope delete * update the delete message when deleting a scope * support disabling and not showing help text for checkboxes * group TLS fields and render new allowed operations widget * add operation-field-display component for kmip roles * use operation-field-display component * switch glyph for false value in info-table-row * divvy up roles and tls * fix JSDoc - showHelpText defaults to true * fix tests and linting * rename vars in operation-field-display component * make the action name clearer re: what it's actually doing * align the allowed-ops header * show all operations as checked if you check to allow all
This commit is contained in:
parent
18fa9e418c
commit
b0dfbde741
|
@ -14,6 +14,8 @@ export default BaseAdapter.extend({
|
|||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
return this.ajax(this._url(type.modelName, { backend: snapshot.record.backend }, snapshot.id), 'DELETE');
|
||||
let url = this._url(type.modelName, { backend: snapshot.record.backend }, snapshot.id);
|
||||
url = `${url}?force=true`;
|
||||
return this.ajax(url, 'DELETE');
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,8 +15,12 @@ export const COMPUTEDS = {
|
|||
return this.operationFields.slice().removeObjects(['operationAll', 'operationNone']);
|
||||
}),
|
||||
|
||||
nonOperationFields: computed('operationFields', function() {
|
||||
let excludeFields = ['role'].concat(this.operationFields);
|
||||
tlsFields: computed(function() {
|
||||
return ['tlsClientKeyBits', 'tlsClientKeyType', 'tlsClientTtl'];
|
||||
}),
|
||||
|
||||
nonOperationFields: computed('tlsFields', 'operationFields', function() {
|
||||
let excludeFields = ['role'].concat(this.operationFields, this.tlsFields);
|
||||
return this.newFields.slice().removeObjects(excludeFields);
|
||||
}),
|
||||
};
|
||||
|
@ -29,14 +33,43 @@ const Model = DS.Model.extend(COMPUTEDS, {
|
|||
getHelpUrl(path) {
|
||||
return `/v1/${path}/scope/example/role/example?help=1`;
|
||||
},
|
||||
fieldGroups: computed('fields', 'nonOperationFields', function() {
|
||||
const groups = [{ default: this.nonOperationFields }, { 'Allowed Operations': this.operationFields }];
|
||||
fieldGroups: computed('fields', 'tlsFields', 'nonOperationFields', function() {
|
||||
const groups = [{ TLS: this.tlsFields }];
|
||||
if (this.nonOperationFields.length) {
|
||||
groups.unshift({ default: this.nonOperationFields });
|
||||
}
|
||||
let ret = fieldToAttrs(this, groups);
|
||||
return ret;
|
||||
}),
|
||||
|
||||
operationFormFields: computed('operationFieldsWithoutSpecial', function() {
|
||||
return expandAttributeMeta(this, this.operationFieldsWithoutSpecial);
|
||||
let objects = [
|
||||
'operationCreate',
|
||||
'operationActivate',
|
||||
'operationGet',
|
||||
'operationLocate',
|
||||
'operationRekey',
|
||||
'operationRevoke',
|
||||
'operationDestroy',
|
||||
];
|
||||
|
||||
let attributes = ['operationAddAttribute', 'operationGetAttributes'];
|
||||
let server = ['operationDiscoverVersion'];
|
||||
let others = this.operationFieldsWithoutSpecial.slice().removeObjects(objects.concat(attributes, server));
|
||||
const groups = [
|
||||
{ 'Managed Cryptographic Objects': objects },
|
||||
{ 'Object Attributes': attributes },
|
||||
{ Server: server },
|
||||
];
|
||||
if (others.length) {
|
||||
groups.push({
|
||||
'': others,
|
||||
});
|
||||
}
|
||||
return fieldToAttrs(this, groups);
|
||||
}),
|
||||
tlsFormFields: computed('tlsFields', function() {
|
||||
return expandAttributeMeta(this, this.tlsFields);
|
||||
}),
|
||||
fields: computed('nonOperationFields', function() {
|
||||
return expandAttributeMeta(this, this.nonOperationFields);
|
||||
|
|
14
ui/app/styles/components/kmip-role-edit.scss
Normal file
14
ui/app/styles/components/kmip-role-edit.scss
Normal file
|
@ -0,0 +1,14 @@
|
|||
.kmip-allowed-operations-header {
|
||||
@extend .title;
|
||||
@extend .is-6;
|
||||
padding-left: $spacing-s;
|
||||
}
|
||||
.kmip-role-allowed-operations {
|
||||
@extend .box;
|
||||
flex: 1 1 auto;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
.kmip-role-allowed-operations .field {
|
||||
margin-bottom: $spacing-xxs;
|
||||
}
|
|
@ -60,6 +60,7 @@
|
|||
@import './components/init-illustration';
|
||||
@import './components/info-table-row';
|
||||
@import './components/input-hint';
|
||||
@import './components/kmip-role-edit';
|
||||
@import './components/linked-block';
|
||||
@import './components/list-item-row';
|
||||
@import './components/list-pagination';
|
||||
|
|
|
@ -19,6 +19,9 @@ import layout from '../templates/components/form-field';
|
|||
* @param [onChange=null] {Func} - Called whenever a value on the model changes via the component.
|
||||
* @param attr=null {Object} - This is usually derived from ember model `attributes` lookup, and all members of `attr.options` are optional.
|
||||
* @param model=null {DS.Model} - The Ember Data model that `attr` is defined on
|
||||
* @param [disabled=false] {Boolean} - whether the field is disabled
|
||||
* @param [showHelpText=true] {Boolean} - whether to show the tooltip with help text from OpenAPI
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
|
@ -26,6 +29,8 @@ export default Component.extend({
|
|||
layout,
|
||||
'data-test-field': true,
|
||||
classNames: ['field'],
|
||||
disabled: false,
|
||||
showHelpText: true,
|
||||
|
||||
onChange() {},
|
||||
|
||||
|
|
|
@ -162,14 +162,14 @@
|
|||
</div>
|
||||
{{else if (eq attr.type "boolean")}}
|
||||
<div class="b-checkbox">
|
||||
<input type="checkbox" id="{{attr.name}}" class="styled" checked={{get model attr.name}} onchange={{action
|
||||
<input disabled={{this.disabled}} type="checkbox" id="{{attr.name}}" class="styled" checked={{get model attr.name}} onchange={{action
|
||||
(action "setAndBroadcast" valuePath)
|
||||
value="target.checked"
|
||||
}} data-test-input={{attr.name}} />
|
||||
|
||||
<label for="{{attr.name}}" class="is-label">
|
||||
{{labelString}}
|
||||
{{#if attr.options.helpText}}
|
||||
{{#if (and this.showHelpText attr.options.helpText)}}
|
||||
{{#info-tooltip}}{{attr.options.helpText}}{{/info-tooltip}}
|
||||
{{/if}}
|
||||
</label>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
aria-hidden="true"
|
||||
class="icon-false"
|
||||
@size="l"
|
||||
@glyph="cancel-circle-outline"
|
||||
@glyph="cancel-square-outline"
|
||||
/> No
|
||||
{{/if}}
|
||||
{{else}}
|
||||
|
|
|
@ -1,55 +1,38 @@
|
|||
import EditForm from 'core/components/edit-form';
|
||||
import layout from '../templates/components/edit-form-kmip-role';
|
||||
import { Promise } from 'rsvp';
|
||||
|
||||
export default EditForm.extend({
|
||||
layout,
|
||||
display: null,
|
||||
model: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
let display = 'operationAll';
|
||||
if (this.model.operationNone) {
|
||||
display = 'operationNone';
|
||||
|
||||
if (this.model.isNew) {
|
||||
this.model.set('operationAll', true);
|
||||
}
|
||||
if (!this.model.isNew && !this.model.operationNone && !this.model.operationAll) {
|
||||
display = 'choose';
|
||||
}
|
||||
this.set('display', display);
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateModel(val) {
|
||||
// here we only want to toggle operation(None|All) because we don't want to clear the other options in
|
||||
// the case where the user clicks back to "choose" before saving
|
||||
if (val === 'operationAll') {
|
||||
this.model.set('operationNone', false);
|
||||
this.model.set('operationAll', true);
|
||||
}
|
||||
if (val === 'operationNone') {
|
||||
this.model.set('operationNone', true);
|
||||
this.model.set('operationAll', false);
|
||||
}
|
||||
toggleOperationSpecial(checked) {
|
||||
this.model.set('operationNone', !checked);
|
||||
this.model.set('operationAll', checked);
|
||||
},
|
||||
|
||||
// when operationAll is true, we want all of the items
|
||||
// to appear checked, but we don't want to override what items
|
||||
// a user has selected - so this action creates an object that we
|
||||
// pass to the FormField component as the model instead of the real model
|
||||
placeholderOrModel(isOperationAll, attr) {
|
||||
return isOperationAll ? { [attr.name]: true } : this.model;
|
||||
},
|
||||
|
||||
preSave(model) {
|
||||
let { display } = this;
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
if (display === 'choose') {
|
||||
model.set('operationNone', null);
|
||||
model.set('operationAll', null);
|
||||
return resolve(model);
|
||||
// if we have operationAll or operationNone, we want to clear
|
||||
// out the others so that display shows the right data
|
||||
if (model.operationAll || model.operationNone) {
|
||||
model.operationFieldsWithoutSpecial.forEach(field => model.set(field, null));
|
||||
}
|
||||
model.operationFields.concat(['operationAll', 'operationNone']).forEach(field => {
|
||||
// this will set operationAll or operationNone to true
|
||||
if (field === display) {
|
||||
model.set(field, true);
|
||||
} else {
|
||||
model.set(field, null);
|
||||
}
|
||||
});
|
||||
return resolve(model);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
39
ui/lib/kmip/addon/components/operation-field-display.js
Normal file
39
ui/lib/kmip/addon/components/operation-field-display.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @module OperationFieldDisplay
|
||||
* OperationFieldDisplay components are used on KMIP role show pages to display the allowed operations on that model
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <OperationFieldDisplay @model={{model}} />
|
||||
* ```
|
||||
*
|
||||
* @param model {DS.Model} - model is the KMIP role model that needs to display its allowed operations
|
||||
*
|
||||
*/
|
||||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/operation-field-display';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: '',
|
||||
model: null,
|
||||
|
||||
trueOrFalseString(model, field, trueString, falseString) {
|
||||
if (model.operationAll) {
|
||||
return trueString;
|
||||
}
|
||||
if (model.operationNone) {
|
||||
return falseString;
|
||||
}
|
||||
return model.get(field.name) ? trueString : falseString;
|
||||
},
|
||||
|
||||
actions: {
|
||||
iconClass(model, field) {
|
||||
return this.trueOrFalseString(model, field, 'icon-true', 'icon-false');
|
||||
},
|
||||
iconGlyph(model, field) {
|
||||
return this.trueOrFalseString(model, field, 'check-circle-outline', 'cancel-square-outline');
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,6 +1,5 @@
|
|||
<form {{action (queue (action "preSave" model) (perform save model)) on="submit"}}>
|
||||
<MessageError @model={{model}} data-test-edit-form-error />
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{model}} data-test-edit-form-error /> <div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="save" />
|
||||
{{#if (eq @mode "create")}}
|
||||
<FormField
|
||||
|
@ -9,32 +8,72 @@
|
|||
@model={{model}}
|
||||
/>
|
||||
{{/if}}
|
||||
<h3 class="title is-5">
|
||||
<div class="control is-flex box is-shadowless is-fullwidth is-marginless">
|
||||
<input
|
||||
data-test-input="operationNone"
|
||||
id="operationNone"
|
||||
type="checkbox"
|
||||
class="switch is-rounded is-success is-small"
|
||||
checked={{not this.model.operationNone}}
|
||||
onchange={{action "toggleOperationSpecial" value="target.checked"}}
|
||||
/>
|
||||
<label for="operationNone">
|
||||
Allow this role to perform KMIP operations
|
||||
</label>
|
||||
</div>
|
||||
{{#if (not this.model.operationNone) }}
|
||||
<Toolbar>
|
||||
<h3 class="kmip-allowed-operations-header">
|
||||
Allowed Operations
|
||||
</h3>
|
||||
{{#each (array
|
||||
(hash label="Allow all" value="operationAll")
|
||||
(hash label="Allow none" value="operationNone")
|
||||
(hash label="Let me choose" value="choose")
|
||||
) as |displayType|}}
|
||||
<RadioButton
|
||||
@value={{displayType.value}}
|
||||
@groupValue={{this.display}}
|
||||
@changed={{queue
|
||||
(action (mut this.display))
|
||||
(action "updateModel")
|
||||
}}
|
||||
@name="role-display"
|
||||
@radioId={{displayType.value}}
|
||||
@classNames="vlt-radio is-block"
|
||||
>
|
||||
<label for={{displayType.value}} />
|
||||
{{displayType.label}}
|
||||
</RadioButton>
|
||||
</Toolbar>
|
||||
<div class="box">
|
||||
<FormField
|
||||
@attr={{hash name="operationAll" type="boolean" options=(hash label="Allow this role to perform all operations")}}
|
||||
@model={{this.model}}
|
||||
/>
|
||||
<hr />
|
||||
<div class="is-flex">
|
||||
<div class="kmip-role-allowed-operations">
|
||||
{{#each-in this.model.operationFormFields.firstObject as |groupName fieldsInGroup|}}
|
||||
<h4 class="title is-7">{{groupName}}</h4>
|
||||
{{#each fieldsInGroup as |attr|}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@disabled={{or this.model.operationNone this.model.operationAll}}
|
||||
@attr={{attr}}
|
||||
@model={{compute (action "placeholderOrModel") this.model.operationAll attr}}
|
||||
@showHelpText={{false}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{#if (eq this.display "choose")}}
|
||||
<div class="box is-sideless is-shadowless is-marginless">
|
||||
{{#each this.model.operationFormFields as |attr|}}
|
||||
{{/each-in}}
|
||||
</div>
|
||||
<div class="kmip-role-allowed-operations">
|
||||
{{#each (drop 1 (or this.model.operationFormFields (array))) as |group|}}
|
||||
<div class="kmip-role-allowed-operations">
|
||||
{{#each-in group as |groupName fieldsInGroup|}}
|
||||
<h4 class="title is-7">{{groupName}}</h4>
|
||||
{{#each fieldsInGroup as |attr|}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@disabled={{or this.model.operationNone this.model.operationAll}}
|
||||
@attr={{attr}}
|
||||
@model={{compute (action "placeholderOrModel") this.model.operationAll attr}}
|
||||
@showHelpText={{false}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{/each-in}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="box is-fullwidth is-shadowless">
|
||||
<h3 class="title is-3">
|
||||
TLS
|
||||
</h3>
|
||||
{{#each this.model.tlsFormFields as |attr|}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@attr={{attr}}
|
||||
|
@ -42,8 +81,6 @@
|
|||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#each this.model.fields as |attr|}}
|
||||
<FormField
|
||||
data-test-field
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
{{#if model.operationAll}}
|
||||
<AlertInline @type="info" @message="This role allows all KMIP operations" class="is-marginless" />
|
||||
{{/if}}
|
||||
{{#each @model.operationFormFields as |group|}}
|
||||
{{#each-in group as |groupName fieldsInGroup|}}
|
||||
<InfoTableRow @alwaysRender={{true}}
|
||||
@label={{groupName}}
|
||||
@value={{true}}
|
||||
>
|
||||
<div>
|
||||
{{#each fieldsInGroup as |field|}}
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
class={{compute (action "iconClass") model field}}
|
||||
@glyph={{compute (action "iconGlyph") model field}}
|
||||
/> {{field.options.label}}
|
||||
<br />
|
||||
{{/each}}
|
||||
</div>
|
||||
</InfoTableRow>
|
||||
{{/each-in}}
|
||||
{{/each}}
|
|
@ -33,4 +33,10 @@
|
|||
</Toolbar>
|
||||
<div class="box is-fullwidth is-sideless is-shadowless">
|
||||
<FieldGroupShow @model={{model}} @showAllFields={{false}} />
|
||||
<div class="box is-fullwidth is-shadowless">
|
||||
<h2 class="title is-5">
|
||||
Allowed Operations
|
||||
</h2>
|
||||
<OperationFieldDisplay @model={{this.model}} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -73,7 +73,8 @@
|
|||
(action "refresh")
|
||||
)
|
||||
}}
|
||||
@confirmMessage={{concat "Are you sure you want to delete " list.item.id "?"}}
|
||||
@confirmTitle={{concat "Delete scope " list.item.id "?"}}
|
||||
@confirmMessage="This will permanently delete this scope and all roles and credentials contained within"
|
||||
@cancelButtonText="Cancel"
|
||||
data-test-scope-delete="true"
|
||||
>
|
||||
|
|
|
@ -4,7 +4,7 @@ import EmberObject, { computed } from '@ember/object';
|
|||
import Service from '@ember/service';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, settled, click } from '@ember/test-helpers';
|
||||
import { click, find, render, settled } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import engineResolverFor from 'ember-engines/test-support/engine-resolver-for';
|
||||
|
@ -16,6 +16,8 @@ const flash = Service.extend({
|
|||
});
|
||||
const namespace = Service.extend({});
|
||||
|
||||
const fieldToCheckbox = field => ({ name: field, type: 'boolean' });
|
||||
|
||||
const createModel = options => {
|
||||
let model = EmberObject.extend(COMPUTEDS, {
|
||||
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
|
||||
|
@ -38,7 +40,7 @@ const createModel = options => {
|
|||
'tlsClientTtl',
|
||||
],
|
||||
fields: computed('operationFields', function() {
|
||||
return this.operationFields.map(field => ({ name: field, type: 'boolean' }));
|
||||
return this.operationFields.map(fieldToCheckbox);
|
||||
}),
|
||||
destroyRecord() {
|
||||
return resolve();
|
||||
|
@ -69,15 +71,14 @@ module('Integration | Component | edit form kmip role', function(hooks) {
|
|||
this.set('model', model);
|
||||
await render(hbs`<EditFormKmipRole @model={{model}} />`);
|
||||
|
||||
assert.dom('[name=role-display]:checked').hasValue('operationAll', 'defaults to all on new models');
|
||||
assert.dom('[data-test-input="operationAll"').isChecked('sets operationAll');
|
||||
});
|
||||
|
||||
test('it renders: operationAll', async function(assert) {
|
||||
let model = createModel({ operationAll: true });
|
||||
this.set('model', model);
|
||||
await render(hbs`<EditFormKmipRole @model={{model}} />`);
|
||||
|
||||
assert.dom('[name=role-display]:checked').hasValue('operationAll', 'sets operationAll');
|
||||
assert.dom('[data-test-input="operationAll"').isChecked('sets operationAll');
|
||||
});
|
||||
|
||||
test('it renders: operationNone', async function(assert) {
|
||||
|
@ -85,7 +86,7 @@ module('Integration | Component | edit form kmip role', function(hooks) {
|
|||
this.set('model', model);
|
||||
await render(hbs`<EditFormKmipRole @model={{model}} />`);
|
||||
|
||||
assert.dom('[name=role-display]:checked').hasValue('operationNone', 'sets operationNone');
|
||||
assert.dom('[data-test-input="operationNone"]').isNotChecked('sets operationNone');
|
||||
});
|
||||
|
||||
test('it renders: choose operations', async function(assert) {
|
||||
|
@ -93,14 +94,15 @@ module('Integration | Component | edit form kmip role', function(hooks) {
|
|||
this.set('model', model);
|
||||
await render(hbs`<EditFormKmipRole @model={{model}} />`);
|
||||
|
||||
assert.dom('[name=role-display]:checked').hasValue('choose', 'sets choose');
|
||||
assert.dom('[data-test-input="operationNone"]').isChecked('sets operationNone');
|
||||
assert.dom('[data-test-input="operationAll"').isNotChecked('sets operationAll');
|
||||
});
|
||||
|
||||
let savingTests = [
|
||||
[
|
||||
'setting operationAll',
|
||||
{ operationNone: true, operationGet: true },
|
||||
'operationAll',
|
||||
'operationNone',
|
||||
{
|
||||
operationAll: true,
|
||||
operationNone: false,
|
||||
|
@ -108,7 +110,7 @@ module('Integration | Component | edit form kmip role', function(hooks) {
|
|||
},
|
||||
{
|
||||
operationGet: null,
|
||||
operationNone: null,
|
||||
operationNone: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -123,16 +125,16 @@ module('Integration | Component | edit form kmip role', function(hooks) {
|
|||
{
|
||||
operationNone: true,
|
||||
operationCreate: null,
|
||||
operationAll: null,
|
||||
operationAll: false,
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
'setting choose, and selecting an additional item',
|
||||
{ operationAll: true, operationGet: true, operationCreate: true },
|
||||
'choose,operationDestroy',
|
||||
'operationAll,operationDestroy',
|
||||
{
|
||||
operationAll: true,
|
||||
operationAll: false,
|
||||
operationCreate: true,
|
||||
operationGet: true,
|
||||
},
|
||||
|
@ -140,8 +142,7 @@ module('Integration | Component | edit form kmip role', function(hooks) {
|
|||
operationGet: true,
|
||||
operationCreate: true,
|
||||
operationDestroy: true,
|
||||
operationAll: null,
|
||||
operationNone: null,
|
||||
operationAll: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
@ -159,7 +160,6 @@ module('Integration | Component | edit form kmip role', function(hooks) {
|
|||
for (let beforeStateKey of Object.keys(stateBeforeSave)) {
|
||||
assert.equal(model.get(beforeStateKey), stateBeforeSave[beforeStateKey], `sets ${beforeStateKey}`);
|
||||
}
|
||||
assert.dom('[name=role-display]:checked').hasValue(clickTargets[0], `sets clickTargets[0]`);
|
||||
|
||||
click('[data-test-edit-form-submit]');
|
||||
|
||||
|
|
Loading…
Reference in a new issue