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:
Matthew Irish 2019-07-23 16:00:00 -05:00 committed by GitHub
parent 18fa9e418c
commit b0dfbde741
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 248 additions and 105 deletions

View file

@ -14,6 +14,8 @@ export default BaseAdapter.extend({
}, },
deleteRecord(store, type, snapshot) { 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');
}, },
}); });

View file

@ -15,8 +15,12 @@ export const COMPUTEDS = {
return this.operationFields.slice().removeObjects(['operationAll', 'operationNone']); return this.operationFields.slice().removeObjects(['operationAll', 'operationNone']);
}), }),
nonOperationFields: computed('operationFields', function() { tlsFields: computed(function() {
let excludeFields = ['role'].concat(this.operationFields); return ['tlsClientKeyBits', 'tlsClientKeyType', 'tlsClientTtl'];
}),
nonOperationFields: computed('tlsFields', 'operationFields', function() {
let excludeFields = ['role'].concat(this.operationFields, this.tlsFields);
return this.newFields.slice().removeObjects(excludeFields); return this.newFields.slice().removeObjects(excludeFields);
}), }),
}; };
@ -29,14 +33,43 @@ const Model = DS.Model.extend(COMPUTEDS, {
getHelpUrl(path) { getHelpUrl(path) {
return `/v1/${path}/scope/example/role/example?help=1`; return `/v1/${path}/scope/example/role/example?help=1`;
}, },
fieldGroups: computed('fields', 'nonOperationFields', function() { fieldGroups: computed('fields', 'tlsFields', 'nonOperationFields', function() {
const groups = [{ default: this.nonOperationFields }, { 'Allowed Operations': this.operationFields }]; const groups = [{ TLS: this.tlsFields }];
if (this.nonOperationFields.length) {
groups.unshift({ default: this.nonOperationFields });
}
let ret = fieldToAttrs(this, groups); let ret = fieldToAttrs(this, groups);
return ret; return ret;
}), }),
operationFormFields: computed('operationFieldsWithoutSpecial', function() { 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() { fields: computed('nonOperationFields', function() {
return expandAttributeMeta(this, this.nonOperationFields); return expandAttributeMeta(this, this.nonOperationFields);

View 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;
}

View file

@ -60,6 +60,7 @@
@import './components/init-illustration'; @import './components/init-illustration';
@import './components/info-table-row'; @import './components/info-table-row';
@import './components/input-hint'; @import './components/input-hint';
@import './components/kmip-role-edit';
@import './components/linked-block'; @import './components/linked-block';
@import './components/list-item-row'; @import './components/list-item-row';
@import './components/list-pagination'; @import './components/list-pagination';

View file

@ -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 [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 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 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, layout,
'data-test-field': true, 'data-test-field': true,
classNames: ['field'], classNames: ['field'],
disabled: false,
showHelpText: true,
onChange() {}, onChange() {},

View file

@ -162,14 +162,14 @@
</div> </div>
{{else if (eq attr.type "boolean")}} {{else if (eq attr.type "boolean")}}
<div class="b-checkbox"> <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) (action "setAndBroadcast" valuePath)
value="target.checked" value="target.checked"
}} data-test-input={{attr.name}} /> }} data-test-input={{attr.name}} />
<label for="{{attr.name}}" class="is-label"> <label for="{{attr.name}}" class="is-label">
{{labelString}} {{labelString}}
{{#if attr.options.helpText}} {{#if (and this.showHelpText attr.options.helpText)}}
{{#info-tooltip}}{{attr.options.helpText}}{{/info-tooltip}} {{#info-tooltip}}{{attr.options.helpText}}{{/info-tooltip}}
{{/if}} {{/if}}
</label> </label>

View file

@ -18,7 +18,7 @@
aria-hidden="true" aria-hidden="true"
class="icon-false" class="icon-false"
@size="l" @size="l"
@glyph="cancel-circle-outline" @glyph="cancel-square-outline"
/> No /> No
{{/if}} {{/if}}
{{else}} {{else}}

View file

@ -1,55 +1,38 @@
import EditForm from 'core/components/edit-form'; import EditForm from 'core/components/edit-form';
import layout from '../templates/components/edit-form-kmip-role'; import layout from '../templates/components/edit-form-kmip-role';
import { Promise } from 'rsvp';
export default EditForm.extend({ export default EditForm.extend({
layout, layout,
display: null, model: null,
init() { init() {
this._super(...arguments); this._super(...arguments);
let display = 'operationAll';
if (this.model.operationNone) { if (this.model.isNew) {
display = 'operationNone'; this.model.set('operationAll', true);
} }
if (!this.model.isNew && !this.model.operationNone && !this.model.operationAll) {
display = 'choose';
}
this.set('display', display);
}, },
actions: { actions: {
updateModel(val) { toggleOperationSpecial(checked) {
// here we only want to toggle operation(None|All) because we don't want to clear the other options in this.model.set('operationNone', !checked);
// the case where the user clicks back to "choose" before saving this.model.set('operationAll', checked);
if (val === 'operationAll') { },
this.model.set('operationNone', false);
this.model.set('operationAll', true); // when operationAll is true, we want all of the items
} // to appear checked, but we don't want to override what items
if (val === 'operationNone') { // a user has selected - so this action creates an object that we
this.model.set('operationNone', true); // pass to the FormField component as the model instead of the real model
this.model.set('operationAll', false); placeholderOrModel(isOperationAll, attr) {
} return isOperationAll ? { [attr.name]: true } : this.model;
}, },
preSave(model) { preSave(model) {
let { display } = this; // if we have operationAll or operationNone, we want to clear
// out the others so that display shows the right data
return new Promise(function(resolve) { if (model.operationAll || model.operationNone) {
if (display === 'choose') { model.operationFieldsWithoutSpecial.forEach(field => model.set(field, null));
model.set('operationNone', null); }
model.set('operationAll', null);
return resolve(model);
}
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);
});
}, },
}, },
}); });

View 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');
},
},
});

View file

@ -1,6 +1,5 @@
<form {{action (queue (action "preSave" model) (perform save model)) on="submit"}}> <form {{action (queue (action "preSave" model) (perform save model)) on="submit"}}>
<MessageError @model={{model}} data-test-edit-form-error /> <MessageError @model={{model}} data-test-edit-form-error /> <div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="save" /> <NamespaceReminder @mode="save" />
{{#if (eq @mode "create")}} {{#if (eq @mode "create")}}
<FormField <FormField
@ -9,48 +8,86 @@
@model={{model}} @model={{model}}
/> />
{{/if}} {{/if}}
<h3 class="title is-5"> <div class="control is-flex box is-shadowless is-fullwidth is-marginless">
Allowed Operations <input
</h3> data-test-input="operationNone"
{{#each (array id="operationNone"
(hash label="Allow all" value="operationAll") type="checkbox"
(hash label="Allow none" value="operationNone") class="switch is-rounded is-success is-small"
(hash label="Let me choose" value="choose") checked={{not this.model.operationNone}}
) as |displayType|}} onchange={{action "toggleOperationSpecial" value="target.checked"}}
<RadioButton />
@value={{displayType.value}} <label for="operationNone">
@groupValue={{this.display}} Allow this role to perform KMIP operations
@changed={{queue </label>
(action (mut this.display)) </div>
(action "updateModel") {{#if (not this.model.operationNone) }}
}} <Toolbar>
@name="role-display" <h3 class="kmip-allowed-operations-header">
@radioId={{displayType.value}} Allowed Operations
@classNames="vlt-radio is-block" </h3>
> </Toolbar>
<label for={{displayType.value}} /> <div class="box">
{{displayType.label}} <FormField
</RadioButton> @attr={{hash name="operationAll" type="boolean" options=(hash label="Allow this role to perform all operations")}}
{{/each}} @model={{this.model}}
{{#if (eq this.display "choose")}} />
<div class="box is-sideless is-shadowless is-marginless"> <hr />
{{#each this.model.operationFormFields as |attr|}} <div class="is-flex">
<FormField <div class="kmip-role-allowed-operations">
data-test-field {{#each-in this.model.operationFormFields.firstObject as |groupName fieldsInGroup|}}
@attr={{attr}} <h4 class="title is-7">{{groupName}}</h4>
@model={{model}} {{#each fieldsInGroup as |attr|}}
/> <FormField
{{/each}} 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>
<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> </div>
{{/if}} {{/if}}
<div class="box is-fullwidth is-shadowless">
{{#each this.model.fields as |attr|}} <h3 class="title is-3">
<FormField TLS
data-test-field </h3>
@attr={{attr}} {{#each this.model.tlsFormFields as |attr|}}
@model={{model}} <FormField
/> data-test-field
{{/each}} @attr={{attr}}
@model={{model}}
/>
{{/each}}
</div>
{{#each this.model.fields as |attr|}}
<FormField
data-test-field
@attr={{attr}}
@model={{model}}
/>
{{/each}}
</div> </div>
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless"> <div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<div class="field is-grouped"> <div class="field is-grouped">

View file

@ -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}}

View file

@ -33,4 +33,10 @@
</Toolbar> </Toolbar>
<div class="box is-fullwidth is-sideless is-shadowless"> <div class="box is-fullwidth is-sideless is-shadowless">
<FieldGroupShow @model={{model}} @showAllFields={{false}} /> <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> </div>

View file

@ -73,7 +73,8 @@
(action "refresh") (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" @cancelButtonText="Cancel"
data-test-scope-delete="true" data-test-scope-delete="true"
> >

View file

@ -4,7 +4,7 @@ import EmberObject, { computed } from '@ember/object';
import Service from '@ember/service'; import Service from '@ember/service';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-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 hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon'; import sinon from 'sinon';
import engineResolverFor from 'ember-engines/test-support/engine-resolver-for'; import engineResolverFor from 'ember-engines/test-support/engine-resolver-for';
@ -16,6 +16,8 @@ const flash = Service.extend({
}); });
const namespace = Service.extend({}); const namespace = Service.extend({});
const fieldToCheckbox = field => ({ name: field, type: 'boolean' });
const createModel = options => { const createModel = options => {
let model = EmberObject.extend(COMPUTEDS, { let model = EmberObject.extend(COMPUTEDS, {
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */ /* eslint-disable ember/avoid-leaking-state-in-ember-objects */
@ -38,7 +40,7 @@ const createModel = options => {
'tlsClientTtl', 'tlsClientTtl',
], ],
fields: computed('operationFields', function() { fields: computed('operationFields', function() {
return this.operationFields.map(field => ({ name: field, type: 'boolean' })); return this.operationFields.map(fieldToCheckbox);
}), }),
destroyRecord() { destroyRecord() {
return resolve(); return resolve();
@ -69,15 +71,14 @@ module('Integration | Component | edit form kmip role', function(hooks) {
this.set('model', model); this.set('model', model);
await render(hbs`<EditFormKmipRole @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) { test('it renders: operationAll', async function(assert) {
let model = createModel({ operationAll: true }); let model = createModel({ operationAll: true });
this.set('model', model); this.set('model', model);
await render(hbs`<EditFormKmipRole @model={{model}} />`); await render(hbs`<EditFormKmipRole @model={{model}} />`);
assert.dom('[data-test-input="operationAll"').isChecked('sets operationAll');
assert.dom('[name=role-display]:checked').hasValue('operationAll', 'sets operationAll');
}); });
test('it renders: operationNone', async function(assert) { test('it renders: operationNone', async function(assert) {
@ -85,7 +86,7 @@ module('Integration | Component | edit form kmip role', function(hooks) {
this.set('model', model); this.set('model', model);
await render(hbs`<EditFormKmipRole @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) { 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); this.set('model', model);
await render(hbs`<EditFormKmipRole @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 = [ let savingTests = [
[ [
'setting operationAll', 'setting operationAll',
{ operationNone: true, operationGet: true }, { operationNone: true, operationGet: true },
'operationAll', 'operationNone',
{ {
operationAll: true, operationAll: true,
operationNone: false, operationNone: false,
@ -108,7 +110,7 @@ module('Integration | Component | edit form kmip role', function(hooks) {
}, },
{ {
operationGet: null, operationGet: null,
operationNone: null, operationNone: false,
}, },
], ],
[ [
@ -123,16 +125,16 @@ module('Integration | Component | edit form kmip role', function(hooks) {
{ {
operationNone: true, operationNone: true,
operationCreate: null, operationCreate: null,
operationAll: null, operationAll: false,
}, },
], ],
[ [
'setting choose, and selecting an additional item', 'setting choose, and selecting an additional item',
{ operationAll: true, operationGet: true, operationCreate: true }, { operationAll: true, operationGet: true, operationCreate: true },
'choose,operationDestroy', 'operationAll,operationDestroy',
{ {
operationAll: true, operationAll: false,
operationCreate: true, operationCreate: true,
operationGet: true, operationGet: true,
}, },
@ -140,8 +142,7 @@ module('Integration | Component | edit form kmip role', function(hooks) {
operationGet: true, operationGet: true,
operationCreate: true, operationCreate: true,
operationDestroy: true, operationDestroy: true,
operationAll: null, operationAll: false,
operationNone: null,
}, },
], ],
]; ];
@ -159,7 +160,6 @@ module('Integration | Component | edit form kmip role', function(hooks) {
for (let beforeStateKey of Object.keys(stateBeforeSave)) { for (let beforeStateKey of Object.keys(stateBeforeSave)) {
assert.equal(model.get(beforeStateKey), stateBeforeSave[beforeStateKey], `sets ${beforeStateKey}`); 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]'); click('[data-test-edit-form-submit]');