UI: Ember deprecation - reopen class (#25851)

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
Chelsea Shaw
2024-03-11 16:44:26 -05:00
committed by GitHub
parent 425b6279b5
commit 8d92c3026b
12 changed files with 91 additions and 363 deletions

View File

@@ -3,16 +3,16 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<form {{action (perform this.saveModel) on="submit"}}>
<form {{on "submit" (perform this.saveModel)}}>
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="save" @noun="Auth Method" />
<MessageError @model={{this.model}} />
{{#if this.model.attrs}}
{{#each this.model.attrs as |attr|}}
<MessageError @model={{@model}} />
{{#if @model.attrs}}
{{#each @model.attrs as |attr|}}
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
{{/each}}
{{else if this.model.fieldGroups}}
<FormFieldGroups @model={{this.model}} @mode={{this.mode}} />
{{else if @model.fieldGroups}}
<FormFieldGroups @model={{@model}} @mode={{this.mode}} />
{{/if}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">

View File

@@ -5,7 +5,7 @@
import AdapterError from '@ember-data/adapter/error';
import { service } from '@ember/service';
import Component from '@ember/component';
import Component from '@glimmer/component';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
@@ -14,40 +14,31 @@ import { waitFor } from '@ember/test-waiters';
* The `AuthConfigForm/Config` is the base form to configure auth methods.
*
* @example
* ```js
* {{auth-config-form/config model.model}}
* ```
* <AuthConfigForm::Config @model={{this.model}} />
*
* @property model=null {DS.Model} - The corresponding auth model that is being configured.
*
*/
const AuthConfigBase = Component.extend({
tagName: '',
model: null,
export default class AuthConfigBase extends Component {
@service flashMessages;
@service router;
flashMessages: service(),
router: service(),
saveModel: task(
waitFor(function* () {
try {
yield this.model.save();
} catch (err) {
// AdapterErrors are handled by the error-message component
// in the form
if (err instanceof AdapterError === false) {
throw err;
}
return;
@task
@waitFor
*saveModel(evt) {
evt.preventDefault();
try {
yield this.args.model.save();
} catch (err) {
// AdapterErrors are handled by the error-message component
// in the form
if (err instanceof AdapterError === false) {
throw err;
}
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
this.flashMessages.success('The configuration was saved successfully.');
})
),
});
AuthConfigBase.reopenClass({
positionalParams: ['model'],
});
export default AuthConfigBase;
return;
}
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
this.flashMessages.success('The configuration was saved successfully.');
}
}

View File

@@ -3,26 +3,26 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<form {{action (perform this.saveModel) on="submit"}}>
<form {{on "submit" (perform this.saveModel)}}>
<div class="box is-sideless is-fullwidth is-marginless">
<MessageError @model={{this.model}} @errorMessage={{this.model.errorMessage}} />
<MessageError @model={{@model}} @errorMessage={{this.errorMessage}} />
<NamespaceReminder @mode="save" @noun="Auth Method" />
{{#each this.model.tuneAttrs as |attr|}}
{{#if (not (includes attr.name this.model.userLockoutConfig.modelAttrs))}}
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
{{#each @model.tuneAttrs as |attr|}}
{{#if (not (includes attr.name @model.userLockoutConfig.modelAttrs))}}
<FormField data-test-field @attr={{attr}} @model={{@model}} />
{{/if}}
{{/each}}
{{#if this.model.supportsUserLockoutConfig}}
{{#if @model.supportsUserLockoutConfig}}
<hr class="has-top-margin-xl has-bottom-margin-l has-background-gray-200" />
<Hds::Text::Display @tag="h2" @size="400" @weight="bold" data-test-user-lockout-section>User lockout configuration</Hds::Text::Display>
<Hds::Text::Body @tag="p" @size="100" @color="faint" class="has-bottom-margin-m">
Specifies the user lockout settings for this auth mount.
</Hds::Text::Body>
{{#each this.model.tuneAttrs as |attr|}}
{{#if (includes attr.name this.model.userLockoutConfig.modelAttrs)}}
<FormField @attr={{attr}} @model={{this.model}} />
{{#each @model.tuneAttrs as |attr|}}
{{#if (includes attr.name @model.userLockoutConfig.modelAttrs)}}
<FormField @attr={{attr}} @model={{@model}} />
{{/if}}
{{/each}}
{{/if}}

View File

@@ -8,63 +8,61 @@ import AuthConfigComponent from './config';
import { service } from '@ember/service';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import { tracked } from '@glimmer/tracking';
import errorMessage from 'vault/utils/error-message';
/**
* @module AuthConfigForm/Options
* The `AuthConfigForm/Options` is options portion of the auth config form.
*
* @example
* ```js
* {{auth-config-form/options model.model}}
* ```
* <AuthConfigForm::Options @model={{this.model}} />
*
* @property model=null {DS.Model} - The corresponding auth model that is being configured.
*
*/
export default AuthConfigComponent.extend({
flashMessages: service(),
router: service(),
export default class AuthConfigOptions extends AuthConfigComponent {
@service flashMessages;
@service router;
saveModel: task(
waitFor(function* () {
const data = this.model.config.serialize();
data.description = this.model.description;
@tracked errorMessage;
if (this.model.supportsUserLockoutConfig) {
data.user_lockout_config = {};
this.model.userLockoutConfig.apiParams.forEach((attr) => {
if (Object.keys(data).includes(attr)) {
data.user_lockout_config[attr] = data[attr];
delete data[attr];
}
});
}
@task
@waitFor
*saveModel(evt) {
evt.preventDefault();
this.errorMessage = null;
const data = this.args.model.config.serialize();
data.description = this.args.model.description;
// token_type should not be tuneable for the token auth method.
if (this.model.methodType === 'token') {
delete data.token_type;
}
try {
yield this.model.tune(data);
} catch (err) {
// AdapterErrors are handled by the error-message component
// in the form
if (err instanceof AdapterError === false) {
throw err;
if (this.args.model.supportsUserLockoutConfig) {
data.user_lockout_config = {};
this.args.model.userLockoutConfig.apiParams.forEach((attr) => {
if (Object.keys(data).includes(attr)) {
data.user_lockout_config[attr] = data[attr];
delete data[attr];
}
});
}
// token_type should not be tuneable for the token auth method.
if (this.args.model.methodType === 'token') {
delete data.token_type;
}
try {
yield this.args.model.tune(data);
} catch (err) {
if (err instanceof AdapterError) {
// because we're not calling model.save the model never updates with
// the error. Forcing the error message by manually setting the errorMessage
try {
this.model.set('errorMessage', err.errors?.join(','));
} catch {
// do nothing
}
// the error, so we set it manually in the component instead.
this.errorMessage = errorMessage(err);
return;
}
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
this.flashMessages.success('The configuration was saved successfully.');
})
),
});
throw err;
}
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
this.flashMessages.success('The configuration was saved successfully.');
}
}

View File

@@ -3,14 +3,14 @@
SPDX-License-Identifier: BUSL-1.1
~}}
{{#let (tabs-for-auth-section this.model this.tabType this.paths) as |tabs|}}
{{#let (tabs-for-auth-section @model this.tabType @paths) as |tabs|}}
{{#if tabs.length}}
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
<nav class="tabs" aria-label="navigation to manage {{this.model.type}}">
<nav class="tabs" aria-label="navigation to manage {{@model.type}}">
<ul>
{{#each tabs as |tab|}}
<li>
<LinkTo @route={{get tab.routeParams 0}} @model={{get tab.routeParams 1}} data-test-auth-section-tab={{true}}>
<LinkTo @route={{get tab.routeParams 0}} @model={{get tab.routeParams 1}} data-test-auth-section-tab>
{{tab.label}}
</LinkTo>
</li>

View File

@@ -3,16 +3,10 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@ember/component';
import Component from '@glimmer/component';
const SectionTabs = Component.extend({
tagName: '',
model: null,
tabType: 'authSettings',
});
SectionTabs.reopenClass({
positionalParams: ['model', 'tabType', 'paths'],
});
export default SectionTabs;
export default class SectionTabs extends Component {
get tabType() {
return this.args.tabType || 'authSettings';
}
}

View File

@@ -1,87 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from '@ember-data/model';
import { assert, debug } from '@ember/debug';
import { typeOf } from '@ember/utils';
import { isArray } from '@ember/array';
/*
*
* attachCapabilities
*
* @param modelClass = An Ember Data model class
* @param capabilities - an Object whose keys will added to the model class as related 'capabilities' models
* and whose values should be functions that return the id of the related capabilities model
*
* definition of capabilities be done shorthand with the apiPath tagged template function
*
*
* @usage
*
* let Model = DS.Model.extend({
* backend: attr(),
* scope: attr(),
* });
*
* export default attachCapabilities(Model, {
* updatePath: apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`,
* });
*
*/
export default function attachCapabilities(modelClass, capabilities) {
const capabilityKeys = Object.keys(capabilities);
const newRelationships = capabilityKeys.reduce((ret, key) => {
ret[key] = belongsTo('capabilities', { async: false, inverse: null });
return ret;
}, {});
//TODO: move this to the application serializer and do it JIT instead of on app boot
debug(`adding new relationships: ${capabilityKeys.join(', ')} to ${modelClass.toString()}`);
modelClass.reopen(newRelationships);
modelClass.reopenClass({
// relatedCapabilities is called in the application serializer's
// normalizeResponse hook to add the capabilities relationships to the
// JSON-API document used by Ember Data
relatedCapabilities(jsonAPIDoc) {
let { data, included } = jsonAPIDoc;
if (!data) {
data = jsonAPIDoc;
}
if (isArray(data)) {
const newData = data.map(this.relatedCapabilities);
return {
data: newData,
included,
};
}
const context = {
id: data.id,
...data.attributes,
};
for (const newCapability of capabilityKeys) {
const templateFn = capabilities[newCapability];
const type = typeOf(templateFn);
assert(`expected value of ${newCapability} to be a function but found ${type}.`, type === 'function');
data.relationships[newCapability] = {
data: {
type: 'capabilities',
id: templateFn(context),
},
};
}
if (included) {
return {
data,
included,
};
} else {
return data;
}
},
});
return modelClass;
}

View File

@@ -30,5 +30,5 @@
{{/if}}
</p.levelRight>
</PageHeader>
{{section-tabs this.model "authShow"}}
<SectionTabs @model={{this.model}} @tabType="authShow" />
{{component (concat "auth-method/" this.section) model=this.model}}

View File

@@ -25,7 +25,7 @@
</div>
{{/if}}
{{section-tabs this.model "authShow" this.paths}}
<SectionTabs @model={{this.model}} @tabType="authShow" @paths={{this.paths}} />
{{#if (eq this.section "configuration")}}
<Toolbar>

View File

@@ -19,7 +19,7 @@
</p.levelLeft>
</PageHeader>
{{section-tabs this.model}}
<SectionTabs @model={{this.model}} />
<Toolbar>
<ToolbarActions>

View File

@@ -4,7 +4,7 @@
~}}
{{#if (eq this.model.section "options")}}
{{auth-config-form/options this.model.model}}
<AuthConfigForm::Options @model={{this.model.model}} />
{{else}}
{{auth-config-form/config this.model.model}}
<AuthConfigForm::Config @model={{this.model.model}} />
{{/if}}

View File

@@ -1,168 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Model from '@ember-data/model';
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import attachCapabilities from 'vault/lib/attach-capabilities';
import apiPath from 'vault/utils/api-path';
const MODEL_TYPE = 'test-form-model';
const makeModelClass = () => {
return Model.extend();
};
module('Unit | lib | attach capabilities', function (hooks) {
setupTest(hooks);
test('it attaches passed capabilities', function (assert) {
let mc = makeModelClass();
mc = attachCapabilities(mc, {
updatePath: apiPath`update/{'id'}`,
deletePath: apiPath`delete/{'id'}`,
});
let relationship = mc.relationshipsByName.get('updatePath');
assert.strictEqual(relationship.key, 'updatePath', 'has updatePath relationship');
assert.strictEqual(relationship.kind, 'belongsTo', 'kind of relationship is belongsTo');
assert.strictEqual(relationship.type, 'capabilities', 'updatePath is a related capabilities model');
relationship = mc.relationshipsByName.get('deletePath');
assert.strictEqual(relationship.key, 'deletePath', 'has deletePath relationship');
assert.strictEqual(relationship.kind, 'belongsTo', 'kind of relationship is belongsTo');
assert.strictEqual(relationship.type, 'capabilities', 'deletePath is a related capabilities model');
});
test('it adds a static method to the model class', function (assert) {
let mc = makeModelClass();
mc = attachCapabilities(mc, {
updatePath: apiPath`update/{'id'}`,
deletePath: apiPath`delete/{'id'}`,
});
const relatedCapabilities = !!mc.relatedCapabilities && typeof mc.relatedCapabilities === 'function';
assert.true(relatedCapabilities, 'model class now has a relatedCapabilities static function');
});
test('calling static method with single response JSON-API document adds expected relationships', function (assert) {
let mc = makeModelClass();
mc = attachCapabilities(mc, {
updatePath: apiPath`update/${'id'}`,
deletePath: apiPath`delete/${'id'}`,
});
const jsonAPIDocSingle = {
data: {
id: 'test',
type: MODEL_TYPE,
attributes: {},
relationships: {},
},
included: [],
};
const expected = {
data: {
id: 'test',
type: MODEL_TYPE,
attributes: {},
relationships: {
updatePath: {
data: {
type: 'capabilities',
id: 'update/test',
},
},
deletePath: {
data: {
type: 'capabilities',
id: 'delete/test',
},
},
},
},
included: [],
};
mc.relatedCapabilities(jsonAPIDocSingle);
assert.strictEqual(
Object.keys(jsonAPIDocSingle.data.relationships).length,
2,
'document now has 2 relationships'
);
assert.deepEqual(jsonAPIDocSingle, expected, 'has the exected new document structure');
});
test('calling static method with an arrary response JSON-API document adds expected relationships', function (assert) {
let mc = makeModelClass();
mc = attachCapabilities(mc, {
updatePath: apiPath`update/${'id'}`,
deletePath: apiPath`delete/${'id'}`,
});
const jsonAPIDocSingle = {
data: [
{
id: 'test',
type: MODEL_TYPE,
attributes: {},
relationships: {},
},
{
id: 'foo',
type: MODEL_TYPE,
attributes: {},
relationships: {},
},
],
included: [],
};
const expected = {
data: [
{
id: 'test',
type: MODEL_TYPE,
attributes: {},
relationships: {
updatePath: {
data: {
type: 'capabilities',
id: 'update/test',
},
},
deletePath: {
data: {
type: 'capabilities',
id: 'delete/test',
},
},
},
},
{
id: 'foo',
type: MODEL_TYPE,
attributes: {},
relationships: {
updatePath: {
data: {
type: 'capabilities',
id: 'update/foo',
},
},
deletePath: {
data: {
type: 'capabilities',
id: 'delete/foo',
},
},
},
},
],
included: [],
};
mc.relatedCapabilities(jsonAPIDocSingle);
assert.deepEqual(jsonAPIDocSingle, expected, 'has the exected new document structure');
});
});