Skip to content

Commit

Permalink
UI: refactor KMIP role model (#28418)
Browse files Browse the repository at this point in the history
* update kmip/role model and adapter

* New KMIP role form component

* cleanup on kmip role adapter/model

* fix role details view

* update tests to check for kmip role form and details validity

* cleanup

* Add kmip-role-fields test

* add headers, remove old component

* Address PR comments
  • Loading branch information
hashishaw committed Sep 20, 2024
1 parent 2ce6877 commit 520f141
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 257 deletions.
33 changes: 18 additions & 15 deletions ui/app/adapters/kmip/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
import BaseAdapter from './base';
import { decamelize } from '@ember/string';
import { getProperties } from '@ember/object';
import { nonOperationFields } from 'vault/utils/kmip-role-fields';

export default BaseAdapter.extend({
createRecord(store, type, snapshot) {
const name = snapshot.id || snapshot.attr('name');
const name = snapshot.id || snapshot.record.role;
const url = this._url(
type.modelName,
{
Expand All @@ -18,18 +19,20 @@ export default BaseAdapter.extend({
},
name
);
return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then(() => {
const data = this.serialize(snapshot);
return this.ajax(url, 'POST', { data }).then(() => {
return {
id: name,
name,
role: name,
backend: snapshot.record.backend,
scope: snapshot.record.scope,
};
});
},

deleteRecord(store, type, snapshot) {
const name = snapshot.id || snapshot.attr('name');
// records must always have IDs
const name = snapshot.id;
const url = this._url(
type.modelName,
{
Expand All @@ -41,35 +44,35 @@ export default BaseAdapter.extend({
return this.ajax(url, 'DELETE');
},

updateRecord() {
return this.createRecord(...arguments);
},

serialize(snapshot) {
// the endpoint here won't allow sending `operation_all` and `operation_none` at the same time or with
// other operation_ values, so we manually check for them and send an abbreviated object
const json = snapshot.serialize();
const keys = snapshot.record.nonOperationFields.map(decamelize);
const nonOperationFields = getProperties(json, keys);
for (const field in nonOperationFields) {
if (nonOperationFields[field] == null) {
delete nonOperationFields[field];
const keys = nonOperationFields(snapshot.record.editableFields).map(decamelize);
const nonOp = getProperties(json, keys);
for (const field in nonOp) {
if (nonOp[field] == null) {
delete nonOp[field];
}
}
if (json.operation_all) {
return {
operation_all: true,
...nonOperationFields,
...nonOp,
};
}
if (json.operation_none) {
return {
operation_none: true,
...nonOperationFields,
...nonOp,
};
}
delete json.operation_none;
delete json.operation_all;
return json;
},

updateRecord() {
return this.createRecord(...arguments);
},
});
75 changes: 26 additions & 49 deletions ui/app/models/kmip/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,35 @@
*/

import Model, { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import apiPath from 'vault/utils/api-path';
import lazyCapabilities from 'vault/macros/lazy-capabilities';
import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
import { operationFields, operationFieldsWithoutSpecial, tlsFields } from 'vault/utils/kmip-role-fields';
import { removeManyFromArray } from 'vault/helpers/remove-from-array';

const COMPUTEDS = {
operationFields: computed('newFields', function () {
return this.newFields.filter((key) => key.startsWith('operation'));
}),
@withExpandedAttributes()
export default class KmipRoleModel extends Model {
@attr({ readOnly: true }) backend;
@attr({ readOnly: true }) scope;

operationFieldsWithoutSpecial: computed('operationFields', function () {
return removeManyFromArray(this.operationFields, ['operationAll', 'operationNone']);
}),
get editableFields() {
return Object.keys(this.allByKey).filter((k) => !['backend', 'scope', 'role'].includes(k));
}

tlsFields: computed(function () {
return ['tlsClientKeyBits', 'tlsClientKeyType', 'tlsClientTtl'];
}),

// For rendering on the create/edit pages
defaultFields: computed('newFields', 'operationFields', 'tlsFields', function () {
const excludeFields = ['role'].concat(this.operationFields, this.tlsFields);
return removeManyFromArray(this.newFields, excludeFields);
}),

// For adapter/serializer
nonOperationFields: computed('newFields', 'operationFields', function () {
return removeManyFromArray(this.newFields, this.operationFields);
}),
};

export default Model.extend(COMPUTEDS, {
backend: attr({ readOnly: true }),
scope: attr({ readOnly: true }),
name: attr({ readOnly: true }),

fieldGroups: computed('fields', 'defaultFields.length', 'tlsFields', function () {
const groups = [{ TLS: this.tlsFields }];
if (this.defaultFields.length) {
groups.unshift({ default: this.defaultFields });
get fieldGroups() {
const tls = tlsFields();
const groups = [{ TLS: tls }];
// op fields are shown in OperationFieldDisplay
const opFields = operationFields(this.editableFields);
// not op fields, tls fields, or role/backend/scope
const defaultFields = this.editableFields.filter((f) => ![...opFields, ...tls].includes(f));
if (defaultFields.length) {
groups.unshift({ default: defaultFields });
}
const ret = fieldToAttrs(this, groups);
return ret;
}),
return this._expandGroups(groups);
}

operationFormFields: computed('operationFieldsWithoutSpecial', function () {
get operationFormFields() {
const objects = [
'operationCreate',
'operationActivate',
Expand All @@ -62,7 +45,7 @@ export default Model.extend(COMPUTEDS, {

const attributes = ['operationAddAttribute', 'operationGetAttributes'];
const server = ['operationDiscoverVersions'];
const others = removeManyFromArray(this.operationFieldsWithoutSpecial, [
const others = removeManyFromArray(operationFieldsWithoutSpecial(this.editableFields), [
...objects,
...attributes,
...server,
Expand All @@ -77,14 +60,8 @@ export default Model.extend(COMPUTEDS, {
Other: others,
});
}
return fieldToAttrs(this, groups);
}),
tlsFormFields: computed('tlsFields', function () {
return expandAttributeMeta(this, this.tlsFields);
}),
fields: computed('defaultFields', function () {
return expandAttributeMeta(this, this.defaultFields);
}),
return this._expandGroups(groups);
}

updatePath: lazyCapabilities(apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`, 'backend', 'scope', 'id'),
});
@lazyCapabilities(apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`, 'backend', 'scope', 'id') updatePath;
}
5 changes: 4 additions & 1 deletion ui/app/styles/components/kmip-role-edit.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
* SPDX-License-Identifier: BUSL-1.1
*/

.kmip-role-operations {
column-count: 2;
}
.kmip-role-allowed-operations {
@extend .box;
flex: 1 1 auto;
box-shadow: none;
padding: 0;
padding: $spacing-4 0;
}
.kmip-role-allowed-operations .field {
margin-bottom: $spacing-4;
Expand Down
27 changes: 27 additions & 0 deletions ui/app/utils/kmip-role-fields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import { removeManyFromArray } from 'vault/helpers/remove-from-array';

export const operationFields = (fieldNames) => {
if (!Array.isArray(fieldNames)) {
throw new Error('fieldNames must be an array');
}
return fieldNames.filter((key) => key.startsWith('operation'));
};

export const operationFieldsWithoutSpecial = (fieldNames) => {
const opFields = operationFields(fieldNames);
return removeManyFromArray(opFields, ['operationAll', 'operationNone']);
};

export const nonOperationFields = (fieldNames) => {
const opFields = operationFields(fieldNames);
return removeManyFromArray(fieldNames, opFields);
};

export const tlsFields = () => {
return ['tlsClientKeyBits', 'tlsClientKeyType', 'tlsClientTtl'];
};
55 changes: 0 additions & 55 deletions ui/lib/kmip/addon/components/edit-form-kmip-role.js

This file was deleted.

82 changes: 82 additions & 0 deletions ui/lib/kmip/addon/components/kmip/role-form.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<form {{on "submit" (perform this.save)}}>
<MessageError @model={{@model}} data-test-edit-form-error />
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="save" />
{{#if @model.isNew}}
{{! Show role name only in create mode }}
<FormField data-test-field @attr={{hash name="role" type="string" options=(hash label="Name")}} @model={{@model}} />
{{/if}}
<div class="control is-flex box is-shadowless is-fullwidth is-marginless">
<input
data-test-input="operationNone"
id="operationNone"
type="checkbox"
class="toggle is-success is-small"
checked={{not @model.operationNone}}
{{on "change" this.toggleOperationSpecial}}
/>
<label for="operationNone" class="has-text-weight-bold is-size-8">
Allow this role to perform KMIP operations
</label>
</div>
{{#unless @model.operationNone}}
<Toolbar>
<h3 class="title is-6 has-left-padding-s" data-test-kmip-section="Allowed Operations">
Allowed Operations
</h3>
</Toolbar>
<div class="box">
<FormField
@attr={{hash name="operationAll" type="boolean" options=(hash label="Allow this role to perform all operations")}}
@model={{@model}}
/>
<hr />
<div class="kmip-role-operations">
{{#each this.operationFormGroups as |group|}}
<div class="kmip-role-allowed-operations">
<h4 class="title is-7" data-test-kmip-operations={{group.name}}>{{group.name}}</h4>
{{#each group.fields as |attr|}}
<FormField
data-test-field
@disabled={{or @model.operationNone @model.operationAll}}
@attr={{attr}}
@model={{this.placeholderOrModel @model attr.name}}
@showHelpText={{false}}
/>
{{/each}}
</div>
{{/each}}
</div>
</div>
{{/unless}}
<div class="box is-fullwidth is-shadowless">
<h3 class="title is-3" data-test-kmip-section="TLS">
TLS
</h3>
{{#each this.tlsFormFields as |attr|}}
<FormField data-test-field @attr={{attr}} @model={{@model}} />
{{/each}}
</div>
{{#each @model.fields as |attr|}}
<FormField data-test-field @attr={{attr}} @model={{@model}} />
{{/each}}
</div>

<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<Hds::ButtonSet>
<Hds::Button
@text="Save"
@icon={{if this.save.isRunning "loading"}}
type="submit"
disabled={{this.save.isRunning}}
data-test-save
/>
<Hds::Button @text="Cancel" @color="secondary" {{on "click" @onCancel}} data-test-cancel />
</Hds::ButtonSet>
</div>
</form>
Loading

0 comments on commit 520f141

Please sign in to comment.