Model Validation Warnings (#19913)

* updates model validations to support warnings and adds alert to form field

* adds changelog entry

* updates model validations tests
This commit is contained in:
Jordan Reimer 2023-04-03 15:24:58 -06:00 committed by GitHub
parent 3f0620ce2c
commit efe31ae32e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 16 deletions

3
changelog/19913.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Adds whitespace warning to secrets engine and auth method path inputs
```

View File

@ -63,6 +63,17 @@ export default class MountBackendForm extends Component {
return isValid; return isValid;
} }
checkModelWarnings() {
// check for warnings on change
// since we only show errors on submit we need to clear those out and only send warning state
const { state } = this.args.mountModel.validate();
for (const key in state) {
state[key].errors = [];
}
this.modelValidations = state;
this.invalidFormAlert = null;
}
async showWarningsForKvv2() { async showWarningsForKvv2() {
try { try {
const capabilities = await this.store.findRecord('capabilities', `${this.args.mountModel.path}/config`); const capabilities = await this.store.findRecord('capabilities', `${this.args.mountModel.path}/config`);
@ -141,6 +152,7 @@ export default class MountBackendForm extends Component {
@action @action
onKeyUp(name, value) { onKeyUp(name, value) {
this.args.mountModel[name] = value; this.args.mountModel[name] = value;
this.checkModelWarnings();
} }
@action @action

View File

@ -11,14 +11,20 @@ import { get } from '@ember/object';
* used to validate properties on a class * used to validate properties on a class
* *
* decorator expects validations object with the following shape: * decorator expects validations object with the following shape:
* { [propertyKeyName]: [{ type, options, message, validator }] } * { [propertyKeyName]: [{ type, options, message, level, validator }] }
* each key in the validations object should refer to the property on the class to apply the validation to * each key in the validations object should refer to the property on the class to apply the validation to
* type refers to the type of validation to apply -- must be exported from validators util for lookup *
* options is an optional object for given validator -- min, max, nullable etc. -- see validators in util * type - string referring to the type of validation to apply -- must be exported from validators util for lookup
* message is added to the errors array and returned from the validate method if validation fails *
* validator may be used in place of type to provide a function that gets executed in the validate method * options - an optional object for given validator -- min, max, nullable etc. -- see validators in util
* validator is useful when specific validations are needed (dependent on other class properties etc.) *
* validator must be passed as function that takes the class context (this) as the only argument and returns true or false * message - string added to the errors array and returned from the validate method if validation fails
*
* level - optional string that defaults to 'error'. Currently the only other accepted value is 'warn'
*
* validator - function that may be used in place of type that is invoked in the validate method
* useful when specific validations are needed (dependent on other class properties etc.)
* must be passed as function that takes the class context (this) as the only argument and returns true or false
* each property supports multiple validations provided as an array -- for example, presence and length for string * each property supports multiple validations provided as an array -- for example, presence and length for string
* *
* validations must be invoked using the validate method which is added directly to the decorated class * validations must be invoked using the validate method which is added directly to the decorated class
@ -59,6 +65,7 @@ import { get } from '@ember/object';
* -> state.foo.errors = ['foo is required if bar includes test.']; * -> state.foo.errors = ['foo is required if bar includes test.'];
* *
* *** example adding class in hbs file * *** example adding class in hbs file
*
* all form-validations need to have a red border around them. Add this by adding a conditional class 'has-error-border' * all form-validations need to have a red border around them. Add this by adding a conditional class 'has-error-border'
* class="input field {{if this.errors.name.errors 'has-error-border'}}" * class="input field {{if this.errors.name.errors 'has-error-border'}}"
*/ */
@ -91,10 +98,10 @@ export function withModelValidations(validations) {
continue; continue;
} }
state[key] = { errors: [] }; state[key] = { errors: [], warnings: [] };
for (const rule of rules) { for (const rule of rules) {
const { type, options, message, validator: customValidator } = rule; const { type, options, level, message, validator: customValidator } = rule;
// check for custom validator or lookup in validators util by type // check for custom validator or lookup in validators util by type
const useCustomValidator = typeof customValidator === 'function'; const useCustomValidator = typeof customValidator === 'function';
const validator = useCustomValidator ? customValidator : validators[type]; const validator = useCustomValidator ? customValidator : validators[type];
@ -115,9 +122,13 @@ export function withModelValidations(validations) {
if (!passedValidation) { if (!passedValidation) {
// consider setting a prop like validationErrors directly on the model // consider setting a prop like validationErrors directly on the model
// for now return an errors object // for now return an errors object
state[key].errors.push(message); if (level === 'warn') {
if (isValid) { state[key].warnings.push(message);
isValid = false; } else {
state[key].errors.push(message);
if (isValid) {
isValid = false;
}
} }
} }
} }

View File

@ -13,7 +13,15 @@ import attachCapabilities from 'vault/lib/attach-capabilities';
import { withModelValidations } from 'vault/decorators/model-validations'; import { withModelValidations } from 'vault/decorators/model-validations';
const validations = { const validations = {
path: [{ type: 'presence', message: "Path can't be blank." }], path: [
{ type: 'presence', message: "Path can't be blank." },
{
type: 'containsWhiteSpace',
message:
"Path contains whitespace. If this is desired, you'll need to encode it with %20 in API requests.",
level: 'warn',
},
],
}; };
// unsure if ember-api-actions will work on native JS class model // unsure if ember-api-actions will work on native JS class model

View File

@ -14,7 +14,15 @@ import { withExpandedAttributes } from 'vault/decorators/model-expanded-attribut
const LIST_EXCLUDED_BACKENDS = ['system', 'identity']; const LIST_EXCLUDED_BACKENDS = ['system', 'identity'];
const validations = { const validations = {
path: [{ type: 'presence', message: "Path can't be blank." }], path: [
{ type: 'presence', message: "Path can't be blank." },
{
type: 'containsWhiteSpace',
message:
"Path contains whitespace. If this is desired, you'll need to encode it with %20 in API requests.",
level: 'warn',
},
],
maxVersions: [ maxVersions: [
{ type: 'number', message: 'Maximum versions must be a number.' }, { type: 'number', message: 'Maximum versions must be a number.' },
{ type: 'length', options: { min: 1, max: 16 }, message: 'You cannot go over 16 characters.' }, { type: 'length', options: { min: 1, max: 16 }, message: 'You cannot go over 16 characters.' },

View File

@ -318,6 +318,14 @@
{{#if this.validationError}} {{#if this.validationError}}
<AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} /> <AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} />
{{/if}} {{/if}}
{{#if this.validationWarning}}
<AlertInline
@type="warning"
@message={{this.validationWarning}}
@paddingTop={{true}}
data-test-validation-warning
/>
{{/if}}
{{/if}} {{/if}}
</div> </div>
{{else if (eq @attr.type "boolean")}} {{else if (eq @attr.type "boolean")}}

View File

@ -117,6 +117,11 @@ export default class FormFieldComponent extends Component {
const state = validations[this.valuePath]; const state = validations[this.valuePath];
return state && !state.isValid ? state.errors.join(' ') : null; return state && !state.isValid ? state.errors.join(' ') : null;
} }
get validationWarning() {
const validations = this.args.modelValidations || {};
const state = validations[this.valuePath];
return state?.warnings?.length ? state.warnings.join(' ') : null;
}
onChange() { onChange() {
if (this.args.onChange) { if (this.args.onChange) {

View File

@ -210,4 +210,20 @@ module('Integration | Component | form field', function (hooks) {
assert.dom('[data-test-toggle-input="Foo"]').isChecked('Toggle is initially checked when given value'); assert.dom('[data-test-toggle-input="Foo"]').isChecked('Toggle is initially checked when given value');
assert.dom('[data-test-ttl-value="Foo"]').hasValue('1', 'Ttl input displays with correct value'); assert.dom('[data-test-ttl-value="Foo"]').hasValue('1', 'Ttl input displays with correct value');
}); });
test('it should show validation warning', async function (assert) {
const model = this.owner.lookup('service:store').createRecord('auth-method');
model.path = 'foo bar';
this.validations = model.validate().state;
this.setProperties({
model,
attr: createAttr('path', 'string'),
onChange: () => {},
});
await render(
hbs`<FormField @attr={{this.attr}} @model={{this.model}} @modelValidations={{this.validations}} @onChange={{this.onChange}} />`
);
assert.dom('[data-test-validation-warning]').exists('Validation warning renders');
});
}); });

View File

@ -74,7 +74,7 @@ module('Unit | Decorators | ModelValidations', function (hooks) {
assert.false(v1.isValid, 'isValid state is correct when errors exist'); assert.false(v1.isValid, 'isValid state is correct when errors exist');
assert.deepEqual( assert.deepEqual(
v1.state, v1.state,
{ foo: { isValid: false, errors: [message] } }, { foo: { isValid: false, errors: [message], warnings: [] } },
'Correct state returned when property is invalid' 'Correct state returned when property is invalid'
); );
@ -83,7 +83,7 @@ module('Unit | Decorators | ModelValidations', function (hooks) {
assert.true(v2.isValid, 'isValid state is correct when no errors exist'); assert.true(v2.isValid, 'isValid state is correct when no errors exist');
assert.deepEqual( assert.deepEqual(
v2.state, v2.state,
{ foo: { isValid: true, errors: [] } }, { foo: { isValid: true, errors: [], warnings: [] } },
'Correct state returned when property is valid' 'Correct state returned when property is valid'
); );
}); });
@ -115,4 +115,22 @@ module('Unit | Decorators | ModelValidations', function (hooks) {
const v3 = fooClass.validate(); const v3 = fooClass.validate();
assert.strictEqual(v3.invalidFormMessage, null, 'invalidFormMessage is null when form is valid'); assert.strictEqual(v3.invalidFormMessage, null, 'invalidFormMessage is null when form is valid');
}); });
test('it should validate warnings', function (assert) {
const message = 'Value contains whitespace.';
const validations = {
foo: [
{
type: 'containsWhiteSpace',
message,
level: 'warn',
},
],
};
const fooClass = createClass(validations);
fooClass.foo = 'foo bar';
const { state, isValid } = fooClass.validate();
assert.true(isValid, 'Model is considered valid when there are only warnings');
assert.strictEqual(state.foo.warnings.join(' '), message, 'Warnings are returned');
});
}); });