Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI/Fix form validation issues #15560

Merged
merged 18 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/15560.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: fix form validations ignoring default values and disabling submit button
```
23 changes: 16 additions & 7 deletions ui/app/components/mount-backend-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default Component.extend({

// validation related properties
modelValidations: null,
isFormInvalid: false,
invalidFormAlert: null,

mountIssue: false,

Expand Down Expand Up @@ -88,10 +88,24 @@ export default Component.extend({
}
},

checkModelValidity(model) {
const { isValid, state, invalidFormMessage } = model.validate();
this.setProperties({
modelValidations: state,
invalidFormAlert: invalidFormMessage,
});

return isValid;
},

mountBackend: task(
waitFor(function* () {
const mountModel = this.mountModel;
const { type, path } = mountModel;
// only submit form if validations pass
if (!this.checkModelValidity(mountModel)) {
return;
}
let capabilities = null;
try {
capabilities = yield this.store.findRecord('capabilities', `${path}/config`);
Expand Down Expand Up @@ -120,7 +134,6 @@ export default Component.extend({
} catch (err) {
if (err.httpStatus === 403) {
this.mountIssue = true;
this.set('isFormInvalid', this.mountIssue);
hellobontempo marked this conversation as resolved.
Show resolved Hide resolved
this.flashMessages.danger(
'You do not have access to the sys/mounts endpoint. The secret engine was not mounted.'
);
Expand Down Expand Up @@ -163,12 +176,8 @@ export default Component.extend({
actions: {
onKeyUp(name, value) {
this.mountModel.set(name, value);
const { isValid, state } = this.mountModel.validate();
this.setProperties({
modelValidations: state,
isFormInvalid: !isValid,
});
},

onTypeChange(path, value) {
if (path === 'type') {
this.wizard.set('componentState', value);
Expand Down
12 changes: 11 additions & 1 deletion ui/app/decorators/model-validations.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function withModelValidations(validations) {
validate() {
let isValid = true;
const state = {};
let errorCount = 0;

for (const key in this._validations) {
const rules = this._validations[key];
Expand Down Expand Up @@ -110,9 +111,18 @@ export function withModelValidations(validations) {
}
}
}
errorCount += state[key].errors.length;
state[key].isValid = !state[key].errors.length;
}
return { isValid, state };

return { isValid, state, invalidFormMessage: this.generateErrorCountMessage(errorCount) };
}

generateErrorCountMessage(errorCount) {
if (errorCount < 1) return null;
// returns count specific message: 'There is an error/are N errors with this form.'
let isPlural = errorCount > 1 ? `are ${errorCount} errors` : false;
return `There ${isPlural ? isPlural : 'is an error'} with this form.`;
hellobontempo marked this conversation as resolved.
Show resolved Hide resolved
}
};
};
Expand Down
2 changes: 1 addition & 1 deletion ui/app/models/secret-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const LIST_EXCLUDED_BACKENDS = ['system', 'identity'];
const validations = {
path: [{ type: 'presence', message: "Path can't be blank." }],
maxVersions: [
{ type: 'number', options: { asString: true }, 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.' },
],
};
Expand Down
2 changes: 1 addition & 1 deletion ui/app/models/secret-v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { withModelValidations } from 'vault/decorators/model-validations';

const validations = {
maxVersions: [
{ type: 'number', options: { asString: true }, message: 'Maximum versions must be a number.' },
hellobontempo marked this conversation as resolved.
Show resolved Hide resolved
{ type: 'number', message: 'Maximum versions must be a number.' },
{ type: 'length', options: { min: 1, max: 16 }, message: 'You cannot go over 16 characters.' },
],
};
Expand Down
7 changes: 6 additions & 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 this.mountBackend.isRunning 'loading'}}"
disabled={{or this.mountBackend.isRunning this.isFormInvalid}}
disabled={{this.mountBackend.isRunning}}
>
{{#if (eq this.mountType "auth")}}
Enable Method
Expand All @@ -82,6 +82,11 @@
Back
</button>
</div>
{{#if this.invalidFormAlert}}
<div class="control">
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
</div>
{{/if}}
{{else}}
<button
data-test-mount-next
Expand Down
22 changes: 11 additions & 11 deletions ui/app/utils/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ import { isPresent } from '@ember/utils';
export const presence = (value) => isPresent(value);

export const length = (value, { nullable = false, min, max } = {}) => {
let isValid = nullable;
if (typeof value === 'string') {
const underMin = min && value.length < min;
const overMax = max && value.length > max;
isValid = underMin || overMax ? false : true;
if (!min && !max) return;
// value could be an integer if the attr has a default value of some number
const valueLength = value?.toString().length;
if (valueLength) {
const underMin = min && valueLength < min;
const overMax = max && valueLength > max;
return underMin || overMax ? false : true;
}
return isValid;
return nullable;
};

export const number = (value, { nullable = false, asString } = {}) => {
if (!value) return nullable;
if (typeof value === 'string' && !asString) {
return false;
}
export const number = (value, { nullable = false } = {}) => {
// since 0 is falsy, !value returns true even though 0 is a valid number
if (!value && value !== 0) return nullable;
return !isNaN(value);
};

Expand Down
19 changes: 19 additions & 0 deletions ui/lib/core/addon/components/alert-inline.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div
{{did-update this.refresh @message}}
class={{concat "message-inline" this.paddingTop this.isMarginless this.sizeSmall}}
data-test-inline-alert
...attributes
>
{{#if this.isRefreshing}}
<Icon @name="loading" class="loading" />
{{else}}
<Icon @name={{this.alertType.glyph}} class={{this.alertType.glyphClass}} />
<p class={{this.textClass}} data-test-inline-error-message>
{{#if (has-block)}}
{{yield}}
{{else}}
{{@message}}
{{/if}}
</p>
{{/if}}
</div>
67 changes: 44 additions & 23 deletions ui/lib/core/addon/components/alert-inline.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
import { tracked } from '@glimmer/tracking';
import { messageTypes } from 'core/helpers/message-types';
import layout from '../templates/components/alert-inline';

/**
* @module AlertInline
Expand All @@ -12,31 +13,51 @@ import layout from '../templates/components/alert-inline';
* <AlertInline @type="danger" @message="{{model.keyId}} is not a valid lease ID"/>
* ```
*
* @param type=null{String} - The alert type. This comes from the message-types helper.
* @param type=null{String} - The alert type passed to the message-types helper.
* @param [message=null]{String} - The message to display within the alert.
* @param [sizeSmall=false]{Boolean} - Whether or not to display a small font with padding below of alert message.
* @param [paddingTop=false]{Boolean} - Whether or not to add padding above component.
* @param [isMarginless=false]{Boolean} - Whether or not to remove margin bottom below component.
* @param [sizeSmall=false]{Boolean} - Whether or not to display a small font with padding below of alert message.
* @param [mimicRefresh=false]{Boolean} - If true will display a loading icon when attributes change (e.g. when a form submits and the alert message changes).
*/

