UI: Updating KVV2 to use new config/tune flow (#11256) (#11511)

* separate header comp

* replacing header

* redirect to general settings

* moving kv configure under plugin settings

* add exit button

* removing all use of old header with new, updated logic

* reuse secretPath, add button to badge

* test updates pt1

* test updates pt2, refactors

* test fixes

* testing

* removing extendedConfig

* put tabs out of header

* adding new config edit page & updates

* adding page test

* pr comments

* replace type with effectiveType

* test fixes

* adding badges, cleanup test

Co-authored-by: Dan Rivera <dan.rivera@hashicorp.com>
This commit is contained in:
Vault Automation 2025-12-18 16:19:37 -07:00 committed by GitHub
parent 05c153c70d
commit 5013a5e764
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 701 additions and 268 deletions

View file

@ -111,6 +111,7 @@ export default class App extends Application {
externalRoutes: {
secrets: 'vault.cluster.secrets.backends',
syncDestination: 'vault.cluster.sync.secrets.destinations.destination',
secretsGeneralSettingsConfiguration: 'vault.cluster.secrets.backend.configuration.general-settings',
},
},
},

View file

@ -21,7 +21,7 @@
</:actions>
</Page::Header>
<Mount::ConfigureTabs
@configRoute={{if engineDisplayData.isConfigurable (or engineDisplayData.configRoute "configuration.plugin-settings")}}
@configRoute={{this.configRoute}}
@displayName={{engineDisplayData.displayName}}
@path={{@model.secretsEngine.id}}
/>

View file

