mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 12:26:34 +02:00
UI - kmip role edit form (#6973)
* extend edit form with a custom kmip role form * adjust model fields and use new kmip role edit form * customize serialize adapter hook for kmip/role * refresh list routes in the list mixin * style up kmip role edit form * return a promise from preSave so that the queue helper waits to call save * add serialize tests for the kmip/role adapter * rename component to edit-form-kmip-role * add tests for edit-form-kmip-role * add some clarifying comments * make input more realistic in tests * remove delete toolbar
This commit is contained in:
parent
19b1a30071
commit
deb137ebf0
@ -11,7 +11,7 @@ export default BaseAdapter.extend({
|
||||
},
|
||||
name
|
||||
);
|
||||
return this.ajax(url, 'POST', { data: snapshot.serialize() }).then(() => {
|
||||
return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then(() => {
|
||||
return {
|
||||
id: name,
|
||||
name,
|
||||
@ -19,6 +19,21 @@ export default BaseAdapter.extend({
|
||||
});
|
||||
},
|
||||
|
||||
serialize(snapshot) {
|
||||
// the endpoint here won't allow sending `operation_all` and `operation_none` at the same time or with
|
||||
// other values, so we manually check for them and send an abbreviated object
|
||||
let json = snapshot.serialize();
|
||||
if (json.operation_all) {
|
||||
return { operation_all: true };
|
||||
}
|
||||
if (json.operation_none) {
|
||||
return { operation_none: true };
|
||||
}
|
||||
delete json.operation_none;
|
||||
delete json.operation_all;
|
||||
return json;
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createRecord(...arguments);
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import DS from 'ember-data';
|
||||
import fieldToAttrs from 'vault/utils/field-to-attrs';
|
||||
import { computed } from '@ember/object';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import fieldToAttrs from 'vault/utils/field-to-attrs';
|
||||
|
||||
const { attr } = DS;
|
||||
export default DS.Model.extend({
|
||||
@ -11,18 +12,15 @@ export default DS.Model.extend({
|
||||
return `/v1/${path}/scope/example/role/example?help=1`;
|
||||
},
|
||||
|
||||
name: attr('string'),
|
||||
allowedOperations: attr(),
|
||||
name: attr({ readOnly: true }),
|
||||
fieldGroups: computed(function() {
|
||||
let fields = this.newFields.without('role');
|
||||
|
||||
const groups = [
|
||||
{
|
||||
default: ['name'],
|
||||
},
|
||||
{ 'Allowed Operations': fields },
|
||||
];
|
||||
|
||||
const groups = [{ 'Allowed Operations': fields }];
|
||||
return fieldToAttrs(this, groups);
|
||||
}),
|
||||
|
||||
fields: computed(function() {
|
||||
let fields = this.newFields.removeObjects(['role', 'operationAll', 'operationNone']);
|
||||
return expandAttributeMeta(this, fields);
|
||||
}),
|
||||
});
|
||||
|
||||
39
ui/app/styles/components/vlt-radio.scss
Normal file
39
ui/app/styles/components/vlt-radio.scss
Normal file
@ -0,0 +1,39 @@
|
||||
.vlt-radio {
|
||||
position: relative;
|
||||
input[type='radio'] {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
input[type='radio'] + label {
|
||||
content: '';
|
||||
border: 1px solid $grey-light;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin: 0.25rem 0;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 0.3rem;
|
||||
}
|
||||
|
||||
input[type='radio']:checked + label {
|
||||
content: '';
|
||||
background: $blue;
|
||||
border: 1px solid $blue;
|
||||
box-shadow: inset 0 0 0 0.15rem $white;
|
||||
position: relative;
|
||||
left: 0;
|
||||
}
|
||||
input[type='radio']:focus + label {
|
||||
content: '';
|
||||
box-shadow: 0 0 10px 1px rgba($blue, 0.4), inset 0 0 0 0.15rem $white;
|
||||
position: relative;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@ -85,6 +85,7 @@
|
||||
@import './components/unseal-warning';
|
||||
@import './components/ui-wizard';
|
||||
@import './components/vault-loading';
|
||||
@import './components/vlt-radio';
|
||||
|
||||
// bulma-free-zone
|
||||
@import './components/hs-icon';
|
||||
|
||||
@ -27,4 +27,17 @@ export default Mixin.create({
|
||||
controller.set('filter', null);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
willTransition(transition) {
|
||||
window.scrollTo(0, 0);
|
||||
if (transition.targetName !== this.routeName) {
|
||||
this.store.clearAllDatasets();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
reload() {
|
||||
this.store.clearAllDatasets();
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
55
ui/lib/kmip/addon/components/edit-form-kmip-role.js
Normal file
55
ui/lib/kmip/addon/components/edit-form-kmip-role.js
Normal file
@ -0,0 +1,55 @@
|
||||
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,
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
let display = 'operationAll';
|
||||
if (this.model.operationNone) {
|
||||
display = 'operationNone';
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
preSave(model) {
|
||||
let { display } = this;
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
if (display === 'choose') {
|
||||
model.set('operationNone', null);
|
||||
model.set('operationAll', null);
|
||||
return resolve(model);
|
||||
}
|
||||
model.newFields.without('role').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);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,63 @@
|
||||
<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">
|
||||
<NamespaceReminder @mode="save" />
|
||||
{{#if (eq @mode "create")}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@attr={{hash name="name" type="string"}}
|
||||
@model={{model}}
|
||||
/>
|
||||
{{/if}}
|
||||
<h3 class="title is-5">
|
||||
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>
|
||||
{{/each}}
|
||||
{{#if (eq this.display "choose")}}
|
||||
<div class="box is-sideless is-shadowless is-marginless">
|
||||
{{#each this.model.fields as |attr|}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@attr={{attr}}
|
||||
@model={{model}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button type="submit" data-test-edit-form-submit class="button is-primary {{if save.isRunning 'loading'}}" disabled={{save.isRunning}}>
|
||||
{{saveButtonText}}
|
||||
</button>
|
||||
</div>
|
||||
{{#if cancelLinkParams}}
|
||||
<div class="control">
|
||||
{{#link-to params=cancelLinkParams class="button"}}
|
||||
Cancel
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -8,6 +8,8 @@
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<EditForm @model={{model}}
|
||||
<EditFormKmipRole
|
||||
@model={{model}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.kmip.role" this.scope this.role}}
|
||||
@cancelLinkParams={{array "role" this.scope this.role}} />
|
||||
@cancelLinkParams={{array "role" this.scope this.role}}
|
||||
/>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<EditForm
|
||||
<EditFormKmipRole
|
||||
@model={{model}}
|
||||
@mode="create"
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.kmip.scope.roles" this.scope}}
|
||||
|
||||
160
ui/tests/integration/components/edit-form-kmip-role-test.js
Normal file
160
ui/tests/integration/components/edit-form-kmip-role-test.js
Normal file
@ -0,0 +1,160 @@
|
||||
import { later, run } from '@ember/runloop';
|
||||
import { resolve } from 'rsvp';
|
||||
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 hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import engineResolverFor from 'ember-engines/test-support/engine-resolver-for';
|
||||
const resolver = engineResolverFor('kmip');
|
||||
|
||||
const flash = Service.extend({
|
||||
success: sinon.stub(),
|
||||
});
|
||||
const namespace = Service.extend({});
|
||||
|
||||
const createModel = options => {
|
||||
let model = EmberObject.extend({
|
||||
fields: computed('newFields', function() {
|
||||
return this.newFields.map(field => ({ name: field, type: 'boolean' }));
|
||||
}),
|
||||
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
|
||||
newFields: ['operationAll', 'operationNone', 'operationGet', 'operationCreate', 'operationDestroy'],
|
||||
destroyRecord() {
|
||||
return resolve();
|
||||
},
|
||||
save() {
|
||||
return resolve();
|
||||
},
|
||||
rollbackAttributes() {},
|
||||
});
|
||||
return model.create({
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
module('Integration | Component | edit form kmip role', function(hooks) {
|
||||
setupRenderingTest(hooks, { resolver });
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
run(() => {
|
||||
this.owner.unregister('service:flash-messages');
|
||||
this.owner.register('service:flash-messages', flash);
|
||||
this.owner.register('service:namespace', namespace);
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders: new model', async function(assert) {
|
||||
let model = createModel({ isNew: true });
|
||||
this.set('model', model);
|
||||
await render(hbs`<EditFormKmipRole @model={{model}} />`);
|
||||
|
||||
assert.dom('[name=role-display]:checked').hasValue('operationAll', 'defaults to all on new models');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
test('it renders: operationNone', async function(assert) {
|
||||
let model = createModel({ operationNone: true });
|
||||
this.set('model', model);
|
||||
await render(hbs`<EditFormKmipRole @model={{model}} />`);
|
||||
|
||||
assert.dom('[name=role-display]:checked').hasValue('operationNone', 'sets operationNone');
|
||||
});
|
||||
|
||||
test('it renders: choose operations', async function(assert) {
|
||||
let model = createModel({ operationGet: true });
|
||||
this.set('model', model);
|
||||
await render(hbs`<EditFormKmipRole @model={{model}} />`);
|
||||
|
||||
assert.dom('[name=role-display]:checked').hasValue('choose', 'sets choose');
|
||||
});
|
||||
|
||||
let savingTests = [
|
||||
[
|
||||
'setting operationAll',
|
||||
{ operationNone: true, operationGet: true },
|
||||
'operationAll',
|
||||
{
|
||||
operationAll: true,
|
||||
operationNone: false,
|
||||
operationGet: true,
|
||||
},
|
||||
{
|
||||
operationGet: null,
|
||||
operationNone: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
'setting operationNone',
|
||||
{ operationAll: true, operationCreate: true },
|
||||
'operationNone',
|
||||
{
|
||||
operationAll: false,
|
||||
operationNone: true,
|
||||
operationCreate: true,
|
||||
},
|
||||
{
|
||||
operationNone: true,
|
||||
operationCreate: null,
|
||||
operationAll: null,
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
'setting choose, and selecting an additional item',
|
||||
{ operationAll: true, operationGet: true, operationCreate: true },
|
||||
'choose,operationDestroy',
|
||||
{
|
||||
operationAll: true,
|
||||
operationCreate: true,
|
||||
operationGet: true,
|
||||
},
|
||||
{
|
||||
operationGet: true,
|
||||
operationCreate: true,
|
||||
operationDestroy: true,
|
||||
operationAll: null,
|
||||
operationNone: null,
|
||||
},
|
||||
],
|
||||
];
|
||||
for (let testCase of savingTests) {
|
||||
let [name, initialState, displayClicks, stateBeforeSave, stateAfterSave] = testCase;
|
||||
test(name, async function(assert) {
|
||||
let model = createModel(initialState);
|
||||
this.set('model', model);
|
||||
let clickTargets = displayClicks.split(',');
|
||||
await render(hbs`<EditFormKmipRole @model={{model}} />`);
|
||||
|
||||
for (let clickTarget of clickTargets) {
|
||||
await click(`label[for=${clickTarget}]`);
|
||||
}
|
||||
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]');
|
||||
|
||||
later(() => run.cancelTimers(), 50);
|
||||
return settled().then(() => {
|
||||
for (let afterStateKey of Object.keys(stateAfterSave)) {
|
||||
assert.equal(
|
||||
model.get(afterStateKey),
|
||||
stateAfterSave[afterStateKey],
|
||||
`sets ${afterStateKey} on save`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
59
ui/tests/unit/adapters/kmip/role-test.js
Normal file
59
ui/tests/unit/adapters/kmip/role-test.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Adapter | kmip/role', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let serializeTests = [
|
||||
[
|
||||
'operation_all is the only item present after serialization',
|
||||
{
|
||||
serialize() {
|
||||
return { operation_all: true, operation_get: true, operation_create: true };
|
||||
},
|
||||
},
|
||||
{
|
||||
operation_all: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'operation_none is the only item present after serialization',
|
||||
{
|
||||
serialize() {
|
||||
return { operation_none: true, operation_get: true, operation_add_attribute: true };
|
||||
},
|
||||
},
|
||||
{
|
||||
operation_none: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'operation_all and operation_none are removed if not truthy',
|
||||
{
|
||||
serialize() {
|
||||
return {
|
||||
operation_all: false,
|
||||
operation_none: false,
|
||||
operation_get: true,
|
||||
operation_add_attribute: true,
|
||||
operation_destroy: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
operation_get: true,
|
||||
operation_add_attribute: true,
|
||||
operation_destroy: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
for (let testCase of serializeTests) {
|
||||
let [name, snapshotStub, expected] = testCase;
|
||||
|
||||
test(`adapter serialize: ${name}`, function(assert) {
|
||||
let adapter = this.owner.lookup('adapter:kmip/role');
|
||||
let result = adapter.serialize(snapshotStub);
|
||||
assert.deepEqual(result, expected, 'output matches expected');
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user