mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
UI: Glimmerize database/role.js and add model validations (#29754)
* glimmerize db role model * adding validations * updates to validators * formatting fix * changelog * add validations to top fields * updates * added test for form validation * updates from pr review * remove added period * remove extra line
This commit is contained in:
parent
0cec5066e6
commit
6b9467c568
6 changed files with 221 additions and 139 deletions
3
changelog/29754.txt
Normal file
3
changelog/29754.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui/database: Glimmerizing and adding validations to role create
|
||||
```
|
||||
|
|
@ -2,19 +2,39 @@
|
|||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { task } from 'ember-concurrency';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
* @module DatabaseRoleEdit component is used to configure a database role.
|
||||
* See secret-edit-layout which uses options for backend to determine which component to render.
|
||||
* @example
|
||||
* <DatabaseRoleEdit
|
||||
@model={{this.model.database.role}}
|
||||
@tab="edit"
|
||||
@model="edit"
|
||||
@initialKey=this.initialKey
|
||||
/>
|
||||
*
|
||||
* @param {object} model - The database role model.
|
||||
* @param {string} tab - The tab to render.
|
||||
* @param {string} mode - The mode to render. Either 'create' or 'edit'.
|
||||
* @param {string} [initialKey] - The initial key to set for the database role.
|
||||
*/
|
||||
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
|
||||
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
|
||||
|
||||
export default class DatabaseRoleEdit extends Component {
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
@service store;
|
||||
@tracked modelValidations;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked errorMessage = '';
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
|
@ -23,19 +43,30 @@ export default class DatabaseRoleEdit extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
@tracked loading = false;
|
||||
isValid() {
|
||||
const { isValid, state } = this.args.model.validate();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
return isValid;
|
||||
}
|
||||
|
||||
resetErrors() {
|
||||
this.flashMessages.clearMessages();
|
||||
this.errorMessage = this.invalidFormAlert = '';
|
||||
this.modelValidations = null;
|
||||
}
|
||||
|
||||
get warningMessages() {
|
||||
const warnings = {};
|
||||
const { canCreateDynamic, canCreateStatic, type } = this.args.model;
|
||||
if (
|
||||
(this.args.model.type === 'dynamic' && this.args.model.canCreateDynamic === false) ||
|
||||
(this.args.model.type === 'static' && this.args.model.canCreateStatic === false)
|
||||
(type === 'dynamic' && canCreateDynamic === false) ||
|
||||
(type === 'static' && canCreateStatic === false)
|
||||
) {
|
||||
warnings.type = `You don't have permissions to create this type of role.`;
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
get databaseType() {
|
||||
const backend = this.args.model?.backend;
|
||||
const dbs = this.args.model?.database || [];
|
||||
|
|
@ -73,36 +104,29 @@ export default class DatabaseRoleEdit extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
handleCreateEditRole(evt) {
|
||||
evt.preventDefault();
|
||||
this.loading = true;
|
||||
|
||||
const mode = this.args.mode;
|
||||
const roleSecret = this.args.model;
|
||||
const secretId = roleSecret.name;
|
||||
if (mode === 'create') {
|
||||
roleSecret.set('id', secretId);
|
||||
const path = roleSecret.type === 'static' ? 'static-roles' : 'roles';
|
||||
roleSecret.set('path', path);
|
||||
}
|
||||
return roleSecret
|
||||
.save()
|
||||
.then(() => {
|
||||
try {
|
||||
this.router.transitionTo(SHOW_ROUTE, `role/${secretId}`);
|
||||
} catch (e) {
|
||||
console.debug(e); // eslint-disable-line
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
const errorMessage = e.errors?.join('. ') || e.message;
|
||||
handleCreateEditRole = task(
|
||||
waitFor(async (evt) => {
|
||||
evt.preventDefault();
|
||||
this.resetErrors();
|
||||
const { mode, model } = this.args;
|
||||
if (!this.isValid()) return;
|
||||
if (mode === 'create') {
|
||||
model.id = model.name;
|
||||
const path = model.type === 'static' ? 'static-roles' : 'roles';
|
||||
model.path = path;
|
||||
}
|
||||
try {
|
||||
await model.save();
|
||||
this.router.transitionTo(SHOW_ROUTE, `role/${model.name}`);
|
||||
} catch (e) {
|
||||
this.errorMessage = errorMessage(e);
|
||||
this.flashMessages.danger(
|
||||
errorMessage || 'Could not save the role. Please check Vault logs for more information.'
|
||||
this.errorMessage || 'Could not save the role. Please check Vault logs for more information.'
|
||||
);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@action
|
||||
rotateRoleCred(id) {
|
||||
const backend = this.args.model?.backend;
|
||||
|
|
|
|||
|
|
@ -2,21 +2,31 @@
|
|||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import { getRoleFields } from 'vault/utils/model-helpers/database-helpers';
|
||||
|
||||
export default Model.extend({
|
||||
idPrefix: 'role/',
|
||||
backend: attr('string', { readOnly: true }),
|
||||
name: attr('string', {
|
||||
label: 'Role name',
|
||||
}),
|
||||
database: attr('array', {
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
const validations = {
|
||||
database: [{ type: 'presence', message: 'Database is required.' }],
|
||||
type: [{ type: 'presence', message: 'Type is required.' }],
|
||||
username: [
|
||||
{
|
||||
validator(model) {
|
||||
const { type, username } = model;
|
||||
if (!type || type === 'dynamic') return true;
|
||||
if (username) return true;
|
||||
},
|
||||
message: 'Username is required.',
|
||||
},
|
||||
],
|
||||
};
|
||||
@withModelValidations(validations)
|
||||
export default class RoleModel extends Model {
|
||||
idPrefix = 'role/';
|
||||
@attr('string', { readOnly: true }) backend;
|
||||
@attr('string', { label: 'Role name' }) name;
|
||||
@attr('array', {
|
||||
label: 'Connection name',
|
||||
editType: 'searchSelect',
|
||||
fallbackComponent: 'string-list',
|
||||
|
|
@ -24,81 +34,90 @@ export default Model.extend({
|
|||
selectLimit: 1,
|
||||
onlyAllowExisting: true,
|
||||
subText: 'The database connection for which credentials will be generated.',
|
||||
}),
|
||||
type: attr('string', {
|
||||
})
|
||||
database;
|
||||
@attr('string', {
|
||||
label: 'Type of role',
|
||||
noDefault: true,
|
||||
possibleValues: ['static', 'dynamic'],
|
||||
}),
|
||||
default_ttl: attr({
|
||||
})
|
||||
type;
|
||||
@attr({
|
||||
editType: 'ttl',
|
||||
defaultValue: '1h',
|
||||
label: 'Generated credentials’s Time-to-Live (TTL)',
|
||||
helperTextDisabled: 'Vault will use a TTL of 1 hour.',
|
||||
defaultShown: 'Engine default',
|
||||
}),
|
||||
max_ttl: attr({
|
||||
})
|
||||
default_ttl;
|
||||
@attr({
|
||||
editType: 'ttl',
|
||||
defaultValue: '24h',
|
||||
label: 'Generated credentials’s maximum Time-to-Live (Max TTL)',
|
||||
helperTextDisabled: 'Vault will use a TTL of 24 hours.',
|
||||
defaultShown: 'Engine default',
|
||||
}),
|
||||
username: attr('string', {
|
||||
subText: 'The database username that this Vault role corresponds to.',
|
||||
}),
|
||||
rotation_period: attr({
|
||||
})
|
||||
max_ttl;
|
||||
@attr('string', { subText: 'The database username that this Vault role corresponds to.' }) username;
|
||||
@attr({
|
||||
editType: 'ttl',
|
||||
defaultValue: '24h',
|
||||
helperTextDisabled:
|
||||
'Specifies the amount of time Vault should wait before rotating the password. The minimum is 5 seconds. Default is 24 hours.',
|
||||
helperTextEnabled: 'Vault will rotate password after',
|
||||
}),
|
||||
skip_import_rotation: attr({
|
||||
helperTextEnabled: 'Vault will rotate password after.',
|
||||
})
|
||||
rotation_period;
|
||||
@attr({
|
||||
label: 'Skip initial rotation',
|
||||
editType: 'boolean',
|
||||
defaultValue: false,
|
||||
subText: 'When unchecked, Vault automatically rotates the password upon creation',
|
||||
}),
|
||||
creation_statements: attr('array', {
|
||||
subText: 'When unchecked, Vault automatically rotates the password upon creation.',
|
||||
})
|
||||
skip_import_rotation;
|
||||
@attr('array', {
|
||||
editType: 'stringArray',
|
||||
}),
|
||||
revocation_statements: attr('array', {
|
||||
})
|
||||
creation_statements;
|
||||
@attr('array', {
|
||||
editType: 'stringArray',
|
||||
defaultShown: 'Default',
|
||||
}),
|
||||
rotation_statements: attr('array', {
|
||||
})
|
||||
revocation_statements;
|
||||
@attr('array', {
|
||||
editType: 'stringArray',
|
||||
defaultShown: 'Default',
|
||||
}),
|
||||
rollback_statements: attr('array', {
|
||||
})
|
||||
rotation_statements;
|
||||
@attr('array', {
|
||||
editType: 'stringArray',
|
||||
defaultShown: 'Default',
|
||||
}),
|
||||
renew_statements: attr('array', {
|
||||
})
|
||||
rollback_statements;
|
||||
@attr('array', {
|
||||
editType: 'stringArray',
|
||||
defaultShown: 'Default',
|
||||
}),
|
||||
creation_statement: attr('string', {
|
||||
})
|
||||
renew_statements;
|
||||
@attr('string', {
|
||||
editType: 'json',
|
||||
allowReset: true,
|
||||
theme: 'hashi short',
|
||||
defaultShown: 'Default',
|
||||
}),
|
||||
revocation_statement: attr('string', {
|
||||
})
|
||||
creation_statement;
|
||||
@attr('string', {
|
||||
editType: 'json',
|
||||
allowReset: true,
|
||||
theme: 'hashi short',
|
||||
defaultShown: 'Default',
|
||||
}),
|
||||
|
||||
})
|
||||
revocation_statement;
|
||||
/* FIELD ATTRIBUTES */
|
||||
get fieldAttrs() {
|
||||
// Main fields on edit/create form
|
||||
const fields = ['name', 'database', 'type'];
|
||||
return expandAttributeMeta(this, fields);
|
||||
},
|
||||
|
||||
}
|
||||
get showFields() {
|
||||
let fields = ['name', 'database', 'type'];
|
||||
fields = fields.concat(getRoleFields(this.type)).concat(['creation_statements']);
|
||||
|
|
@ -107,9 +126,8 @@ export default Model.extend({
|
|||
fields = fields.concat(['revocation_statements']);
|
||||
}
|
||||
return expandAttributeMeta(this, fields);
|
||||
},
|
||||
|
||||
roleSettingAttrs: computed(function () {
|
||||
}
|
||||
get roleSettingAttrs() {
|
||||
// logic for which get displayed is on DatabaseRoleSettingForm
|
||||
const allRoleSettingFields = [
|
||||
'default_ttl',
|
||||
|
|
@ -126,25 +144,40 @@ export default Model.extend({
|
|||
'renew_statements',
|
||||
];
|
||||
return expandAttributeMeta(this, allRoleSettingFields);
|
||||
}),
|
||||
|
||||
}
|
||||
/* CAPABILITIES */
|
||||
// only used for secretPath
|
||||
path: attr('string', { readOnly: true }),
|
||||
@attr('string', { readOnly: true }) path;
|
||||
@lazyCapabilities(apiPath`${'backend'}/${'path'}/${'id'}`, 'backend', 'path', 'id') secretPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/roles/+`, 'backend') dynamicPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/static-roles/+`, 'backend') staticPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/creds/${'id'}`, 'backend', 'id') credentialPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/static-creds/${'id'}`, 'backend', 'id') staticCredentialPath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/config/${'database[0]'}`, 'backend', 'database') databasePath;
|
||||
@lazyCapabilities(apiPath`${'backend'}/rotate-role/${'id'}`, 'backend', 'id') rotateRolePath;
|
||||
|
||||
secretPath: lazyCapabilities(apiPath`${'backend'}/${'path'}/${'id'}`, 'backend', 'path', 'id'),
|
||||
canEditRole: alias('secretPath.canUpdate'),
|
||||
canDelete: alias('secretPath.canDelete'),
|
||||
dynamicPath: lazyCapabilities(apiPath`${'backend'}/roles/+`, 'backend'),
|
||||
canCreateDynamic: alias('dynamicPath.canCreate'),
|
||||
staticPath: lazyCapabilities(apiPath`${'backend'}/static-roles/+`, 'backend'),
|
||||
canCreateStatic: alias('staticPath.canCreate'),
|
||||
credentialPath: lazyCapabilities(apiPath`${'backend'}/creds/${'id'}`, 'backend', 'id'),
|
||||
staticCredentialPath: lazyCapabilities(apiPath`${'backend'}/static-creds/${'id'}`, 'backend', 'id'),
|
||||
canGenerateCredentials: alias('credentialPath.canRead'),
|
||||
canGetCredentials: alias('staticCredentialPath.canRead'),
|
||||
databasePath: lazyCapabilities(apiPath`${'backend'}/config/${'database[0]'}`, 'backend', 'database'),
|
||||
canUpdateDb: alias('databasePath.canUpdate'),
|
||||
rotateRolePath: lazyCapabilities(apiPath`${'backend'}/rotate-role/${'id'}`, 'backend', 'id'),
|
||||
canRotateRoleCredentials: alias('rotateRolePath.canUpdate'),
|
||||
});
|
||||
get canEditRole() {
|
||||
return this.secretPath.get('canUpdate');
|
||||
}
|
||||
get canDelete() {
|
||||
return this.secretPath.get('canDelete');
|
||||
}
|
||||
get canCreateDynamic() {
|
||||
return this.dynamicPath.get('canCreate');
|
||||
}
|
||||
get canCreateStatic() {
|
||||
return this.staticPath.get('canCreate');
|
||||
}
|
||||
get canGenerateCredentials() {
|
||||
return this.credentialPath.get('canRead');
|
||||
}
|
||||
get canGetCredentials() {
|
||||
return this.staticCredentialPath.get('canRead');
|
||||
}
|
||||
get canUpdateDb() {
|
||||
return this.databasePath.get('canUpdate');
|
||||
}
|
||||
get canRotateRoleCredentials() {
|
||||
return this.rotateRolePath.get('canUpdate');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<KeyValueHeader @path="vault.cluster.secrets.backend.show" @mode={{this.mode}} @root={{@root}} @showCurrent={{true}} />
|
||||
<KeyValueHeader @path="vault.cluster.secrets.backend.show" @mode={{@mode}} @root={{@root}} @showCurrent={{true}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-secret-header="true">
|
||||
|
|
@ -19,7 +18,6 @@
|
|||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if (eq @mode "show")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
|
|
@ -28,7 +26,7 @@
|
|||
@buttonText="Delete role"
|
||||
class="toolbar-button"
|
||||
@buttonColor="secondary"
|
||||
@onConfirmAction={{action "delete"}}
|
||||
@onConfirmAction={{this.delete}}
|
||||
@confirmTitle="Delete role?"
|
||||
@confirmMessage="This role will be permanently deleted. You will need to recreate it to use it again."
|
||||
data-test-database-role-delete
|
||||
|
|
@ -102,12 +100,13 @@
|
|||
{{else}}
|
||||
{{! Edit or Create }}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<form {{on "submit" this.handleCreateEditRole}}>
|
||||
<form {{on "submit" (perform this.handleCreateEditRole)}}>
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
{{#each @model.fieldAttrs as |attr|}}
|
||||
{{#if (eq @mode "edit")}}
|
||||
<ReadonlyFormField @attr={{attr}} @value={{get @model attr.name}} />
|
||||
{{else if (not-eq attr.options.readOnly true)}}
|
||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
|
||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{! TODO: If database && !updateDB show warning }}
|
||||
{{#if (get this.warningMessages attr.name)}}
|
||||
<Hds::Alert @type="inline" @color="warning" class="has-top-margin-negative-s has-bottom-margin-s" as |A|>
|
||||
|
|
@ -117,44 +116,50 @@
|
|||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
<DatabaseRoleSettingForm
|
||||
@attrs={{@model.roleSettingAttrs}}
|
||||
@roleType={{@model.type}}
|
||||
@model={{@model}}
|
||||
@mode={{@mode}}
|
||||
@dbType={{await this.databaseType}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
/>
|
||||
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
|
||||
<div class="field is-grouped">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text={{if (eq @mode "create") "Create role" "Save"}}
|
||||
@icon={{if this.loading "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.loading}}
|
||||
data-test-secret-save
|
||||
/>
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
@model={{@model.backend}}
|
||||
@query={{hash tab="role"}}
|
||||
data-test-database-role-cancel
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
{{#if (not (is-empty-value this.warningMessages))}}
|
||||
<Hds::Alert @type="compact" @color="warning" class="has-left-margin-s" as |A|>
|
||||
<A.Title>Warning</A.Title>
|
||||
<A.Description>
|
||||
You don't have permissions required to
|
||||
{{if (eq @mode "create") "create" "update"}}
|
||||
this role. See form for details.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field is-fullwidth box is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text={{if (eq @mode "create") "Create role" "Save"}}
|
||||
@icon={{if this.handleCreateEditRole.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.handleCreateEditRole.isRunning}}
|
||||
data-test-secret-save
|
||||
/>
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
@model={{@model.backend}}
|
||||
@query={{hash tab="role"}}
|
||||
data-test-database-role-cancel
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<AlertInline
|
||||
data-test-invalid-form-alert
|
||||
class="has-top-padding-s"
|
||||
@type="danger"
|
||||
@message={{this.invalidFormAlert}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (not (is-empty-value this.warningMessages))}}
|
||||
<Hds::Alert @type="compact" @color="warning" class="has-left-margin-s" as |A|>
|
||||
<A.Title>Warning</A.Title>
|
||||
<A.Description>
|
||||
You don't have permissions required to
|
||||
{{if (eq @mode "create") "create" "update"}}
|
||||
this role. See form for details.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
{{#if (and (eq @mode "edit") (eq attr.name "username"))}}
|
||||
<ReadonlyFormField @attr={{attr}} @value={{get @model attr.name}} />
|
||||
{{else}}
|
||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
|
||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} @modelValidations={{@modelValidations}} />
|
||||
{{/if}}
|
||||
|
||||
{{/each}}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,25 @@ module('Integration | Component | database-role-edit', function (hooks) {
|
|||
name: 'my-dynamic-role',
|
||||
id: 'my-dynamic-role',
|
||||
});
|
||||
this.store.pushPayload('database-role', {
|
||||
modelName: 'database/role',
|
||||
database: ['my-mongodb-database'],
|
||||
id: 'test-role',
|
||||
type: 'static',
|
||||
name: 'test-role',
|
||||
});
|
||||
this.modelStatic = this.store.peekRecord('database/role', 'my-static-role');
|
||||
this.modelDynamic = this.store.peekRecord('database/role', 'my-dynamic-role');
|
||||
this.modelEmpty = this.store.peekRecord('database/role', 'test-role');
|
||||
});
|
||||
|
||||
test('it should display form errors when trying to create a role without required fields', async function (assert) {
|
||||
this.server.post('/sys/capabilities-self', capabilitiesStub('database/static-creds/my-role', ['create']));
|
||||
|
||||
await render(hbs`<DatabaseRoleEdit @model={{this.modelEmpty}} @mode="create"/>`);
|
||||
await click('[data-test-secret-save]');
|
||||
|
||||
assert.dom('[data-test-inline-error-message]').exists('Inline form errors exist');
|
||||
});
|
||||
|
||||
test('it should let user edit a static role when given update capability', async function (assert) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue