diff --git a/ui/app/adapters/kmip/role.js b/ui/app/adapters/kmip/role.js index 0777e85518..dc7a9e60f2 100644 --- a/ui/app/adapters/kmip/role.js +++ b/ui/app/adapters/kmip/role.js @@ -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); }, diff --git a/ui/app/models/kmip/role.js b/ui/app/models/kmip/role.js index a553125e5d..ee74124b4e 100644 --- a/ui/app/models/kmip/role.js +++ b/ui/app/models/kmip/role.js @@ -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); + }), }); diff --git a/ui/app/styles/components/vlt-radio.scss b/ui/app/styles/components/vlt-radio.scss new file mode 100644 index 0000000000..bda8e560d9 --- /dev/null +++ b/ui/app/styles/components/vlt-radio.scss @@ -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; + } +} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index 988319477f..3a4c90ecbb 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -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'; diff --git a/ui/lib/core/addon/mixins/list-route.js b/ui/lib/core/addon/mixins/list-route.js index 8799b7a008..45d5fca6b1 100644 --- a/ui/lib/core/addon/mixins/list-route.js +++ b/ui/lib/core/addon/mixins/list-route.js @@ -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(); + }, + }, }); diff --git a/ui/lib/kmip/addon/components/edit-form-kmip-role.js b/ui/lib/kmip/addon/components/edit-form-kmip-role.js new file mode 100644 index 0000000000..47b529a951 --- /dev/null +++ b/ui/lib/kmip/addon/components/edit-form-kmip-role.js @@ -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); + }); + }, + }, +}); diff --git a/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs b/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs new file mode 100644 index 0000000000..69fa889ba6 --- /dev/null +++ b/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs @@ -0,0 +1,63 @@ +
+ +
+ + {{#if (eq @mode "create")}} + + {{/if}} +

+ Allowed Operations +

+ {{#each (array + (hash label="Allow all" value="operationAll") + (hash label="Allow none" value="operationNone") + (hash label="Let me choose" value="choose") + ) as |displayType|}} + + + {{/each}} + {{#if (eq this.display "choose")}} +
+ {{#each this.model.fields as |attr|}} + + {{/each}} +
+ {{/if}} +
+
+
+
+ +
+ {{#if cancelLinkParams}} +
+ {{#link-to params=cancelLinkParams class="button"}} + Cancel + {{/link-to}} +
+ {{/if}} +
+
+ diff --git a/ui/lib/kmip/addon/templates/role/edit.hbs b/ui/lib/kmip/addon/templates/role/edit.hbs index 32dcdae378..aa0a10f0f8 100644 --- a/ui/lib/kmip/addon/templates/role/edit.hbs +++ b/ui/lib/kmip/addon/templates/role/edit.hbs @@ -8,6 +8,8 @@ - + @cancelLinkParams={{array "role" this.scope this.role}} +/> diff --git a/ui/lib/kmip/addon/templates/scope/roles/create.hbs b/ui/lib/kmip/addon/templates/scope/roles/create.hbs index 6eedad1704..de76e1fd70 100644 --- a/ui/lib/kmip/addon/templates/scope/roles/create.hbs +++ b/ui/lib/kmip/addon/templates/scope/roles/create.hbs @@ -8,7 +8,7 @@ - { + 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``); + + 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``); + + 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``); + + 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``); + + 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``); + + 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` + ); + } + }); + }); + } +}); diff --git a/ui/tests/unit/adapters/kmip/role-test.js b/ui/tests/unit/adapters/kmip/role-test.js new file mode 100644 index 0000000000..d33214665a --- /dev/null +++ b/ui/tests/unit/adapters/kmip/role-test.js @@ -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'); + }); + } +});