mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-11 15:26:27 +02:00
🥐Transform-edit-base: remove use in transform-template-edit and alphabet-edit (#29803)
* remove transform-edit-base from alaphabet and transform edit components * cleanup documentation * move hbs file to be next to js * update this.displayErrors to this.errorMessage --------- Co-authored-by: Shannon Roberts <shannon.roberts@hashicorp.com>
This commit is contained in:
parent
49eda90dcd
commit
72cf813cbd
@ -5,44 +5,38 @@
|
||||
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<KeyValueHeader
|
||||
@baseKey={{hash display=this.model.id id=this.model.idForNav}}
|
||||
@path="vault.cluster.secrets.backend.list"
|
||||
@mode={{this.mode}}
|
||||
@root={{this.root}}
|
||||
@showCurrent={{true}}
|
||||
/>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-secret-header="true">
|
||||
{{#if (eq this.mode "create")}}
|
||||
{{#if (eq @mode "create")}}
|
||||
Create Alphabet
|
||||
{{else if (eq this.mode "edit")}}
|
||||
{{else if (eq @mode "edit")}}
|
||||
Edit Alphabet
|
||||
{{else}}
|
||||
Alphabet
|
||||
<code>{{this.model.id}}</code>
|
||||
<code>{{@model.id}}</code>
|
||||
{{/if}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if (eq this.mode "show")}}
|
||||
{{#if (eq @mode "show")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if this.model.updatePath.canDelete}}
|
||||
{{#if @model.updatePath.canDelete}}
|
||||
<Hds::Button
|
||||
@text="Delete alphabet"
|
||||
@color="secondary"
|
||||
class="toolbar-button"
|
||||
{{on "click" (action "delete")}}
|
||||
{{on "click" this.onDelete}}
|
||||
data-test-transformation-alphabet-delete
|
||||
/>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if this.model.updatePath.canUpdate}}
|
||||
{{#if @model.updatePath.canUpdate}}
|
||||
<ToolbarSecretLink
|
||||
@secret={{concat this.model.idPrefix this.model.id}}
|
||||
@secret={{concat @model.idPrefix @model.id}}
|
||||
@mode="edit"
|
||||
data-test-edit-link={{true}}
|
||||
@replace={{true}}
|
||||
@ -54,13 +48,13 @@
|
||||
</Toolbar>
|
||||
{{/if}}
|
||||
|
||||
{{#if (or (eq this.mode "edit") (eq this.mode "create"))}}
|
||||
<form onsubmit={{action "createOrUpdate" this.mode}}>
|
||||
{{#if (or (eq @mode "edit") (eq @mode "create"))}}
|
||||
<form onsubmit={{this.createOrUpdate}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{this.model}} />
|
||||
<NamespaceReminder @mode={{this.mode}} @noun="transform alphabet" />
|
||||
{{#each this.model.attrs as |attr|}}
|
||||
{{#if (and (eq attr.name "name") (eq this.mode "edit"))}}
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
<NamespaceReminder @mode={{@mode}} @noun="transform alphabet" />
|
||||
{{#each @model.attrs as |attr|}}
|
||||
{{#if (and (eq attr.name "name") (eq @mode "edit"))}}
|
||||
<label for={{attr.name}} class="is-label">
|
||||
{{attr.options.label}}
|
||||
</label>
|
||||
@ -72,29 +66,29 @@
|
||||
id={{attr.name}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
value={{or (get this.model attr.name) this.model.id}}
|
||||
value={{or (get @model attr.name) @model.id}}
|
||||
readonly
|
||||
class="field input is-readOnly"
|
||||
type={{attr.type}}
|
||||
/>
|
||||
{{else}}
|
||||
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
|
||||
<FormField data-test-field @attr={{attr}} @model={{@model}} />
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text={{if (eq this.mode "create") "Create alphabet" "Save"}}
|
||||
@text={{if (eq @mode "create") "Create alphabet" "Save"}}
|
||||
type="submit"
|
||||
data-test-alphabet-transform-create
|
||||
/>
|
||||
{{#if (eq this.mode "create")}}
|
||||
{{#if (eq @mode "create")}}
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
@model={{this.model.backend}}
|
||||
@model={{@model.backend}}
|
||||
@query={{hash tab="alphabet"}}
|
||||
/>
|
||||
{{else}}
|
||||
@ -102,7 +96,7 @@
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.show"
|
||||
@models={{array this.model.backend (concat "alphabet/" this.model.id)}}
|
||||
@models={{array @model.backend (concat "alphabet/" @model.id)}}
|
||||
@query={{hash tab="alphabet"}}
|
||||
/>
|
||||
{{/if}}
|
||||
@ -110,29 +104,24 @@
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
{{#if this.model.displayErrors}}
|
||||
<div class="has-top-margin-s">
|
||||
<MessageError @model={{this.model}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each this.model.attrs as |attr|}}
|
||||
{{#each @model.attrs as |attr|}}
|
||||
{{#if (eq attr.type "object")}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{stringify (get this.model attr.name)}}
|
||||
@value={{stringify (get @model attr.name)}}
|
||||
/>
|
||||
{{else if (eq attr.type "array")}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{get this.model attr.name}}
|
||||
@value={{get @model attr.name}}
|
||||
@type={{attr.type}}
|
||||
@isLink={{eq attr.name "transformations"}}
|
||||
/>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{get this.model attr.name}}
|
||||
@value={{get @model attr.name}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
@ -3,6 +3,82 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import TransformBase from './transform-edit-base';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
export default TransformBase;
|
||||
/**
|
||||
* @module AlphabetEdit
|
||||
* `AlphabetEdit` is a component that allows you to create/edit or view an alphabet.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <AlphabetEdit @model={{this.model}} @mode={{this.mode}} />
|
||||
* ```
|
||||
* @param {object} model - This is the transform alphabet model.
|
||||
* @param {string} mode - Is either show, create or edit.
|
||||
*/
|
||||
|
||||
export default class AlphabetEditComponent extends Component {
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
|
||||
@tracked errorMessage = '';
|
||||
|
||||
get breadcrumbs() {
|
||||
// ideally this is created on the controller in the parent route but this is a generic route and adding breadcrumbs to the controller requires a larger refactor.
|
||||
const { backend } = this.args.model;
|
||||
return [
|
||||
{
|
||||
label: backend,
|
||||
route: 'vault.cluster.secrets.backend.list-root',
|
||||
model: backend,
|
||||
query: { tab: 'alphabet' },
|
||||
},
|
||||
{ label: 'Alphabet' },
|
||||
];
|
||||
}
|
||||
|
||||
transition(route = 'show') {
|
||||
this.errorMessage = '';
|
||||
const { backend, id } = this.args.model;
|
||||
if (route === 'list') {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.list-root', backend, {
|
||||
queryParams: { tab: 'alphabet' },
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.show', `alphabet/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@action async createOrUpdate(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!this.args.model.hasDirtyAttributes) {
|
||||
this.flashMessages.info('No changes detected.');
|
||||
this.transition();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.args.model.save();
|
||||
this.flashMessages.success('Alphabet saved.');
|
||||
this.transition();
|
||||
} catch (e) {
|
||||
this.errorMessage = errorMessage(e);
|
||||
}
|
||||
}
|
||||
|
||||
@action async onDelete() {
|
||||
try {
|
||||
await this.args.model.destroyRecord();
|
||||
this.flashMessages.success('Alphabet deleted.');
|
||||
this.transition('list');
|
||||
} catch (e) {
|
||||
this.errorMessage = errorMessage(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,62 +5,51 @@
|
||||
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<KeyValueHeader
|
||||
@baseKey={{hash display=this.model.id id=this.model.idForNav}}
|
||||
@path="vault.cluster.secrets.backend.list"
|
||||
@mode={{this.mode}}
|
||||
@root={{this.root}}
|
||||
@showCurrent={{true}}
|
||||
/>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-secret-header="true">
|
||||
{{#if (eq this.mode "create")}}
|
||||
{{#if (eq @mode "create")}}
|
||||
Create Template
|
||||
{{else if (eq this.mode "edit")}}
|
||||
{{else if (eq @mode "edit")}}
|
||||
Edit Template
|
||||
{{else}}
|
||||
Template
|
||||
<code>{{this.model.id}}</code>
|
||||
<code>{{@model.id}}</code>
|
||||
{{/if}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if (eq this.mode "show")}}
|
||||
{{#if (eq @mode "show")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if this.model.updatePath.canDelete}}
|
||||
{{#if @model.updatePath.canDelete}}
|
||||
<Hds::Button
|
||||
@text="Delete template"
|
||||
@color="secondary"
|
||||
class="toolbar-button"
|
||||
{{on "click" (action "delete")}}
|
||||
{{on "click" this.onDelete}}
|
||||
data-test-transformation-template-delete
|
||||
/>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if this.model.updatePath.canUpdate}}
|
||||
<ToolbarSecretLink
|
||||
@secret={{concat this.model.idPrefix this.model.id}}
|
||||
@mode="edit"
|
||||
data-test-edit-link={{true}}
|
||||
@replace={{true}}
|
||||
>
|
||||
{{#if @model.updatePath.canUpdate}}
|
||||
<ToolbarLink @route="vault.cluster.secrets.backend.edit" @model={{concat "template/" @model.id}} data-test-edit-link>
|
||||
Edit template
|
||||
</ToolbarSecretLink>
|
||||
</ToolbarLink>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
{{/if}}
|
||||
|
||||
{{#if (or (eq this.mode "edit") (eq this.mode "create"))}}
|
||||
<form onsubmit={{action "createOrUpdate" this.mode}}>
|
||||
{{#if (or (eq @mode "edit") (eq @mode "create"))}}
|
||||
<form onsubmit={{this.createOrUpdate}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{this.model}} />
|
||||
<NamespaceReminder @mode={{this.mode}} @noun="transform template" />
|
||||
{{#each this.model.writeAttrs as |attr|}}
|
||||
{{#if (and (eq attr.name "name") (eq this.mode "edit"))}}
|
||||
<NamespaceReminder @mode={{@mode}} @noun="transform template" />
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
{{#each @model.writeAttrs as |attr|}}
|
||||
{{#if (and (eq attr.name "name") (eq @mode "edit"))}}
|
||||
<label for={{attr.name}} class="is-label">
|
||||
{{attr.options.label}}
|
||||
</label>
|
||||
@ -72,32 +61,32 @@
|
||||
id={{attr.name}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
value={{or (get this.model attr.name) attr.options.defaultValue}}
|
||||
value={{or (get @model attr.name) attr.options.defaultValue}}
|
||||
readonly={{true}}
|
||||
class="field input is-readOnly"
|
||||
type={{attr.type}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if (eq attr.name "alphabet")}}
|
||||
<TransformAdvancedTemplating @model={{this.model}} />
|
||||
<TransformAdvancedTemplating @model={{@model}} />
|
||||
{{/if}}
|
||||
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
|
||||
<FormField data-test-field @attr={{attr}} @model={{@model}} />
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text={{if (eq this.mode "create") "Create template" "Save"}}
|
||||
@text={{if (eq @mode "create") "Create template" "Save"}}
|
||||
type="submit"
|
||||
data-test-template-transform-create
|
||||
/>
|
||||
{{#if (eq this.mode "create")}}
|
||||
{{#if (eq @mode "create")}}
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
@model={{this.model.backend}}
|
||||
@model={{@model.backend}}
|
||||
@query={{hash tab="template"}}
|
||||
/>
|
||||
{{else}}
|
||||
@ -105,7 +94,7 @@
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.show"
|
||||
@models={{array this.model.backend (concat "template/" this.model.id)}}
|
||||
@models={{array @model.backend (concat "template/" @model.id)}}
|
||||
@query={{hash tab="template"}}
|
||||
/>
|
||||
{{/if}}
|
||||
@ -113,19 +102,14 @@
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
{{#if this.model.displayErrors}}
|
||||
<div class="has-top-margin-s">
|
||||
<MessageError @model={{this.model}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each this.model.readAttrs as |attr|}}
|
||||
{{#each @model.readAttrs as |attr|}}
|
||||
{{#let (capitalize (or attr.options.label (humanize (dasherize attr.name)))) as |label|}}
|
||||
{{#if (eq attr.name "decodeFormats")}}
|
||||
{{#if (not (is-empty-value this.model.decodeFormats))}}
|
||||
{{#if (not (is-empty-value @model.decodeFormats))}}
|
||||
<InfoTableRow @label={{label}}>
|
||||
<div>
|
||||
{{#each-in this.model.decodeFormats as |key value|}}
|
||||
{{#each-in @model.decodeFormats as |key value|}}
|
||||
<div class="transform-decode-formats">
|
||||
<p class="is-label has-text-grey-light">{{key}}</p>
|
||||
<p>{{value}}</p>
|
||||
@ -137,7 +121,7 @@
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@label={{label}}
|
||||
@value={{get this.model attr.name}}
|
||||
@value={{get @model attr.name}}
|
||||
class={{if (eq attr.name "pattern") "transform-pattern-text"}}
|
||||
/>
|
||||
{{/if}}
|
||||
@ -3,6 +3,82 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import TransformBase from './transform-edit-base';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
export default TransformBase;
|
||||
/**
|
||||
* @module TransformTemplateEdit
|
||||
* `TransformTemplateEdit` is a component that allows you to create/edit or view a transform template.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <TransformTemplateEdit }} />
|
||||
* ```
|
||||
* @param {object} model - This is the transform template model.
|
||||
* @param {string} mode - Is either show, create or edit.
|
||||
*/
|
||||
|
||||
export default class TransformTemplateEditComponent extends Component {
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
|
||||
@tracked errorMessage = '';
|
||||
|
||||
get breadcrumbs() {
|
||||
// ideally this is created on the controller in the parent route but this is a generic route and adding breadcrumbs to the controller requires a larger refactor.
|
||||
const { backend } = this.args.model;
|
||||
return [
|
||||
{
|
||||
label: backend,
|
||||
route: 'vault.cluster.secrets.backend.list-root',
|
||||
model: backend,
|
||||
query: { tab: 'template' },
|
||||
},
|
||||
{ label: 'Template' },
|
||||
];
|
||||
}
|
||||
|
||||
transition(route = 'show') {
|
||||
this.errorMessage = '';
|
||||
const { backend, id } = this.args.model;
|
||||
if (route === 'list') {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.list-root', backend, {
|
||||
queryParams: { tab: 'template' },
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.show', `template/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@action async createOrUpdate(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!this.args.model.hasDirtyAttributes) {
|
||||
this.flashMessages.info('No changes detected.');
|
||||
this.transition();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.args.model.save();
|
||||
this.flashMessages.success('Transform template saved.');
|
||||
this.transition();
|
||||
} catch (e) {
|
||||
this.errorMessage = errorMessage(e);
|
||||
}
|
||||
}
|
||||
|
||||
@action async onDelete() {
|
||||
try {
|
||||
await this.args.model.destroyRecord();
|
||||
this.flashMessages.success('Transform template deleted.');
|
||||
this.transition('list');
|
||||
} catch (e) {
|
||||
this.errorMessage = errorMessage(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,11 +10,6 @@ import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
export default class Alphabet extends Model {
|
||||
idPrefix = 'alphabet/';
|
||||
|
||||
get idForNav() {
|
||||
const modelId = this.id || '';
|
||||
return `${this.idPrefix}${modelId}`;
|
||||
}
|
||||
|
||||
@attr('string', {
|
||||
readOnly: true,
|
||||
subText: 'The alphabet name. Keep in mind that spaces are not allowed and this cannot be edited later.',
|
||||
|
||||
@ -4,28 +4,28 @@
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
|
||||
export default Model.extend({
|
||||
idPrefix: 'template/',
|
||||
idForNav: computed('id', 'idPrefix', function () {
|
||||
const modelId = this.id || '';
|
||||
return `${this.idPrefix}${modelId}`;
|
||||
}),
|
||||
export default class TransformTemplate extends Model {
|
||||
idPrefix = 'template/';
|
||||
|
||||
name: attr('string', {
|
||||
@attr('string', {
|
||||
readOnly: true,
|
||||
subText:
|
||||
'Templates allow Vault to determine what and how to capture the value to be transformed. This cannot be edited later.',
|
||||
}),
|
||||
type: attr('string', { defaultValue: 'regex' }),
|
||||
pattern: attr('string', {
|
||||
})
|
||||
name;
|
||||
|
||||
@attr('string', { defaultValue: 'regex' }) type;
|
||||
|
||||
@attr('string', {
|
||||
editType: 'regex',
|
||||
subText: 'The template’s pattern defines the data format. Expressed in regex.',
|
||||
}),
|
||||
alphabet: attr('array', {
|
||||
})
|
||||
pattern;
|
||||
|
||||
@attr('array', {
|
||||
subText:
|
||||
'Alphabet defines a set of characters (UTF-8) that is used for FPE to determine the validity of plaintext and ciphertext values. You can choose a built-in one, or create your own.',
|
||||
editType: 'searchSelect',
|
||||
@ -34,17 +34,22 @@ export default Model.extend({
|
||||
label: 'Alphabet',
|
||||
models: ['transform/alphabet'],
|
||||
selectLimit: 1,
|
||||
}),
|
||||
encodeFormat: attr('string'),
|
||||
decodeFormats: attr(),
|
||||
backend: attr('string', { readOnly: true }),
|
||||
})
|
||||
alphabet;
|
||||
|
||||
readAttrs: computed(function () {
|
||||
@attr('string') encodeFormat;
|
||||
@attr('') decodeFormats;
|
||||
|
||||
@attr('string', { readOnly: true }) backend;
|
||||
|
||||
get readAttrs() {
|
||||
const keys = ['name', 'pattern', 'encodeFormat', 'decodeFormats', 'alphabet'];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}),
|
||||
writeAttrs: computed(function () {
|
||||
}
|
||||
|
||||
get writeAttrs() {
|
||||
return expandAttributeMeta(this, ['name', 'pattern', 'alphabet']);
|
||||
}),
|
||||
updatePath: lazyCapabilities(apiPath`${'backend'}/template/${'id'}`, 'backend', 'id'),
|
||||
});
|
||||
}
|
||||
|
||||
@lazyCapabilities(apiPath`${'backend'}/template/${'id'}`, 'backend') updatePath;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user