Handle form validation for open api form (#11963)
* 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
This commit is contained in:
parent
e520e470a6
commit
d1cc297cd9
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Add validation support for open api form fields
|
||||
```
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}),
|
||||
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
@ -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') {
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
|
||||
.message-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0 $spacing-l;
|
||||
|
||||
.hs-icon {
|
||||
|
@ -131,6 +132,10 @@
|
|||
&.is-marginless {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> p::first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.has-text-highlight {
|
||||
|
|
|
@ -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")}}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.`,
|
||||
});
|
||||
};
|
|
@ -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
|
||||
*
|
||||
*/
|
||||
|
||||
|
|
|
@ -65,6 +65,8 @@ export default Component.extend({
|
|||
*/
|
||||
attr: null,
|
||||
|
||||
mode: null,
|
||||
|
||||
/*
|
||||
* @private
|
||||
* @param string
|
||||
|
@ -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,
|
||||
|
||||
/*
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<FormField
|
||||
data-test-field
|
||||
@attr={{attr}}
|
||||
@mode={{mode}}
|
||||
@model={{model}}
|
||||
@onChange={{onChange}}
|
||||
@onKeyUp={{onKeyUp}}
|
||||
|
@ -29,6 +30,7 @@
|
|||
<FormField
|
||||
data-test-field
|
||||
@attr={{attr}}
|
||||
@mode={{mode}}
|
||||
@model={{model}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
|
|
@ -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")}}
|
||||
|
@ -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}}
|
||||
|
|
|
@ -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';
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
|
Loading…
Reference in New Issue