Skip to content

Commit

Permalink
Handle form validation for open api form (#11963)
Browse files Browse the repository at this point in the history
* Handle form validation for open api form

- Added required validator for all the default fields

* Fixed field group error and adedd comments

* Fixed acceptance tests

* Added changelog

* Fix validation in edit mode

- Handle read only inputs during edit mode

* Minor improvements

* Restrict validation only for userpass
  • Loading branch information
arnav28 committed Jul 13, 2021
1 parent 9bbe181 commit 55eb96b
Show file tree
Hide file tree
Showing 14 changed files with 133 additions and 6 deletions.
3 changes: 3 additions & 0 deletions changelog/11963.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: Add validation support for open api form fields
```
38 changes: 37 additions & 1 deletion ui/app/components/generated-item.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import AdapterError from '@ember-data/adapter/error';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { computed, set } from '@ember/object';
import { task } from 'ember-concurrency';

/**
Expand All @@ -24,6 +24,8 @@ export default Component.extend({
itemType: null,
flashMessages: service(),
router: service(),
validationMessages: null,
isFormInvalid: true,
props: computed('model', function() {
return this.model.serialize();
}),
Expand All @@ -41,7 +43,41 @@ export default Component.extend({
this.router.transitionTo('vault.cluster.access.method.item.list').followRedirects();
this.flashMessages.success(`Successfully saved ${this.itemType} ${this.model.id}.`);
}).withTestWaiter(),
init() {
this._super(...arguments);
this.set('validationMessages', {});
if (this.mode === 'edit') {
// For validation to work in edit mode,
// reconstruct the model values from field group
this.model.fieldGroups.forEach(element => {
if (element.default) {
element.default.forEach(attr => {
let fieldValue = attr.options && attr.options.fieldValue;
if (fieldValue) {
this.model[attr.name] = this.model[fieldValue];
}
});
}
});
}
},
actions: {
onKeyUp(name, value) {
this.model.set(name, value);
if (this.model.validations) {
// Set validation error message for updated attribute
this.model.validations.attrs[name] && this.model.validations.attrs[name].isValid
? set(this.validationMessages, name, '')
: set(this.validationMessages, name, this.model.validations.attrs[name].message);

// Set form button state
this.model.validate().then(({ validations }) => {
this.set('isFormInvalid', !validations.isValid);
});
} else {
this.set('isFormInvalid', false);
}
},
deleteItem() {
this.model.destroyRecord().then(() => {
this.router.transitionTo('vault.cluster.access.method.item.list').followRedirects();
Expand Down
8 changes: 8 additions & 0 deletions ui/app/components/mount-backend-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export default Component.extend({

showEnable: false,

// cp-validation related properties
validationMessages: null,
isFormInvalid: false,

init() {
this._super(...arguments);
const type = this.mountType;
Expand Down Expand Up @@ -108,6 +112,10 @@ export default Component.extend({
this.mountModel.validations.attrs.path.isValid
? set(this.validationMessages, 'path', '')
: set(this.validationMessages, 'path', this.mountModel.validations.attrs.path.message);

this.mountModel.validate().then(({ validations }) => {
this.set('isFormInvalid', !validations.isValid);
});
},
onTypeChange(path, value) {
if (path === 'type') {
Expand Down
10 changes: 9 additions & 1 deletion ui/app/models/auth-method.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import { computed } from '@ember/object';
import { fragment } from 'ember-data-model-fragments/attributes';
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import { memberAction } from 'ember-api-actions';
import { validator, buildValidations } from 'ember-cp-validations';

import apiPath from 'vault/utils/api-path';
import attachCapabilities from 'vault/lib/attach-capabilities';

let ModelExport = Model.extend({
const Validations = buildValidations({
path: validator('presence', {
presence: true,
message: "Path can't be blank.",
}),
});

let ModelExport = Model.extend(Validations, {
authConfigs: hasMany('auth-config', { polymorphic: true, inverse: 'backend', async: false }),
path: attr('string'),
accessor: attr('string'),
Expand Down
8 changes: 8 additions & 0 deletions ui/app/services/path-help.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { resolve, reject } from 'rsvp';
import { debug } from '@ember/debug';
import { dasherize, capitalize } from '@ember/string';
import { singularize } from 'ember-inflector';
import buildValidations from 'vault/utils/build-api-validators';

import generatedItemAdapter from 'vault/adapters/generated-item-list';
export function sanitizePath(path) {
Expand Down Expand Up @@ -280,11 +281,18 @@ export default Service.extend({
// if our newModel doesn't have fieldGroups already
// we need to create them
try {
// Initialize prototype to access field groups
let fieldGroups = newModel.proto().fieldGroups;
if (!fieldGroups) {
debug(`Constructing fieldGroups for ${backend}`);
fieldGroups = this.getFieldGroups(newModel);
newModel = newModel.extend({ fieldGroups });
// Build and add validations on model
// NOTE: For initial phase, initialize validations only for user pass auth
if (backend === 'userpass') {
let validations = buildValidations(fieldGroups);
newModel = newModel.extend(validations);
}
}
} catch (err) {
// eat the error, fieldGroups is computed in the model definition
Expand Down
5 changes: 5 additions & 0 deletions ui/app/styles/core/message.scss
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@

.message-inline {
display: flex;
align-items: center;
margin: 0 0 $spacing-l;

.hs-icon {
Expand All @@ -131,6 +132,10 @@
&.is-marginless {
margin-bottom: 0;
}

> p::first-letter {
text-transform: capitalize;
}
}

.has-text-highlight {
Expand Down
4 changes: 2 additions & 2 deletions ui/app/templates/components/generated-item.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="save" @noun={{itemType}} />
<MessageError @model={{model}} />
<FormFieldGroups @model={{model}} @mode={{mode}} />
<FormFieldGroups @model={{model}} @mode={{mode}} @onKeyUp={{action "onKeyUp"}} @validationMessages={{validationMessages}}/>
</div>
<div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control">
<button type="submit" data-test-save-config="true"
class="button is-primary {{if saveModel.isRunning "loading"}}" disabled={{saveModel.isRunning}}>
class="button is-primary {{if saveModel.isRunning "loading"}}" disabled={{or saveModel.isRunning isFormInvalid}}>
Save
</button>
{{#if (eq mode "create")}}
Expand Down
2 changes: 1 addition & 1 deletion ui/app/templates/components/mount-backend-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
type="submit"
data-test-mount-submit="true"
class="button is-primary {{if mountBackend.isRunning "loading"}}"
disabled={{or mountBackend.isRunning validationError}}
disabled={{or mountBackend.isRunning isFormInvalid}}
>
{{#if (eq mountType "auth")}}
Enable Method
Expand Down
30 changes: 30 additions & 0 deletions ui/app/utils/build-api-validators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { validator, buildValidations } from 'ember-cp-validations';

/**
* Add validation on dynamic form fields generated via open api spec
* For fields grouped under default category, add the require/presence validator
* @param {Array} fieldGroups
* fieldGroups param example:
* [ { default: [{name: 'username'}, {name: 'password'}] },
* { Tokens: [{name: 'tokenBoundCidrs'}] }
* ]
* @returns ember cp validation class
*/
export default function initValidations(fieldGroups) {
let validators = {};
fieldGroups.forEach(element => {
if (element.default) {
element.default.forEach(v => {
validators[v.name] = createPresenceValidator(v.name);
});
}
});
return buildValidations(validators);
}

export const createPresenceValidator = function(label) {
return validator('presence', {
presence: true,
message: `${label} can't be blank.`,
});
};
4 changes: 4 additions & 0 deletions ui/lib/core/addon/components/form-field-groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import layout from '../templates/components/form-field-groups';
* @model={{mountModel}}
* @onChange={{action "onTypeChange"}}
* @renderGroup="Method Options"
* @onKeyUp={{action "onKeyUp"}}
* @validationMessages={{validationMessages}}
* />
* ```
*
* @param [renderGroup=null] {String} - An allow list of groups to include in the render.
* @param model=null {DS.Model} - Model to be passed down to form-field component. If `fieldGroups` is present on the model then it will be iterated over and groups of `FormField` components will be rendered.
* @param onChange=null {Func} - Handler that will get set on the `FormField` component.
* @param onKeyUp=null {Func} - Handler that will set the value and trigger validation on input changes
* @param validationMessages=null {Object} Object containing validation message for each property
*
*/

Expand Down
7 changes: 7 additions & 0 deletions ui/lib/core/addon/components/form-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export default Component.extend({
*/
attr: null,

mode: null,

/*
* @private
* @param string
Expand Down Expand Up @@ -93,6 +95,11 @@ export default Component.extend({
*/
valuePath: or('attr.options.fieldValue', 'attr.name'),

isReadOnly: computed('attr.options.readOnly', 'mode', function() {
let readonly = this.attr.options?.readOnly || false;
return readonly && this.mode === 'edit';
}),

model: null,

/*
Expand Down
2 changes: 2 additions & 0 deletions ui/lib/core/addon/templates/components/form-field-groups.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<FormField
data-test-field
@attr={{attr}}
@mode={{mode}}
@model={{model}}
@onChange={{onChange}}
@onKeyUp={{onKeyUp}}
Expand All @@ -29,6 +30,7 @@
<FormField
data-test-field
@attr={{attr}}
@mode={{mode}}
@model={{model}}
/>
{{/each}}
Expand Down
12 changes: 12 additions & 0 deletions ui/lib/core/addon/templates/components/form-field.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,18 @@
@value={{or (get model valuePath) attr.options.defaultValue}}
@allowCopy="true"
@onChange={{action (action "setAndBroadcast" valuePath)}}
onkeyup={{action
(action "handleKeyUp" attr.name)
value="target.value"
}}
/>
{{#if (get validationMessages attr.name)}}
<AlertInline
@type="danger"
@message={{get validationMessages attr.name}}
@paddingTop=true
/>
{{/if}}
{{else if (or (eq attr.type "number") (eq attr.type "string"))}}
<div class="control">
{{#if (eq attr.options.editType "textarea")}}
Expand Down Expand Up @@ -251,6 +262,7 @@
<input
data-test-input={{attr.name}}
id={{attr.name}}
readonly={{isReadOnly}}
autocomplete="off"
spellcheck="false"
value={{or (get model valuePath) attr.options.defaultValue}}
Expand Down
6 changes: 5 additions & 1 deletion ui/tests/acceptance/auth-list-test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { click, fillIn, settled, visit } from '@ember/test-helpers';
import { click, fillIn, settled, visit, triggerKeyEvent } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
Expand Down Expand Up @@ -31,7 +31,9 @@ module('Acceptance | userpass secret backend', function(hooks) {
await visit(`/vault/access/${path1}/item/user/create`);
await settled();
await fillIn('[data-test-input="username"]', user1);
await triggerKeyEvent('[data-test-input="username"]', 'keyup', 65);
await fillIn('[data-test-textarea]', user1);
await triggerKeyEvent('[data-test-textarea]', 'keyup', 65);
await click('[data-test-save-config="true"]');
await settled();

Expand All @@ -53,7 +55,9 @@ module('Acceptance | userpass secret backend', function(hooks) {
await click('[data-test-create="user"]');
await settled();
await fillIn('[data-test-input="username"]', user2);
await triggerKeyEvent('[data-test-input="username"]', 'keyup', 65);
await fillIn('[data-test-textarea]', user2);
await triggerKeyEvent('[data-test-textarea]', 'keyup', 65);
await click('[data-test-save-config="true"]');
await settled();

Expand Down

0 comments on commit 55eb96b

Please sign in to comment.