@ -14,6 +14,7 @@ import type FlashMessageService from 'vault/services/flash-messages';
import type ApiService from 'vault/services/api';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type UnsavedChangesService from 'vault/services/unsaved-changes';
import engineDisplayData from 'vault/helpers/engines-display-data';
const CHARACTER_LIMIT = 500;
@ -65,6 +66,19 @@ export default class GeneralSettingsComponent extends Component<Args> {
return changedFieldsCopy;
}
get configRoute() {
const engine = this.args.model.secretsEngine;
const isKvv2 = engine.version === 2 && engine.effectiveEngineType === 'kv';
const engineMetadata = engineDisplayData(engine.effectiveEngineType);
// Kvv2 is configurable but shares metadata with Kvv1 so isConfigurable is left unset
if (engineMetadata.isConfigurable || isKvv2) {
return engineMetadata.configRoute || 'configuration.plugin-settings';
} else {
return false;
}
}
validateTtl(ttlValue: FormDataEntryValue | number | null) {
if (isNaN(Number(ttlValue))) {
return false;

View file

@ -22,7 +22,7 @@
</:actions>
</Page::Header>
<Mount::ConfigureTabs
@configRoute={{or engineDisplayData.configRoute "configuration.plugin-settings"}}
@configRoute={{this.configRoute}}
@displayName={{engineDisplayData.displayName}}
@path={{@model.secretsEngine.id}}
/>

View file

@ -79,6 +79,19 @@ export default class PluginSettingsComponent extends Component<Args> {
}
}
get configRoute() {
const engine = this.args.model.secretsEngine;
const isKvv2 = engine.version === 2 && engine.effectiveEngineType === 'kv';
const engineMetadata = engineDisplayData(engine.effectiveEngineType);
// Kvv2 is configurable but shares metadata with Kvv1 so isConfigurable is left unset
if (engineMetadata.isConfigurable || isKvv2) {
return engineMetadata.configRoute || 'configuration.plugin-settings';
} else {
return false;
}
}
label = (field: string) => {
const label = toLabel([field]);
// convert words like id and ttl to uppercase

View file

@ -159,6 +159,7 @@ export const ALL_ENGINES: EngineDisplayData[] = [
pluginCategory: 'generic',
displayName: 'KV',
engineRoute: 'kv.list',
configRoute: 'kv.configuration', // only utilized to display config data for kvv2, not in conjunction with isConfigurable as templates determine whether engine is kv v1 or v2
glyph: 'key-values',
mountCategory: ['secret'],
type: 'kv',

View file

@ -0,0 +1,62 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title={{@pageTitle}} @description={{if @backend "KV" ""}} @icon={{@backend.icon}}>
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</:breadcrumbs>
<:actions>
{{#if (has-block "actions")}}
{{yield to="actions"}}
{{/if}}
</:actions>
<:badges>
{{#if (has-block "badges")}}
{{yield to="badges"}}
{{/if}}
{{#if @secretPath}}
<Hds::Copy::Button
@isIconOnly={{true}}
@text="Copy your secret path"
@textToCopy={{@secretPath}}
data-test-copy-button
/>
{{/if}}
</:badges>
</Page::Header>
{{#if @configRoute}}
<Mount::ConfigureTabs
@configRoute={{@configRoute}}
@displayName="KV"
@path={{@backend.id}}
@externalRoute="secretsGeneralSettingsConfiguration"
/>
{{else}}
{{#if (has-block "tabs")}}
<div class="tabs-container box is-marginless is-fullwidth is-paddingless">
<nav class="tabs" aria-label="kv tabs">
<ul>
{{yield to="tabs"}}
</ul>
</nav>
</div>
{{/if}}
{{/if}}
{{#if (has-block "syncDetails")}}
{{yield to="syncDetails"}}
{{/if}}
{{#if (or (has-block "toolbarFilters") (has-block "toolbarActions"))}}
<Toolbar aria-label="menu items for managing {{or @mountName @secretPath @pageTitle}}">
<ToolbarFilters aria-label="filters for secrets list">
{{yield to="toolbarFilters"}}
</ToolbarFilters>
<ToolbarActions aria-label="actions for secrets">
{{yield to="toolbarActions"}}
</ToolbarActions>
</Toolbar>
{{/if}}

View file

@ -1,49 +0,0 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-header-title>
{{#if @mountName}}
<Icon @name="key-values" @size="24" class="has-text-grey-light" />
{{@mountName}}
<Hds::Badge @text="version 2" />
{{else if @secretPath}}
{{@secretPath}}
<Hds::Copy::Button @isIconOnly={{true}} @text="Copy your secret path" @textToCopy={{@secretPath}} />
{{else}}
{{@pageTitle}}
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
{{#if (has-block "syncDetails")}}
{{yield to="syncDetails"}}
{{/if}}
{{#if (has-block "tabLinks")}}
<div class="tabs-container box is-bottomless is-marginless is-paddingless">
<nav class="tabs" aria-label="kv tabs">
<ul>
{{yield to="tabLinks"}}
</ul>
</nav>
</div>
{{/if}}
{{#if (or (has-block "toolbarFilters") (has-block "toolbarActions"))}}
<Toolbar aria-label="menu items for managing {{or @mountName @secretPath @pageTitle}}">
<ToolbarFilters aria-label="filters for secrets list">
{{yield to="toolbarFilters"}}
</ToolbarFilters>
<ToolbarActions aria-label="actions for secrets">
{{yield to="toolbarActions"}}
</ToolbarActions>
</Toolbar>
{{/if}}

View file

@ -3,12 +3,25 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @mountName={{@backend}}>
<:tabLinks>
<li><LinkTo @route="list" data-test-secrets-tab="Secrets">Secrets</LinkTo></li>
<li><LinkTo @route="configuration" data-test-secrets-tab="Configuration">Configuration</LinkTo></li>
</:tabLinks>
</KvPageHeader>
<KvHeader
@pageTitle="{{@backend.id}} configuration"
@backend={{@backend}}
@breadcrumbs={{@breadcrumbs}}
@configRoute="configuration"
>
<:badges>
<Hds::Badge @text="version 2" data-test-badge />
</:badges>
<:actions>
<Hds::Button @color="secondary" @route="list" @text="Exit configuration" data-test-button="Exit configuration" />
</:actions>
<:toolbarActions>
<ToolbarLink @route="configure" data-test-secret-backend-configure>
Edit configuration
</ToolbarLink>
</:toolbarActions>
</KvHeader>
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each-in @config as |key value|}}

View file

@ -0,0 +1,63 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<KvHeader
@pageTitle="{{@backend.id}} configuration"
@backend={{@backend}}
@breadcrumbs={{@breadcrumbs}}
@configRoute="configure"
>
<:badges>
<Hds::Badge @text="version 2" data-test-badge />
</:badges>
<:actions>
<Hds::Button @color="secondary" @route="list" @text="Exit configuration" data-test-button="Exit configuration" />
</:actions>
<:toolbarActions>
</:toolbarActions>
</KvHeader>
<form {{on "submit" (perform this.save)}}>
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="update" @noun="KV secret metadata" />
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
{{#each @form.metadataFields as |field|}}
{{#unless (eq field.name "custom_metadata")}}
<FormField @attr={{field}} @model={{@form}} @modelValidations={{this.modelValidations}} />
{{/unless}}
{{/each}}
</div>
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<div class="has-top-padding-s">
<Hds::Button
@text="Save"
@icon={{if this.save.isRunning "loading"}}
type="submit"
disabled={{this.save.isRunning}}
data-test-kv-save
/>
<Hds::Button
@text="Cancel"
@color="secondary"
class="has-left-margin-s"
disabled={{this.save.isRunning}}
{{on "click" this.navigateToConfiguration}}
data-test-kv-cancel
/>
{{#if this.invalidFormAlert}}
<div class="control">
<AlertInline
data-test-invalid-form-alert
@type="danger"
class="has-top-padding-s"
@message={{this.invalidFormAlert}}
/>
</div>
{{/if}}
</div>
</div>
</form>

View file

@ -0,0 +1,67 @@
/**
* Copyright IBM Corp. 2016, 2025
* 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 { task } from 'ember-concurrency';
import type KvForm from 'vault/app/forms/secrets/kv';
import type { Breadcrumb } from 'vault/vault/app-types';
import type FlashMessageService from 'vault/services/flash-messages';
import type RouterService from '@ember/routing/router-service';
import type ApiService from 'vault/services/api';
import SecretsEngineResource from 'vault/app/resources/secrets/engine';
interface Args {
form: KvForm;
backend: SecretsEngineResource;
breadcrumbs: Array<Breadcrumb>;
}
/**
* @module KvConfigurePageComponent
* KvConfigurePageComponent is a component to show secrets mount and engine configuration data
*
* @param {object} form - config form data for mount and engine
* @param {string} backend - The kv secrets engine data
* @param {array} breadcrumbs - Breadcrumbs as an array of objects that contain label, route, and modelId. They are updated via the util kv-breadcrumbs to handle dynamic *pathToSecret on the list-directory route.
*/
export default class KvConfigurePageComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@service('app-router') declare readonly router: RouterService;
@service declare readonly api: ApiService;
@tracked errorBanner = '';
@tracked invalidFormAlert = '';
@tracked modelValidations = null;
@action
navigateToConfiguration() {
this.router.transitionTo(`vault.cluster.secrets.backend.kv.configuration`);
}
@task
*save(event: Event | null) {
event.preventDefault();
try {
const { isValid, state, invalidFormMessage, data } = this.args.form.toJSON();
this.modelValidations = isValid ? null : state;
this.invalidFormAlert = invalidFormMessage;
if (isValid) {
yield this.api.secrets.kvV2Configure(data.path, data);
this.flashMessages.success(`Successfully updated ${data.path}'s configuration.`);
this.navigateToConfiguration();
}
} catch (error) {
const { message } = yield this.api.parseError(error);
this.errorBanner = message;
this.invalidFormAlert = 'There was an error submitting this form.';
}
}
}

View file

@ -3,39 +3,47 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @mountName={{@backend}}>
<:tabLinks>
<li>
<LinkTo
@route={{this.router.currentRoute.localName}}
@models={{@currentRouteParams}}
data-test-secrets-tab="Secrets"
@current-when={{true}}
>Secrets</LinkTo>
</li>
<li>
<LinkTo @route="configuration" @model={{@backend}} data-test-secrets-tab="Configuration">Configuration</LinkTo>
</li>
</:tabLinks>
<KvHeader @pageTitle={{@backendModel.id}} @backend={{@backendModel}} @breadcrumbs={{@breadcrumbs}} @filter={{@filterValue}}>
<:toolbarFilters>
{{#if (and (not-eq @secrets 403) (or @secrets @filterValue))}}
<KvListFilter @mountPoint={{this.mountPoint}} @filterValue={{@filterValue}} />
{{/if}}
</:toolbarFilters>
<:toolbarActions>
<ToolbarLink
data-test-toolbar-create-secret
<:tabs>
<li><LinkTo @route="list" data-test-tab="Secrets" @model={{@backend}}>Secrets</LinkTo></li>
</:tabs>
<:badges>
<Hds::Badge @text="version 2" data-test-badge />
</:badges>
<:actions>
<Hds::Dropdown as |D|>
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
<D.Interactive
@icon="settings"
@route="configuration"
@model={{@backendModel.id}}
data-test-popup-menu="Configure"
>Configure</D.Interactive>
<D.Interactive
{{on "click" (fn (mut this.engineToDisable) @backendModel)}}
@color="critical"
@icon="trash"
data-test-popup-menu="Delete"
>Delete</D.Interactive>
</Hds::Dropdown>
<Hds::Button
@text="Create secret"
@icon="plus"
@route="create"
@model={{@backend}}
@model={{@backendModel}}
@query={{hash initialKey=@filterValue}}
@type="add"
>
Create secret
</ToolbarLink>
</:toolbarActions>
</KvPageHeader>
data-test-button="create secret"
/>
</:actions>
</KvHeader>
{{#if (eq @secrets 403)}}
<div class="box is-fullwidth is-shadowless has-tall-padding">
@ -177,4 +185,14 @@
/>
{{/if}}
{{/if}}
{{/if}}
{{#if this.engineToDisable}}
<ConfirmModal
@color="critical"
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@onClose={{fn (mut this.engineToDisable) null}}
@onConfirm={{perform this.disableEngine this.engineToDisable}}
/>
{{/if}}

View file

@ -10,6 +10,7 @@ import { tracked } from '@glimmer/tracking';
import { getOwner } from '@ember/owner';
import { ancestorKeysForKey } from 'core/utils/key-utils';
import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs';
import { task } from 'ember-concurrency';
/**
* @module List
@ -31,6 +32,7 @@ export default class KvListPageComponent extends Component {
@tracked secretPath;
@tracked metadataToDelete = null; // set to the metadata intended to delete
@tracked engineToDisable = undefined;
// used for KV list and list-directory view
// ex: beep/
@ -57,6 +59,24 @@ export default class KvListPageComponent extends Component {
};
}
@task
*disableEngine(engine) {
const { engineType, id, path } = engine;
try {
yield this.api.sys.mountsDisableSecretsEngine(id);
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
this.router.transitionTo('vault.cluster.secrets.backends');
} catch (err) {
const { message } = yield this.api.parseError(err);
this.flashMessages.danger(
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
);
} finally {
this.engineToDisable = undefined;
}
}
@action
async onDelete(secretPath) {
try {

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @secretPath={{@path}}>
<KvHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}} @secretPath={{@path}}>
<:syncDetails>
{{#if this.syncStatus}}
<Hds::Alert data-test-sync-alert @type="inline" class="has-top-margin-s has-bottom-margin-m" @color="neutral" as |A|>
@ -45,7 +45,7 @@
{{/if}}
</:syncDetails>
<:tabLinks>
<:tabs>
<li>
<LinkTo @route="secret.index" @models={{array @backend @path}} data-test-secrets-tab="Overview">Overview</LinkTo>
</li>
@ -67,7 +67,7 @@
>Version History</LinkTo>
</li>
{{/if}}
</:tabLinks>
</:tabs>
<:toolbarFilters>
{{#unless this.emptyState}}
@ -132,7 +132,7 @@
</ToolbarLink>
{{/if}}
</:toolbarActions>
</KvPageHeader>
</KvHeader>
{{#if (or this.isSecretDeleted (not this.emptyState))}}
<div class="info-table-row-header">

View file

@ -3,13 +3,13 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Create New Version">
<KvHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Create New Version">
<:toolbarFilters>
<Toggle @name="json" @checked={{this.showJsonView}} @onChange={{fn (mut this.showJsonView)}}>
<span class="has-text-grey">JSON</span>
</Toggle>
</:toolbarFilters>
</KvPageHeader>
</KvHeader>
{{#if this.showOldVersionAlert}}
<Hds::Alert data-test-secret-version-alert @type="inline" @color="warning" class="has-top-bottom-margin" as |A|>

View file

@ -3,8 +3,8 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @secretPath={{@path}}>
<:tabLinks>
<KvHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}} @secretPath={{@path}}>
<:tabs>
<li>
<LinkTo @route="secret.index" @models={{array @backend @path}} data-test-secrets-tab="Overview">Overview</LinkTo>
</li>
@ -30,7 +30,7 @@
>Version History</LinkTo>
</li>
{{/if}}
</:tabLinks>
</:tabs>
<:toolbarActions>
{{#if @capabilities.canDeleteMetadata}}
@ -42,7 +42,7 @@
</ToolbarLink>
{{/if}}
</:toolbarActions>
</KvPageHeader>
</KvHeader>
<h2 class="title is-5 has-bottom-padding-s has-top-margin-l">
Custom metadata

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Edit Secret Metadata" />
<KvHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Edit Secret Metadata" />
{{#if @capabilities.canUpdateMetadata}}
<hr class="is-marginless has-background-gray-200" />

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Version Diff">
<KvHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Version Diff">
<:toolbarFilters>
<span class="has-text-grey has-text-weight-semibold is-size-8">FROM:</span>
<KvVersionDropdown
@ -24,7 +24,7 @@
</div>
{{/if}}
</:toolbarFilters>
</KvPageHeader>
</KvHeader>
{{#if this.deactivatedState}}
<EmptyState

View file

@ -3,8 +3,8 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @secretPath={{@path}}>
<:tabLinks>
<KvHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}} @secretPath={{@path}}>
<:tabs>
<li>
<LinkTo @route="secret.index" @models={{array @backend @path}} data-test-secrets-tab="Overview">Overview</LinkTo>
</li>
@ -29,14 +29,14 @@
Version History
</LinkTo>
</li>
</:tabLinks>
</:tabs>
<:toolbarActions>
{{#if @capabilities.canReadMetadata}}
<ToolbarLink @route="secret.metadata.diff" @models={{array @backend @path}}>Version diff</ToolbarLink>
{{/if}}
</:toolbarActions>
</KvPageHeader>
</KvHeader>
{{#if @capabilities.canReadMetadata}}
<div class="sub-text has-text-weight-semibold is-flex-end has-short-padding">

View file

@ -3,8 +3,8 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @secretPath={{@path}}>
<:tabLinks>
<KvHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}} @secretPath={{@path}}>
<:tabs>
<li>
<LinkTo @route="secret.index" @models={{array @backend @path}} data-test-secrets-tab="Overview">Overview</LinkTo>
</li>
@ -30,10 +30,10 @@
>Version History</LinkTo>
</li>
{{/if}}
</:tabLinks>
</:tabs>
<:toolbarActions>
</:toolbarActions>
</KvPageHeader>
</KvHeader>
{{#if (or @metadata @subkeys.metadata)}}
<div class="flex row-wrap gap-24 has-top-margin-l">

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Patch Secret to New Version" />
<KvHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Patch Secret to New Version" />
{{#if this.controlGroupError}}
<ControlGroupInlineError @error={{this.controlGroupError}} class="has-top-margin-s has-bottom-margin-s">
<:customMessage>

View file

@ -3,8 +3,8 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @secretPath={{@path}}>
<:tabLinks>
<KvHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}} @secretPath={{@path}}>
<:tabs>
<li>
<LinkTo @route="secret.index" @models={{array @backend @path}} data-test-secrets-tab="Overview">Overview</LinkTo>
</li>
@ -36,7 +36,7 @@
</LinkTo>
</li>
{{/if}}
</:tabLinks>
</KvPageHeader>
</:tabs>
</KvHeader>
<KvPathsCard @backend={{@backend}} @path={{@path}} class="has-top-margin-xl" />

View file

@ -3,13 +3,13 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Create Secret">
<KvHeader @backend={{@backend}} @breadcrumbs={{@breadcrumbs}} @pageTitle="Create Secret">
<:toolbarFilters>
<Toggle @name="json" @checked={{this.showJsonView}} @onChange={{fn (mut this.showJsonView)}}>
<span class="has-text-grey">JSON</span>
</Toggle>
</:toolbarFilters>
</KvPageHeader>
</KvHeader>
<KvCreateEditForm
@form={{@form}}

View file

@ -27,7 +27,7 @@ export default class KvEngine extends Engine {
'secret-mount-path',
'version',
],
externalRoutes: ['secrets', 'syncDestination'],
externalRoutes: ['secrets', 'syncDestination', 'secretsGeneralSettingsConfiguration'],
};
}

View file

@ -25,4 +25,5 @@ export default buildRoutes(function () {
});
});
this.route('configuration');
this.route('configure');
});

View file

@ -11,40 +11,21 @@ export default class KvConfigurationRoute extends Route {
async model() {
const backend = this.modelFor('application');
const {
type,
path,
accessor,
running_plugin_version,
local,
seal_wrap,
config: { default_lease_ttl, max_lease_ttl },
options: { version },
} = await this.api.sys.internalUiReadMountInformation(backend.id);
// display mount config if engine config request fails
const engineConfig = await this.api.secrets.kvV2ReadConfiguration(backend.id).catch(() => {});
return {
...engineConfig,
type,
path,
accessor,
running_plugin_version,
local,
seal_wrap,
default_lease_ttl,
max_lease_ttl,
version,
};
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const { id } = this.modelFor('application');
controller.backend = id;
const backend = this.modelFor('application');
controller.backend = backend;
controller.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: id, route: 'list', model: id },
{ label: backend.id, route: 'list', model: backend.id },
{ label: 'Configuration' },
];
}

View file

@ -0,0 +1,35 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Route from '@ember/routing/route';
import KvForm from 'vault/forms/secrets/kv';
import { service } from '@ember/service';
export default class KvConfigureRoute extends Route {
@service api;
@service('app-router') router;
async model() {
const backend = this.modelFor('application');
const engineConfig = await this.api.secrets.kvV2ReadConfiguration(backend.id).catch(() => {});
const { max_versions, cas_required, delete_version_after } = engineConfig;
return {
form: new KvForm({ path: backend.id, max_versions, cas_required, delete_version_after }),
};
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const backend = this.modelFor('application');
controller.backend = backend;
controller.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: backend.id, route: 'list', model: backend.id },
{ label: 'Configuration', route: 'configuration', model: backend },
{ label: 'Edit' },
];
}
}

View file

@ -58,8 +58,10 @@ export default class KvSecretsListRoute extends Route {
const filterValue = pathToSecret ? (pageFilter ? pathToSecret + pageFilter : pathToSecret) : pageFilter;
const secrets = await this.fetchMetadata(backend, pathToSecret, params);
const capabilities = await this.capabilities.for('kvMetadata', { backend, path: path_to_secret });
const backendModel = this.modelFor('application');
return {
backendModel,
secrets,
backend,
pathToSecret,

View file

@ -0,0 +1,6 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Configure @form={{this.model.form}} @backend={{this.backend}} @breadcrumbs={{this.breadcrumbs}} />

View file

@ -3,15 +3,20 @@
SPDX-License-Identifier: BUSL-1.1
}}
<KvPageHeader @breadcrumbs={{this.breadcrumbs}} @mountName={{this.mountName}}>
<:tabLinks>
<KvHeader
@backend={{this.backend}}
@pageTitle={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
@mountName={{this.mountName}}
>
<:tabs>
<li><LinkTo @route="list" @model={{this.mountName}} data-test-secrets-tab="Secrets">Secrets</LinkTo></li>
<li><LinkTo
@route="configuration"
@model={{this.mountName}}
data-test-secrets-tab="Configuration"
>Configuration</LinkTo></li>
</:tabLinks>
</KvPageHeader>
</:tabs>
</KvHeader>
<Page::Error @error={{this.model}} />

View file

@ -6,6 +6,7 @@
<Page::List
@secrets={{this.model.secrets}}
@backend={{this.model.backend}}
@backendModel={{this.model.backendModel}}
@pathToSecret={{this.model.pathToSecret}}
@filterValue={{this.model.filterValue}}
@failedDirectoryQuery={{this.model.failedDirectoryQuery}}

View file

@ -6,6 +6,7 @@
<Page::List
@secrets={{this.model.secrets}}
@backend={{this.model.backend}}
@backendModel={{this.model.backendModel}}
@pathToSecret={{this.model.pathToSecret}}
@filterValue={{this.model.filterValue}}
@failedDirectoryQuery={{this.model.failedDirectoryQuery}}

View file

@ -11,7 +11,8 @@
"ember-concurrency": "*",
"@ember/test-waiters": "*",
"ember-inflector": "*",
"ember-template-lint": "*"
"ember-template-lint": "*",
"ember-cli-typescript": "*"
},
"ember-addon": {
"paths": [

View file

@ -85,7 +85,6 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) {
});
test('it can navigate to secrets within a secret directory', async function (assert) {
assert.expect(23);
const backend = this.backend;
const [root, subdirectory, secret] = this.fullSecretPath.split('/');
@ -115,14 +114,16 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) {
);
// Title correct
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(GENERAL.hdsPageHeaderTitle).hasText(`${backend}`);
// Tabs correct
assert.dom(PAGE.secretTab('Secrets')).hasText('Secrets');
assert.dom(PAGE.secretTab('Secrets')).hasClass('active');
assert.dom(PAGE.secretTab('Configuration')).hasText('Configuration');
assert.dom(PAGE.secretTab('Configuration')).doesNotHaveClass('active');
assert.dom(GENERAL.tab('Secrets')).hasText('Secrets');
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
assert.dom(GENERAL.menuItem('Configure')).exists('renders configure option');
// Create button correct
assert.dom(GENERAL.button('create secret')).exists('renders create secret button');
// Toolbar correct
assert.dom(PAGE.toolbarAction).exists({ count: 1 }, 'toolbar only renders create secret action');
assert.dom(PAGE.list.filter).hasValue(`${root}/`);
// List content correct
assert.dom(GENERAL.listItem(`${subdirectory}/`)).exists('renders linked block for subdirectory');
@ -134,7 +135,7 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) {
.hasText(`Current version The current version of this secret. 1`);
// Secret details visible
await click(PAGE.secretTab('Secret'));
assert.dom(PAGE.title).hasText(this.fullSecretPath);
assert.dom(GENERAL.hdsPageHeaderTitle).hasText(this.fullSecretPath);
assert.dom(PAGE.secretTab('Secret')).hasText('Secret');
assert.dom(PAGE.secretTab('Secret')).hasClass('active');
assert.dom(PAGE.secretTab('Metadata')).hasText('Metadata');

View file

@ -243,7 +243,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
return login(token);
});
test('empty backend - breadcrumbs, title, tabs, emptyState (a)', async function (assert) {
assert.expect(23);
assert.expect(19);
const backend = this.emptyBackend;
await navToBackend(backend);
@ -254,20 +254,18 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
'lands on secrets list page'
);
// CONFIGURATION TAB
await click(PAGE.secretTab('Configuration'));
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
await click(GENERAL.menuItem('Configure'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'Configuration']);
assert.dom(PAGE.secretTab('Configuration')).hasClass('active');
assert.dom(PAGE.secretTab('Configuration')).hasText('Configuration');
assert.dom(PAGE.secretTab('Secrets')).hasText('Secrets');
assert.dom(PAGE.secretTab('Secrets')).doesNotHaveClass('active');
assert.dom(GENERAL.tabLink('plugin-settings')).hasClass('active');
// SECRETS TAB
await click(PAGE.secretTab('Secrets'));
await visit(`/vault/secrets-engines/${backend}/kv/list`);
await click(GENERAL.tab('Secrets'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.secretTab('Secrets')).hasText('Secrets');
assert.dom(PAGE.secretTab('Secrets')).hasClass('active');
assert.dom(PAGE.secretTab('Configuration')).hasText('Configuration');
assert.dom(PAGE.secretTab('Configuration')).doesNotHaveClass('active');
assert.dom(PAGE.title).hasText(backend);
assert.dom(GENERAL.tab('Secrets')).hasText('Secrets');
assert.dom(GENERAL.tab('Secrets')).hasClass('active');
// Toolbar correct
assert.dom(PAGE.toolbar).exists({ count: 1 }, 'toolbar renders');
assert.dom(PAGE.list.filter).doesNotExist('List filter does not show because no secrets exists.');
@ -295,7 +293,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.expect(count);
const backend = this.backend;
await navToBackend(backend);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title text correct');
assert.dom(PAGE.title).hasText(backend, 'title text correct');
assert.dom(PAGE.emptyStateTitle).doesNotExist('No empty state');
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.list.filter).hasNoValue('List filter input is empty');
@ -308,7 +306,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
`navigated to ${currentURL()}`
);
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'app']);
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
assert.dom(PAGE.list.filter).hasValue('app/', 'List filter input is prefilled');
assert.dom(PAGE.list.item('nested/')).exists('Shows nested secret');
@ -319,7 +317,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
`navigated to ${currentURL()}`
);
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'app', 'nested']);
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
assert.dom(PAGE.list.filter).hasValue('app/nested/', 'List filter input is prefilled');
assert.dom(PAGE.list.item('secret')).exists('Shows deeply nested secret');
@ -618,12 +616,14 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
// Breadcrumbs correct
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
// Title correct
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
// Tabs correct
assert.dom(PAGE.secretTab('Secrets')).hasText('Secrets');
assert.dom(PAGE.secretTab('Secrets')).hasClass('active');
assert.dom(PAGE.secretTab('Configuration')).hasText('Configuration');
assert.dom(PAGE.secretTab('Configuration')).doesNotHaveClass('active');
assert.dom(GENERAL.tab('Secrets')).hasText('Secrets');
assert.dom(GENERAL.tab('Secrets')).hasClass('active');
// Dropdown correct
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
assert.dom(GENERAL.menuItem('Configure')).exists('renders configure option');
// Toolbar correct
assert.dom(PAGE.toolbar).exists({ count: 1 }, 'toolbar renders');
assert
@ -662,7 +662,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.expect(23);
const backend = this.backend;
await navToBackend(backend);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title text correct');
assert.dom(PAGE.title).hasText(backend, 'title text correct');
assert.dom(PAGE.emptyStateTitle).doesNotExist('No empty state');
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert
@ -757,16 +757,18 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.dom(PAGE.metadata.editBtn).doesNotExist('edit button hidden');
});
test('breadcrumbs & page titles are correct (dr)', async function (assert) {
assert.expect(35);
assert.expect(36);
const backend = this.backend;
await navToBackend(backend);
await click(PAGE.secretTab('Configuration'));
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
await click(GENERAL.menuItem('Configure'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'Configuration']);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title correct on config page');
assert.dom(PAGE.title).hasText(`${backend} configuration`, 'title correct on config page');
await click(PAGE.secretTab('Secrets'));
await visit(`/vault/secrets-engines/${backend}/kv/list`);
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title correct on secrets list');
assert.dom(PAGE.title).hasText(backend, 'title correct on secrets list');
await typeIn(PAGE.list.overviewInput, 'app/nested/secret');
await click(GENERAL.submitButton);
@ -816,12 +818,14 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
// Breadcrumbs correct
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
// Title correct
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
// Tabs correct
assert.dom(PAGE.secretTab('Secrets')).hasText('Secrets');
assert.dom(PAGE.secretTab('Secrets')).hasClass('active');
assert.dom(PAGE.secretTab('Configuration')).hasText('Configuration');
assert.dom(PAGE.secretTab('Configuration')).doesNotHaveClass('active');
assert.dom(GENERAL.tab('Secrets')).hasText('Secrets');
assert.dom(GENERAL.tab('Secrets')).hasClass('active');
// Dropdown correct
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
assert.dom(GENERAL.menuItem('Configure')).exists('renders configure option');
// Toolbar correct
assert.dom(PAGE.toolbar).exists({ count: 1 }, 'toolbar renders');
assert.dom(PAGE.list.filter).doesNotExist('List filter does not show because no secrets exists.');
@ -847,7 +851,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.expect(32);
const backend = this.backend;
await navToBackend(backend);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title text correct');
assert.dom(PAGE.title).hasText(backend, 'title text correct');
assert.dom(PAGE.emptyStateTitle).doesNotExist('No empty state');
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.list.filter).hasNoValue('List filter input is empty');
@ -860,7 +864,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
`navigated to ${currentURL()}`
);
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'app']);
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
assert.dom(PAGE.list.filter).doesNotExist('List filter hidden since no nested list access');
assert
@ -956,17 +960,20 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.dom(PAGE.metadata.editBtn).doesNotExist('edit button hidden');
});
test('breadcrumbs & page titles are correct (dlr)', async function (assert) {
assert.expect(29);
assert.expect(30);
const backend = this.backend;
await navToBackend(backend);
await click(PAGE.secretTab('Configuration'));
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
await click(GENERAL.menuItem('Configure'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'Configuration']);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'correct page title for configuration');
assert.dom(PAGE.title).hasText(`${backend} configuration`, 'correct page title for configuration');
await click(PAGE.secretTab('Secrets'));
await visit(`/vault/secrets-engines/${backend}/kv/list`);
await click(GENERAL.tab('Secrets'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'correct page title for secret list');
assert.dom(PAGE.title).hasText(backend, 'correct page title for secret list');
await click(PAGE.list.item(secretPath));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath]);
@ -1013,12 +1020,14 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
// Breadcrumbs correct
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
// Title correct
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
// Tabs correct
assert.dom(PAGE.secretTab('Secrets')).hasText('Secrets');
assert.dom(PAGE.secretTab('Secrets')).hasClass('active');
assert.dom(PAGE.secretTab('Configuration')).hasText('Configuration');
assert.dom(PAGE.secretTab('Configuration')).doesNotHaveClass('active');
assert.dom(GENERAL.tab('Secrets')).hasText('Secrets');
assert.dom(GENERAL.tab('Secrets')).hasClass('active');
// Dropdown correct
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
assert.dom(GENERAL.menuItem('Configure')).exists('renders configure option');
// Toolbar correct
assert.dom(PAGE.toolbar).exists({ count: 1 }, 'toolbar only renders create secret action');
assert.dom(PAGE.list.filter).doesNotExist('List filter does not show because no secrets exists.');
@ -1044,7 +1053,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.expect(42);
const backend = this.backend;
await navToBackend(backend);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title text correct');
assert.dom(PAGE.title).hasText(backend, 'title text correct');
assert.dom(PAGE.emptyStateTitle).doesNotExist('No empty state');
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.list.filter).hasNoValue('List filter input is empty');
@ -1057,7 +1066,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
`navigated to ${currentURL()}`
);
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'app']);
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
assert.dom(PAGE.list.filter).hasValue('app/', 'List filter input is prefilled');
assert.dom(PAGE.list.item('nested/')).exists('Shows nested secret');
@ -1068,7 +1077,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
`navigated to ${currentURL()}`
);
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'app', 'nested']);
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
assert.dom(PAGE.list.filter).hasValue('app/nested/', 'List filter input is prefilled');
assert.dom(PAGE.list.item('secret')).exists('Shows deeply nested secret');
@ -1182,16 +1191,19 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
);
});
test('breadcrumbs & page titles are correct (mm)', async function (assert) {
assert.expect(39);
assert.expect(40);
const backend = this.backend;
await navToBackend(backend);
await click(PAGE.secretTab('Configuration'));
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
await click(GENERAL.menuItem('Configure'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'Configuration']);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'correct page title for configuration');
assert.dom(PAGE.title).hasText(`${backend} configuration`, 'correct page title for configuration');
await click(PAGE.secretTab('Secrets'));
await visit(`/vault/secrets-engines/${backend}/kv/list`);
await click(GENERAL.tab('Secrets'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'correct page title for secret list');
assert.dom(PAGE.title).hasText(backend, 'correct page title for secret list');
await click(PAGE.list.item(secretPath));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath]);
@ -1242,12 +1254,14 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
// Breadcrumbs correct
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
// Title correct
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
// Tabs correct
assert.dom(PAGE.secretTab('Secrets')).hasText('Secrets');
assert.dom(PAGE.secretTab('Secrets')).hasClass('active');
assert.dom(PAGE.secretTab('Configuration')).hasText('Configuration');
assert.dom(PAGE.secretTab('Configuration')).doesNotHaveClass('active');
assert.dom(GENERAL.tab('Secrets')).hasText('Secrets');
assert.dom(GENERAL.tab('Secrets')).hasClass('active');
// Dropdown correct
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
assert.dom(GENERAL.menuItem('Configure')).exists('renders configure option');
// Toolbar correct
assert.dom(PAGE.toolbar).exists({ count: 1 }, 'toolbar only renders create secret action');
assert.dom(PAGE.list.filter).doesNotExist('List filter input is not rendered');
@ -1275,7 +1289,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.expect(24);
const backend = this.backend;
await navToBackend(backend);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title text correct');
assert.dom(PAGE.title).hasText(backend, 'title text correct');
assert.dom(PAGE.emptyStateTitle).doesNotExist('No empty state');
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.list.filter).doesNotExist('List filter input is not rendered');
@ -1398,16 +1412,19 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.dom(PAGE.metadata.editBtn).doesNotExist('edit metadata button does not render');
});
test('breadcrumbs & page titles are correct (sc)', async function (assert) {
assert.expect(39);
assert.expect(40);
const backend = this.backend;
await navToBackend(backend);
await click(PAGE.secretTab('Configuration'));
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
await click(GENERAL.menuItem('Configure'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'Configuration']);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'correct page title for configuration');
assert.dom(PAGE.title).hasText(`${backend} configuration`, 'correct page title for configuration');
await click(PAGE.secretTab('Secrets'));
await visit(`/vault/secrets-engines/${backend}/kv/list`);
await click(GENERAL.tab('Secrets'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'correct page title for secret list');
assert.dom(PAGE.title).hasText(backend, 'correct page title for secret list');
await typeIn(PAGE.list.overviewInput, secretPath);
await click(GENERAL.submitButton);
@ -1473,7 +1490,7 @@ path "${this.backend}/subkeys/*" {
assert.expect(44);
const backend = this.backend;
await navToBackend(backend);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title text correct');
assert.dom(PAGE.title).hasText(backend, 'title text correct');
assert.dom(PAGE.emptyStateTitle).doesNotExist('No empty state');
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.list.filter).hasNoValue('List filter input is empty');
@ -1486,7 +1503,7 @@ path "${this.backend}/subkeys/*" {
`navigated to ${currentURL()}`
);
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'app']);
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
assert.dom(PAGE.list.filter).hasValue('app/', 'List filter input is prefilled');
assert.dom(PAGE.list.item('nested/')).exists('Shows nested secret');
@ -1497,7 +1514,7 @@ path "${this.backend}/subkeys/*" {
`navigated to ${currentURL()}`
);
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'app', 'nested']);
assert.dom(PAGE.title).hasText(`${backend} version 2`);
assert.dom(PAGE.title).hasText(backend);
assert.dom(PAGE.list.filter).hasValue('app/nested/', 'List filter input is prefilled');
assert.dom(PAGE.list.item('secret')).exists('Shows deeply nested secret');
@ -1555,16 +1572,20 @@ path "${this.backend}/subkeys/*" {
);
});
test('breadcrumbs & page titles are correct (cg)', async function (assert) {
assert.expect(42);
assert.expect(43);
const backend = this.backend;
await navToBackend(backend);
await click(PAGE.secretTab('Configuration'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'Configuration']);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'correct page title for configuration');
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
await click(GENERAL.menuItem('Configure'));
await click(PAGE.secretTab('Secrets'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'Configuration']);
assert.dom(PAGE.title).hasText(`${backend} configuration`, 'correct page title for configuration');
await visit(`/vault/secrets-engines/${backend}/kv/list`);
await click(GENERAL.tab('Secrets'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend]);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'correct page title for secret list');
assert.dom(PAGE.title).hasText(backend, 'correct page title for secret list');
await visit(`/vault/secrets-engines/${backend}/kv/${secretPathUrlEncoded}/details`);

View file

@ -58,7 +58,9 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) {
await click(GENERAL.submitButton);
await click(PAGE.secretTab('Configuration'));
await click(GENERAL.dropdownToggle('Manage'));
await click(GENERAL.menuItem('Configure'));
await click(GENERAL.tabLink('plugin-settings'));
assert
.dom(PAGE.infoRowValue('Maximum number of versions'))

View file

@ -22,7 +22,6 @@ import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
import { create } from 'ember-cli-page-object';
import page from 'vault/tests/pages/settings/mount-secret-backend';
import configPage from 'vault/tests/pages/secrets/backend/configuration';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
@ -69,10 +68,17 @@ module('Acceptance | secrets-engines/enable', function (hooks) {
await page.maxTTLUnit('h').maxTTLVal(maxTTLHours);
await click(GENERAL.submitButton);
// TODO: Update this when KVV2 is migrated to new configuration flow
await click('[data-test-secrets-tab="Configuration"]');
assert.strictEqual(configPage.defaultTTL, `${this.calcDays(defaultTTLHours)}`, 'shows the proper TTL');
assert.strictEqual(configPage.maxTTL, `${this.calcDays(maxTTLHours)}`, 'shows the proper max TTL');
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
await click(GENERAL.menuItem('Configure'));
await click(GENERAL.tabLink('general-settings'));
assert
.dom(GENERAL.inputByAttr('default_lease_ttl'))
.hasValue(`${defaultTTLHours}`, 'shows the proper TTL');
assert.dom(GENERAL.selectByAttr('default_lease_ttl')).hasValue('h', 'shows the proper TTL unit');
assert.dom(GENERAL.inputByAttr('max_lease_ttl')).hasValue(`${maxTTLHours}`, 'shows the proper max TTL');
assert.dom(GENERAL.selectByAttr('max_lease_ttl')).hasValue('h', 'shows the proper max TTL unit');
});
test('it sets the ttl when enabled then disabled', async function (assert) {
@ -91,10 +97,14 @@ module('Acceptance | secrets-engines/enable', function (hooks) {
await page.maxTTLUnit('h').maxTTLVal(maxTTLHours);
await click(GENERAL.submitButton);
// TODO: Update this when KVV2 is migrated to new configuration flow
await click('[data-test-secrets-tab="Configuration"]');
assert.strictEqual(configPage.defaultTTL, '1 month 1 day', 'shows system default TTL');
assert.strictEqual(configPage.maxTTL, `${this.calcDays(maxTTLHours)}`, 'shows the proper max TTL');
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
await click(GENERAL.menuItem('Configure'));
await click(GENERAL.tabLink('general-settings'));
assert.dom(GENERAL.inputByAttr('default_lease_ttl')).hasValue('32', 'shows system default TTL');
assert.dom(GENERAL.inputByAttr('max_lease_ttl')).hasValue(`${maxTTLHours}`, 'shows the proper max TTL');
assert.dom(GENERAL.selectByAttr('max_lease_ttl')).hasValue('h', 'shows the proper max TTL unit');
});
test('it sets the max ttl after pki chosen, resets after', async function (assert) {

View file

@ -5,7 +5,7 @@
export const PAGE = {
// General selectors that are common between pages
title: '[data-test-header-title]',
title: '.hds-page-header__title',
breadcrumbs: '[data-test-breadcrumbs]',
breadcrumb: '[data-test-breadcrumbs] li',
breadcrumbAtIdx: (idx) => `[data-test-breadcrumbs] li:nth-child(${idx + 1}) a`,
@ -56,7 +56,7 @@ export const PAGE = {
toggleDiffDescription: '[data-test-diff-description]',
},
list: {
createSecret: '[data-test-toolbar-create-secret]',
createSecret: '[data-test-button="create secret"]',
item: (secret) => (!secret ? '[data-test-list-item]' : `[data-test-list-item="${secret}"]`),
menuItem: (label) => `[data-test-list-menu-item="${label}"]`,
filter: `[data-test-kv-list-filter]`,

View file

@ -8,8 +8,9 @@ import { setupRenderingTest } from 'vault/tests/helpers';
import { setupEngine } from 'ember-engines/test-support';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Integration | Component | kv | kv-page-header', function (hooks) {
module('Integration | Component | kv | kv-header', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'kv');
@ -27,16 +28,20 @@ module('Integration | Component | kv | kv-page-header', function (hooks) {
this.renderComponent = () =>
render(
hbs`
<KvPageHeader
<KvHeader
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
@pageTitle={{this.pageTitle}}
@mountName={{this.mountName}}
@secretPath={{this.secretPath}}
>
<:tabLinks>
<:tabs>
<li><LinkTo @route="list" data-test-secrets-tab="Secrets">Secrets</LinkTo></li>
<li><LinkTo @route="configuration" data-test-secrets-tab="Configuration">Configuration</LinkTo></li>
</:tabLinks>
</:tabs>
<:badges>
<Hds::Badge @text="version 2" data-test-badge />
</:badges>
<:toolbarActions>
<ToolbarLink @route="secrets.create" @type="add">Create secret</ToolbarLink>
@ -45,7 +50,7 @@ module('Integration | Component | kv | kv-page-header', function (hooks) {
<:toolbarFilters>
<p>stuff here</p>
</:toolbarFilters>
</KvPageHeader>
</KvHeader>
`,
{ owner: this.engine }
);
@ -66,38 +71,38 @@ module('Integration | Component | kv | kv-page-header', function (hooks) {
assert.expect(2);
this.pageTitle = 'Create new version';
await this.renderComponent();
assert.dom('[data-test-header-title]').hasText('Create new version', 'displays custom title.');
assert.dom('[data-test-header-title] svg').doesNotExist('Does not show icon if not at engine level.');
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Create new version', 'displays custom title.');
assert.dom(GENERAL.icon('key-values')).doesNotExist('Does not show icon if not at engine level.');
});
test('it renders a title and copy button for @secretPath', async function (assert) {
assert.expect(3);
this.secretPath = 'my/secret/path';
this.pageTitle = this.secretPath;
await this.renderComponent();
assert.dom('[data-test-header-title]').hasText('my/secret/path', 'displays path');
assert.dom('[data-test-header-title] button').exists('renders copy button for path');
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('my/secret/path', 'displays path');
assert.dom(GENERAL.copyButton).exists('renders copy button for path');
assert.dom('[data-test-icon="clipboard-copy"]').exists('renders copy icon');
});
test('it renders a title, icon and tag if engine view', async function (assert) {
assert.expect(2);
this.mountName = this.backend;
assert.expect(3);
this.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: this.backend, route: 'secrets' },
];
this.backend = { id: this.backend, icon: 'key-values' };
this.pageTitle = this.backend.id;
await this.renderComponent();
assert
.dom('[data-test-header-title]')
.hasText(`${this.backend} version 2`, 'Mount path and version tag render for title.');
assert
.dom('[data-test-header-title] [data-test-icon="key-values"]')
.exists('An icon renders next to title.');
assert.dom(GENERAL.hdsPageHeaderTitle).hasText(`${this.pageTitle}`, 'Mount path renders for title.');
assert.dom(GENERAL.badge()).hasText('version 2', 'version badge renders in header.');
assert.dom(GENERAL.icon('key-values')).exists('An icon renders next to title.');
});
test('it renders tabs', async function (assert) {
assert.expect(2);
assert.expect(1);
await this.renderComponent();
assert.dom('[data-test-secrets-tab="Secrets"]').hasText('Secrets', 'Secrets tab renders');
assert
.dom('[data-test-secrets-tab="Configuration"]')
.hasText('Configuration', 'Configuration tab renders');
});
test('it should yield block for toolbar actions', async function (assert) {

View file

@ -6,9 +6,10 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { setupEngine } from 'ember-engines/test-support';
import { render } from '@ember/test-helpers';
import { render, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Integration | Component | kv-v2 | Page::Configuration', function (hooks) {
setupRenderingTest(hooks);
@ -29,7 +30,29 @@ module('Integration | Component | kv-v2 | Page::Configuration', function (hooks)
max_lease_ttl: '123h',
version: '2',
};
this.backend = 'my-kv';
this.backend = {
accessor: 'kv_05319fa9',
config: {
default_lease_ttl: 2764800,
force_no_cache: false,
listing_visibility: 'hidden',
max_lease_ttl: 2764800,
},
description: '',
external_entropy_access: false,
local: false,
options: {
version: '2',
},
path: 'my-kv/',
plugin_version: '',
running_plugin_version: 'v0.25.0+builtin',
running_sha256: '',
seal_wrap: false,
type: 'kv',
uuid: '0cd6346f-c93a-ecfa-b01d-6b690a745c8e',
id: 'my-kv',
};
this.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: 'my-kv', route: 'list' },
@ -38,7 +61,7 @@ module('Integration | Component | kv-v2 | Page::Configuration', function (hooks)
});
test('it renders kv configuration details', async function (assert) {
assert.expect(15);
assert.expect(6);
await render(
hbs`
@ -51,21 +74,13 @@ module('Integration | Component | kv-v2 | Page::Configuration', function (hooks)
{ owner: this.engine }
);
assert.dom(PAGE.title).includesText('my-kv', 'renders engine path as page title');
assert.dom(PAGE.secretTab('Secrets')).exists('renders Secrets tab');
assert.dom(PAGE.secretTab('Configuration')).exists('renders Configuration tab');
assert.dom(PAGE.title).includesText('my-kv configuration', 'renders engine path as page title');
assert.dom(GENERAL.tab('general-settings')).exists('renders general settings tab');
assert.dom(GENERAL.tab('plugin-settings')).exists('renders kv settings tab');
await click(GENERAL.tab('plugin-settings'));
assert.dom(PAGE.infoRowValue('Require check and set')).hasText('Yes');
assert.dom(PAGE.infoRowValue('Automate secret deletion')).hasText('Never delete');
assert.dom(PAGE.infoRowValue('Maximum number of versions')).hasText('0');
assert.dom(PAGE.infoRowValue('Type')).hasText('kv');
assert.dom(PAGE.infoRowValue('Path')).hasText('my-kv');
assert.dom(PAGE.infoRowValue('Accessor')).hasText('kv_80616825');
assert.dom(PAGE.infoRowValue('Running plugin version')).hasText('2.7.0');
assert.dom(PAGE.infoRowValue('Local')).hasText('No');
assert.dom(PAGE.infoRowValue('Seal wrap')).hasText('No');
assert.dom(PAGE.infoRowValue('Default Lease TTL')).hasText('3 days');
assert.dom(PAGE.infoRowValue('Max Lease TTL')).hasText('5 days 3 hours');
assert.dom(PAGE.infoRowValue('Version')).hasText('2');
});
});

View file

@ -0,0 +1,97 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { setupEngine } from 'ember-engines/test-support';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import KvForm from 'vault/forms/secrets/kv';
module('Integration | Component | kv-v2 | Page::Configure', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'kv');
hooks.beforeEach(async function () {
this.config = {
cas_required: true,
max_versions: 5,
delete_version_after: '10000s',
};
this.form = new KvForm({});
this.editForm = new KvForm(this.config);
this.backend = {
accessor: 'kv_05319fa9',
config: {
default_lease_ttl: 2764800,
force_no_cache: false,
listing_visibility: 'hidden',
max_lease_ttl: 2764800,
},
description: '',
external_entropy_access: false,
local: false,
options: {
version: '2',
},
path: 'my-kv/',
plugin_version: '',
running_plugin_version: 'v0.25.0+builtin',
running_sha256: '',
seal_wrap: false,
type: 'kv',
uuid: '0cd6346f-c93a-ecfa-b01d-6b690a745c8e',
id: 'my-kv',
};
this.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: 'my-kv', route: 'list' },
{ label: 'Configuration', route: 'configuration', model: this.backend },
{ label: 'Edit' },
];
});
test('it renders kv configure form', async function (assert) {
assert.expect(3);
await render(
hbs`
<Page::Configure
@form={{this.form}}
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
/>
`,
{ owner: this.engine }
);
assert.dom(GENERAL.inputByAttr('max_versions')).exists();
assert.dom(GENERAL.inputByAttr('cas_required')).exists();
assert.dom(GENERAL.toggleInput('Automate secret deletion')).exists();
});
test('it renders kv configure form with existing config', async function (assert) {
assert.expect(5);
this.form = this.editForm;
await render(
hbs`
<Page::Configure
@form={{this.form}}
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
/>
`,
{ owner: this.engine }
);
assert.dom(GENERAL.inputByAttr('max_versions')).hasValue('5');
assert.dom(GENERAL.inputByAttr('cas_required')).hasValue('on');
assert.dom(GENERAL.toggleInput('Automate secret deletion')).isChecked();
assert.dom(GENERAL.ttl.input('Automate secret deletion')).hasValue('10000');
assert.dom(GENERAL.selectByAttr('ttl-unit')).hasValue('s');
});
});

View file

@ -38,6 +38,29 @@ module('Integration | Component | kv-v2 | Page::List', function (hooks) {
{ label: this.backend, route: 'list' },
];
this.capabilities = { canRead: true, canDelete: true };
this.backendModel = {
accessor: 'kv_05319fa9',
config: {
default_lease_ttl: 2764800,
force_no_cache: false,
listing_visibility: 'hidden',
max_lease_ttl: 2764800,
},
description: '',
external_entropy_access: false,
local: false,
options: {
version: '2',
},
path: 'kv-engine/',
plugin_version: '',
running_plugin_version: 'v0.25.0+builtin',
running_sha256: '',
seal_wrap: false,
type: 'kv',
uuid: '0cd6346f-c93a-ecfa-b01d-6b690a745c8e',
id: 'kv-engine',
};
this.renderComponent = () =>
render(
@ -45,6 +68,7 @@ module('Integration | Component | kv-v2 | Page::List', function (hooks) {
<Page::List
@secrets={{this.secrets}}
@backend={{this.backend}}
@backendModel={{this.backendModel}}
@pathToSecret={{this.pathToSecret}}
@filterValue={{this.filterValue}}
@failedDirectoryQuery={{this.failedDirectoryQuery}}
@ -69,8 +93,10 @@ module('Integration | Component | kv-v2 | Page::List', function (hooks) {
await this.renderComponent();
assert.dom(PAGE.title).includesText(this.backend, 'renders mount path as page title');
assert.dom(PAGE.secretTab('Secrets')).exists('renders Secrets tab');
assert.dom(PAGE.secretTab('Configuration')).exists('renders Configuration tab');
assert.dom(GENERAL.tab('Secrets')).exists('renders Secrets tab');
assert.dom(GENERAL.dropdownToggle('Manage')).exists('renders manage dropdown');
await click(GENERAL.dropdownToggle('Manage'));
assert.dom(GENERAL.menuItem('Configure')).exists('renders configure option');
assert.dom(PAGE.list.filter).exists('renders filter input');
assert.dom(PAGE.list.createSecret).exists('renders create secret action');
});