export default Component.extend({
layout,
type: null,
message: null,
sizeSmall: false,
paddingTop: false,
classNames: ['message-inline'],
classNameBindings: ['sizeSmall:size-small', 'paddingTop:padding-top', 'isMarginless:is-marginless'],

textClass: computed('type', function () {
if (this.type == 'danger') {
return messageTypes([this.type]).glyphClass;
export default class AlertInlineComponent extends Component {
@tracked isRefreshing = false;

get mimicRefresh() {
return this.args.mimicRefresh || false;
}

get paddingTop() {
return this.args.paddingTop ? ' padding-top' : '';
}

get isMarginless() {
return this.args.isMarginless ? ' is-marginless' : '';
}

get sizeSmall() {
return this.args.sizeSmall ? ' size-small' : '';
}

get textClass() {
if (this.args.type === 'danger') {
return this.alertType.glyphClass;
}
return null;
}

return;
}),
get alertType() {
return messageTypes([this.args.type]);
}

alertType: computed('type', function () {
return messageTypes([this.type]);
}),
});
@action
refresh() {
if (this.mimicRefresh) {
this.isRefreshing = true;
later(() => {
this.isRefreshing = false;
}, 200);
}
}
}
2 changes: 1 addition & 1 deletion ui/lib/core/addon/components/form-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default class FormFieldComponent extends Component {
get validationError() {
const validations = this.args.modelValidations || {};
const state = validations[this.valuePath];
return state && !state.isValid ? state.errors.join('. ') : null;
return state && !state.isValid ? state.errors.join(' ') : null;
}

onChange() {
Expand Down
8 changes: 0 additions & 8 deletions ui/lib/core/addon/templates/components/alert-inline.hbs

This file was deleted.

77 changes: 71 additions & 6 deletions ui/tests/integration/components/alert-inline-test.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,82 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { render, settled, find, waitUntil } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Component | alert-inline', function (hooks) {
setupRenderingTest(hooks);

test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
hooks.beforeEach(function () {
this.set('message', 'some very important alert');
});

test('it renders alert message with correct class args', async function (assert) {
await render(hbs`
<AlertInline
@paddingTop={{true}}
@isMarginless={{true}}
@sizeSmall={{true}}
@message={{this.message}}
/>
`);
assert.dom('[data-test-inline-error-message]').hasText('some very important alert');
assert
.dom('[data-test-inline-alert]')
.hasAttribute('class', 'message-inline padding-top is-marginless size-small');
});

test('it yields to block text', async function (assert) {
await render(hbs`
<AlertInline @message={{this.message}}>
A much more important alert
</AlertInline>
`);
assert.dom('[data-test-inline-error-message]').hasText('A much more important alert');
});

test('it renders correctly for type=danger', async function (assert) {
this.set('type', 'danger');
await render(hbs`
<AlertInline
@type={{this.type}}
@message={{this.message}}
/>
`);
assert
.dom('[data-test-inline-error-message]')
.hasAttribute('class', 'has-text-danger', 'has danger text');
assert.dom('[data-test-icon="x-square-fill"]').exists('danger icon exists');
});

test('it renders correctly for type=warning', async function (assert) {
this.set('type', 'warning');
await render(hbs`
<AlertInline
@type={{this.type}}
@message={{this.message}}
/>
`);
assert.dom('[data-test-inline-error-message]').doesNotHaveAttribute('class', 'does not have styled text');
assert.dom('[data-test-icon="alert-triangle-fill"]').exists('warning icon exists');
});

await render(hbs`{{alert-inline type="danger" message="test message"}}`);
test('it mimics loading when message changes', async function (assert) {
await render(hbs`
<AlertInline
@message={{this.message}}
@mimicRefresh={{true}}
/>
`);
assert
.dom('[data-test-inline-error-message]')
.hasText('some very important alert', 'it renders original message');

assert.dom(this.element).hasText('test message');
this.set('message', 'some changed alert!!!');
await waitUntil(() => find('[data-test-icon="loading"]'));
assert.ok(find('[data-test-icon="loading"]'), 'it shows loading icon when message changes');
await settled();
assert
.dom('[data-test-inline-error-message]')
.hasText('some changed alert!!!', 'it shows updated message');
});
});
Loading