[UI] VAULT-37386 Plugin management: General Settings Route + Templates (#8726) (#8801)

* Move components and routes over to new PR

* Move components to secrets-engine folder

* Use native FormData

* Update params that are passed in

* Add loading state

* Add comments

* Update jsdoc description

* Remove unused action

* Remove debugger

* Fix linting errors

* Add version card component and fix merge conflict issues

Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
Vault Automation 2025-08-21 09:32:42 -06:00 committed by GitHub
parent 9d27d4f837
commit ec0ca21dec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 228 additions and 67 deletions

View file

@ -2,7 +2,7 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12">
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12" ...attributes>
<Hds::Text::Display @size="300">Metadata</Hds::Text::Display>
<div class="flex gap-16 is-flex-column has-top-padding-s">
@ -12,14 +12,12 @@
<F.Control>
<div class="flex column-gap-8">
<Hds::Form::TextInput::Field
@value={{@model.path}}
@value={{@model.secretsEngine.path}}
autocomplete="off"
disabled={{@disabled}}
disabled
class="path-input-text"
name="Path"
data-test-input="path"
{{! TODO: Update onchange handler, maybe match form-field.hbs }}
{{on "input" this.updatePath}}
/>
<Hds::Copy::Button @text="Copy" @isIconOnly={{true}} @targetToCopy=".path-input-text" />
</div>
@ -32,23 +30,21 @@
<F.Control>
<div class="flex column-gap-8">
<Hds::Form::TextInput::Field
@value={{@model.accessor}}
@value={{@model.secretsEngine.accessor}}
autocomplete="off"
disabled={{@disabled}}
disabled
name="Accessor"
class="accessor-input-text"
data-test-input="accessor"
{{! TODO: update onchange accordingly }}
{{on "input" this.updateAccessor}}
/>
<Hds::Copy::Button @text="Copy" @isIconOnly={{true}} @targetToCopy=".accessor-input-text" />
</div>
</F.Control>
</Hds::Form::Fieldset>
<Hds::Form::Textarea::Field name="demo-description" @value={{@model.description}} as |F|>
<Hds::Form::Textarea::Field name="description" @value={{@model.secretsEngine.description}} as |F|>
<F.Label>Description</F.Label>
<F.HelperText>A short description of the secrets engines purpose.</F.HelperText>
<F.HelperText>A short description of the secrets engine's purpose.</F.HelperText>
{{! TODO: Confirm with Design - Is there a length limit for descriptions? Design has it around 765 characters, but not sure theres a backend limit? }}
<F.CharacterCount @maxLength={{500}} />
</Hds::Form::Textarea::Field>

View file

@ -1,24 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import SecretsEngineResource from 'vault/resources/secrets/engine';
interface Args {
model: SecretsEngineResource;
}
export default class Metadata extends Component<Args> {
constructor(owner: unknown, args: Args) {
super(owner, args);
}
updatePath() {
// This method can be used to update the path of the secrets engine.
}
updateAccessor() {
// This method can be used to update the accessor of the secrets engine.
}
}

View file

@ -2,18 +2,17 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12">
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12" ...attributes>
<Hds::Form::Toggle::Group as |G|>
<G.Legend>
<Hds::Text::Display @size="300">Security</Hds::Text::Display>
</G.Legend>
{{! TODO: Toggle fields here are currently hardcoded, will be replaced with actual data from model once wired into parent component }}
<G.ToggleField name="local" {{on "click" this.toggleLocal}} as |F|>
<G.ToggleField name="local" checked={{@model.secretsEngine.local}} as |F|>
<F.Label>Local</F.Label>
<F.HelperText>Secrets stay in one cluster and are not replicated.</F.HelperText>
</G.ToggleField>
<G.ToggleField name="seal-wrap" {{on "click" this.toggleSealWrap}} as |F|>
<G.ToggleField name="seal-wrap" checked={{@model.secretsEngine.seal_wrap}} as |F|>
<F.Label>Seal wrap</F.Label>
<F.HelperText>Wrap secrets with an additional encryption layer using a seal.</F.HelperText>
</G.ToggleField>

View file

@ -2,7 +2,7 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12">
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12" ...attributes>
<Hds::Text::Display @size="300">Version</Hds::Text::Display>
{{! TODO: Having this set up with flex for now, grid might be better? }}
@ -14,9 +14,12 @@
</div>
<div class="flex is-flex-column is-flex-align-start row-gap-12">
<Hds::Text::Body @tag="p" class="hds-border-strong side-padding-4 border-radius-4">{{@model.type}}</Hds::Text::Body>
<Hds::Text::Body
@tag="p"
class="hds-border-strong side-padding-4 border-radius-4"
>{{@model.secretsEngine.type}}</Hds::Text::Body>
{{! TODO: Verify if we want to display the full version or chop down ie. v0.17.1 vs v0.17.1-0.230942309423094... }}
<Hds::Text::Body @tag="p">{{@model.running_plugin_version}}</Hds::Text::Body>
<Hds::Text::Body @tag="p">{{@model.secretsEngine.running_plugin_version}}</Hds::Text::Body>
{{! TODO: leaving as is for now to match design, but we might be removing this if we cant get latest version from some source }}
<Hds::Text::Body @tag="p">v.12.46</Hds::Text::Body>
</div>

View file

@ -1,16 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import SecretsEngineResource from 'vault/resources/secrets/engine';
interface Args {
model: SecretsEngineResource;
}
export default class Version extends Component<Args> {
constructor(owner: unknown, args: Args) {
super(owner, args);
}
}

View file

@ -0,0 +1,38 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
{{#let (engines-display-data @model.secretsEngine.type) as |engineData|}}
<Hds::PageHeader class="page-header" as |PH|>
<PH.Title>{{@model.secretsEngine.id}} configuration</PH.Title>
<PH.Description>{{get engineData "displayName"}}</PH.Description>
<PH.Breadcrumb>
<Hds::Breadcrumb>
<Hds::Breadcrumb::Item @text="Secrets" />
<Hds::Breadcrumb::Item
@text={{@model.secretsEngine.id}}
@route="vault.cluster.secrets.backend.list-root"
@model={{@model.secretsEngine.id}}
/>
<Hds::Breadcrumb::Item @text="Configuration" />
</Hds::Breadcrumb>
</PH.Breadcrumb>
<PH.IconTile @icon={{get engineData "glyph"}} />
<PH.Subtitle>{{get engineData "typeDisplay"}}</PH.Subtitle>
</Hds::PageHeader>
<div class="has-top-margin-l">
<div class="tabs-container box is-marginless is-fullwidth is-paddingless">
<nav class="tabs" aria-label={{@model.secretsEngine.id}}>
<ul>
<li>
<LinkTo @route="vault.cluster.secrets.backend.configuration.general-settings" @model={{@model.secretsEngine.id}}>
General settings
</LinkTo>
</li>
</ul>
</nav>
</div>
</div>
{{/let}}

View file

@ -0,0 +1,65 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<SecretEngine::PageHeader @model={{@model}} />
{{#if this.saveGeneralSettings.isRunning}}
{{! TODO: Fix loading state styles }}
<Hds::Layout::Flex @gap="24" @direction="column" @justify="center" @align="center">
<div class="one-fourth-width">
<Hds::Icon @name="loading-static" @size="24" @stretched={{true}} />
</div>
<Hds::Text::Display @tag="h2">Saving configuration...</Hds::Text::Display>
</Hds::Layout::Flex>
{{else}}
<form method="POST" {{on "submit" (perform this.saveGeneralSettings)}} aria-label="general settings form">
<Hds::Text::Body class="has-top-bottom-margin-xxs">
Mount parameters that you can tune to fit required engine behavior.
</Hds::Text::Body>
<Hds::Layout::Flex @gap="24">
<SecretEngine::Card::Version @model={{@model}} class="is-fullwidth" />
{{! TODO: Lease duration component }}
</Hds::Layout::Flex>
<Hds::Layout::Flex @gap="24">
<SecretEngine::Card::Metadata @model={{@model}} class="is-fullwidth" />
<SecretEngine::Card::Security @model={{@model}} class="is-fullwidth" />
</Hds::Layout::Flex>
<div class="field is-grouped has-top-bottom-margin-12">
<Hds::ButtonSet>
<Hds::Button @text="Save changes" type="submit" disabled={{this.saveGeneralSettings.isRunning}} data-test-submit />
<Hds::Button
@text="Discard"
@color="secondary"
data-test-cancel
{{on "click" (fn (mut this.showUnsavedChangesModal))}}
/>
</Hds::ButtonSet>
</div>
</form>
{{/if}}
{{#if this.showUnsavedChangesModal}}
<Hds::Modal id="unsavedChangesModal" @onClose={{(fn (mut this.showUnsavedChangesModal))}} as |M|>
<M.Header>
Unsaved changes
</M.Header>
<M.Body>
<p class="hds-typography-body-300 hds-foreground-primary">You've made changes to the following
<Hds::Text::Display>{{@model.secretsEngine.id}}</Hds::Text::Display>
settings:</p>
{{! TODO: display what fields were changed }}
<br />
Would you like to apply them?
</M.Body>
<M.Footer as |F|>
<Hds::Button type="button" @text="Save changes" {{on "click" (action (perform this.saveGeneralSettings))}} />
{{! TODO: confirm with design where we want to transition to if Discard changes is clicked }}
<Hds::Button type="button" @text="Discard changes" @color="secondary" {{on "click" F.close}} />
</M.Footer>
</Hds::Modal>
{{/if}}

View file

@ -0,0 +1,60 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { task } from 'ember-concurrency';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import type Router from '@ember/routing/router';
import type FlashMessageService from 'vault/services/flash-messages';
import type ApiService from 'vault/services/api';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
/**
* @module GeneralSettingsComponent is used to configure the SSH secret engine.
*
* @example
* ```js
* <Secrets:Page:GeneralSettings
* @model={{this.model}}
* />
* ```
*
* @param {string} secretsEngine - secrets engine resource
*/
interface Args {
model: {
secretsEngine: SecretsEngineResource;
};
}
export default class GeneralSettingsComponent extends Component<Args> {
@service declare readonly router: Router;
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@tracked errorMessage: string | null = null;
@tracked invalidFormAlert: string | null = null;
@tracked showUnsavedChangesModal = false;
saveGeneralSettings = task(async (event) => {
event.preventDefault();
try {
const fd = new FormData(event.target as HTMLFormElement);
await this.api.sys.mountsTuneConfigurationParameters(this.args.model.secretsEngine.id, {
// TODO: add other params when other card components are made
description: fd.get('description') as string,
});
this.flashMessages.success('Engine settings successfully updated.');
} catch (e) {
// handle error state
const { message } = await this.api.parseError(e);
this.flashMessages.danger(`Try again or check your network connection. ${message}`);
}
});
}

View file

@ -183,6 +183,8 @@ Router.map(function () {
this.mount('pki');
this.route('index', { path: '/' });
this.route('configuration', function () {
this.route('index', { path: '/' });
this.route('general-settings');
// only CONFIGURABLE_SECRET_ENGINES can be configured and access the edit route
this.route('edit');
});

View file

@ -0,0 +1,16 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Route from '@ember/routing/route';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
export default class SecretsBackendConfigurationGeneralSettingsRoute extends Route {
async model() {
const secretsEngine = this.modelFor('vault.cluster.secrets.backend') as SecretsEngineResource;
// TODO: get list of versions using the sys/plugins/catalog endpoint.
return { secretsEngine };
}
}

View file

@ -4,9 +4,23 @@
*/
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import engineDisplayData from 'vault/helpers/engines-display-data';
export default class SecretsBackendConfigurationIndexRoute extends Route {
@service router;
beforeModel() {
const {
secretsEngine: { type },
} = this.modelFor('vault.cluster.secrets.backend.configuration');
const engine = engineDisplayData(type);
if (!engine?.isOldEngine) {
return this.router.replaceWith('vault.cluster.secrets.backend.configuration.general-settings');
}
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const engine = engineDisplayData(resolvedModel.secretsEngine.type);

View file

@ -0,0 +1,6 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<SecretEngine::Page::GeneralSettings @model={{this.model}} />

View file

@ -52,15 +52,17 @@
/>
</li>
{{/each}}
<li>
<LinkTo
@route="vault.cluster.secrets.backend.configuration"
@model={{@model.id}}
data-test-configuration-tab={{true}}
>
Configuration
</LinkTo>
</li>
{{#if options.isOldEngine}}
<li>
<LinkTo
@route="vault.cluster.secrets.backend.configuration"
@model={{@model.id}}
data-test-configuration-tab={{true}}
>
Configuration
</LinkTo>
</li>
{{/if}}
</ul>
</nav>
</div>