diff --git a/ui/app/components/secret-engine/configure-tabs.hbs b/ui/app/components/secret-engine/configure-tabs.hbs
deleted file mode 100644
index 41f552b5ca..0000000000
--- a/ui/app/components/secret-engine/configure-tabs.hbs
+++ /dev/null
@@ -1,23 +0,0 @@
-{{!
- Copyright IBM Corp. 2016, 2025
- SPDX-License-Identifier: BUSL-1.1
-}}
-
-
\ No newline at end of file
diff --git a/ui/app/components/secret-engine/configure-tabs.ts b/ui/app/components/secret-engine/configure-tabs.ts
deleted file mode 100644
index e4e42a90be..0000000000
--- a/ui/app/components/secret-engine/configure-tabs.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Copyright (c) HashiCorp, Inc.
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-import Component from '@glimmer/component';
-
-import type { EngineDisplayData } from 'vault/utils/all-engines-metadata';
-
-/**
- * @module ConfigureTabs
- * These tabs render in the shared general-settings, plugin-settings and edit routes of the secret engines headers.
- *
- * @param {string} [configRoute] - only passed when rendering the vault.cluster.secrets.backend.configuration.edit route to highlight the tab for that view
- * @param {object} engineMetadata - engine specific metadata
- * @param {boolean} [isConfigured] - whether an engine has been configured. if configured, plugin settings exist
- * @param {object} path - the secret engine mount path, sometimes referred to as the engine "id" or "backend"
- */
-
-interface Args {
- configRoute?: string;
- engineMetadata: EngineDisplayData;
- isConfigured: boolean;
- path: string;
-}
-
-export default class ConfigureTabs extends Component {
- routePrefix = 'vault.cluster.secrets.backend.';
-
- // The plugin settings tab only renders if an engine is configurable.
- // `configEditRoute` and `configReadRoute` are defined for engines with custom route patterns, like Ember engines.
- // Otherwise navigates to default 'backend.configuration' routes.
- get pluginSettingsRoute() {
- const { engineMetadata, isConfigured } = this.args;
-
- // If the engine is configurable, but not configured, navigate to its edit route
- if (engineMetadata.isConfigurable && !isConfigured) {
- const route = engineMetadata.configEditRoute || 'configuration.edit';
- return this.routePrefix + route;
- }
-
- // For configured engines, determine route based on context:
- // - If @configRoute is passed (from edit.hbs) the user has navigated to edit and this ensures the tab is highlighted.
- // - Otherwise, route to the read view (`configReadRoute` or 'plugin-settings')
- const route = this.args.configRoute || engineMetadata?.configReadRoute || 'configuration.plugin-settings';
- return this.routePrefix + route;
- }
-}
diff --git a/ui/app/components/secret-engine/page/general-settings.hbs b/ui/app/components/secret-engine/page/general-settings.hbs
index e0f275602e..19cb130956 100644
--- a/ui/app/components/secret-engine/page/general-settings.hbs
+++ b/ui/app/components/secret-engine/page/general-settings.hbs
@@ -7,15 +7,17 @@
@title="{{@model.secretsEngine.id}} configuration"
@description={{engineDisplayData.displayName}}
@icon={{engineDisplayData.glyph}}
- @subtitle={{engineDisplayData.typeDisplay}}
>
<:breadcrumbs>
<:tabs>
-
diff --git a/ui/app/components/secret-engine/page/plugin-settings.hbs b/ui/app/components/secret-engine/page/plugin-settings.hbs
index 3283c13ab5..9bab071fe4 100644
--- a/ui/app/components/secret-engine/page/plugin-settings.hbs
+++ b/ui/app/components/secret-engine/page/plugin-settings.hbs
@@ -2,20 +2,20 @@
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
+
{{#let (engines-display-data @model.secretsEngine.type) as |engineDisplayData|}}
<:breadcrumbs>
<:tabs>
-
@@ -28,70 +28,62 @@
/>
-{{/let}}
-{{#if @model.config}}
-
-
-
- Edit configuration
-
-
-
+ {{#if @model.config}}
+
+
+
+ Edit configuration
+
+
+
- {{#each this.displayFields as |field|}}
- {{! public key while not sensitive when editing/creating, should be hidden by default on viewing }}
- {{#if (eq field "public_key")}}
-
-
-
- {{else}}
-
- {{/if}}
- {{/each}}
-{{else}}
- {{#if (get (engines-display-data @model.secretsEngine.type) "isConfigurable")}}
+ {{#each this.displayFields as |field|}}
+ {{! public key while not sensitive when editing/creating, should be hidden by default on viewing }}
+ {{#if (eq field "public_key")}}
+
+
+
+ {{else}}
+
+ {{/if}}
+ {{/each}}
+ {{else if engineDisplayData.isConfigurable}}
{{! Prompt user to configure the secret engine }}
{{else}}
{{/if}}
-{{/if}}
\ No newline at end of file
+{{/let}}
\ No newline at end of file
diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration.js b/ui/app/routes/vault/cluster/secrets/backend/configuration.js
index befaacd8cb..663bb01ca2 100644
--- a/ui/app/routes/vault/cluster/secrets/backend/configuration.js
+++ b/ui/app/routes/vault/cluster/secrets/backend/configuration.js
@@ -21,15 +21,23 @@ export default class SecretsBackendConfigurationRoute extends Route {
const { type, id } = secretsEngine;
return {
secretsEngine,
- config: await this.fetchConfig(type, id), // fetch config for configurable engines (aws, azure, gcp, ssh)
+ // fetch config for configurable secrets engines that use the "general" route pattern: aws, azure, gcp, ssh
+ // ember-engines manage their own engine config routes and requests so do not fetch here.
+ config: await this.fetchConfig(type, id),
};
}
+ // TODO after update to show separated general settings vs plugin settings redirect if not configured?
+ // afterModel(resolvedModel) {
+ // // Redirect to edit route if not configured
+ // if (!resolvedModel.config) {
+ // this.router.transitionTo('vault.cluster.secrets.backend.configuration.edit');
+ // }
+ // }
+
fetchConfig(type, id) {
// id is the path where the backend is mounted since there's only one config per engine (often this path is referred to just as backend)
switch (type) {
- case 'ldap':
- return this.fetchLdapConfigs(id);
case 'aws':
return this.fetchAwsConfigs(id);
case 'azure':
@@ -41,18 +49,6 @@ export default class SecretsBackendConfigurationRoute extends Route {
}
}
- async fetchLdapConfigs(path) {
- try {
- const { data } = await this.api.secrets.ldapReadConfiguration(path);
- return data;
- } catch (e) {
- const error = await this.parseApiError(e);
- if (error.httpStatus === 404) {
- return {};
- }
- }
- }
-
async fetchAwsConfigs(path) {
// AWS has two configuration endpoints root and lease, as well as a separate endpoint for the issuer.
const handleError = async (e) => {
diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration/index.js b/ui/app/routes/vault/cluster/secrets/backend/configuration/index.js
index 290c07b2ac..f9c7bf592f 100644
--- a/ui/app/routes/vault/cluster/secrets/backend/configuration/index.js
+++ b/ui/app/routes/vault/cluster/secrets/backend/configuration/index.js
@@ -20,12 +20,4 @@ export default class SecretsBackendConfigurationIndexRoute extends Route {
return this.router.replaceWith('vault.cluster.secrets.backend.configuration.general-settings');
}
}
-
- setupController(controller, resolvedModel) {
- super.setupController(controller, resolvedModel);
- const engine = engineDisplayData(resolvedModel.secretsEngine.type);
- controller.typeDisplay = engine.displayName;
- controller.isConfigurable = engine.isConfigurable ?? false;
- controller.modelId = resolvedModel.secretsEngine.id;
- }
}
diff --git a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs
index 90385ac469..150eb7d097 100644
--- a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs
+++ b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs
@@ -26,7 +26,6 @@
@title={{concat this.model.secretsEngine.id " configuration"}}
@description={{engineDisplayData.displayName}}
@icon={{engineDisplayData.glyph}}
- @subtitle={{engineDisplayData.typeDisplay}}
>
<:breadcrumbs>
@@ -40,10 +39,9 @@
/>
<:tabs>
-
diff --git a/ui/app/templates/vault/cluster/secrets/backend/configuration/index.hbs b/ui/app/templates/vault/cluster/secrets/backend/configuration/index.hbs
index b43313b7d6..2193a21493 100644
--- a/ui/app/templates/vault/cluster/secrets/backend/configuration/index.hbs
+++ b/ui/app/templates/vault/cluster/secrets/backend/configuration/index.hbs
@@ -5,36 +5,41 @@
-{{#if this.isConfigurable}}
-
-
-
- Configure
-
-
-
+{{#let (engines-display-data @model.secretsEngine.type) as |engineMetadata|}}
+ {{#if engineMetadata.isConfigurable}}
+
+
+
+ Configure
+
+
+
-
+
-
-
-{{else}}
-
- {{#each this.displayFields as |field|}}
-
- {{/each}}
-
-{{/if}}
\ No newline at end of file
+
+ {{else}}
+
+ {{#each this.displayFields as |field|}}
+
+ {{/each}}
+
+ {{/if}}
+{{/let}}
\ No newline at end of file
diff --git a/ui/app/utils/all-engines-metadata.ts b/ui/app/utils/all-engines-metadata.ts
index 89529088f8..0d665e7edb 100644
--- a/ui/app/utils/all-engines-metadata.ts
+++ b/ui/app/utils/all-engines-metadata.ts
@@ -35,8 +35,7 @@ export interface EngineDisplayData {
isOldEngine?: boolean; // flag for engine views, if set to true, the engine will show pre-existing page design, if not, then the new views will be used. This is temporary until all engines have been migrated to the new design.
type: string;
value?: string;
- configReadRoute?: string; // override for custom route if not "configuration.plugin-settings" (used for Ember engines)
- configEditRoute?: string; // override for custom route if not "configuration.edit" (used for Ember engines)
+ configRoute?: string; // override for custom route if not "configuration.plugin-settings" (used for Ember engines)
}
/**
@@ -215,8 +214,7 @@ export const ALL_ENGINES: EngineDisplayData[] = [
displayName: 'LDAP',
isConfigurable: true,
engineRoute: 'ldap.overview',
- configEditRoute: 'ldap.configure',
- configReadRoute: 'ldap.configuration',
+ configRoute: 'ldap.configuration',
glyph: 'folder-users',
mountCategory: ['auth', 'secret'],
type: 'ldap',
diff --git a/ui/lib/core/addon/components/mount/configure-tabs.hbs b/ui/lib/core/addon/components/mount/configure-tabs.hbs
new file mode 100644
index 0000000000..d4eba36cec
--- /dev/null
+++ b/ui/lib/core/addon/components/mount/configure-tabs.hbs
@@ -0,0 +1,35 @@
+{{!
+ Copyright IBM Corp. 2016, 2025
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
\ No newline at end of file
diff --git a/ui/lib/core/app/components/mount/configure-tabs.js b/ui/lib/core/app/components/mount/configure-tabs.js
new file mode 100644
index 0000000000..2e7b954232
--- /dev/null
+++ b/ui/lib/core/app/components/mount/configure-tabs.js
@@ -0,0 +1,6 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+export { default } from 'core/components/mount/configure-tabs';
diff --git a/ui/lib/ldap/addon/components/ldap-header.hbs b/ui/lib/ldap/addon/components/ldap-header.hbs
index 53f11b57e9..ce23be56aa 100644
--- a/ui/lib/ldap/addon/components/ldap-header.hbs
+++ b/ui/lib/ldap/addon/components/ldap-header.hbs
@@ -33,28 +33,22 @@
{{/if}}
<:tabs>
-
+ {{#if @configRoute}}
+
+ {{else}}
-
+ {{/if}}
diff --git a/ui/tests/acceptance/secrets/backend/ldap/overview-test.js b/ui/tests/acceptance/secrets/backend/ldap/overview-test.js
index 6a12cbc9d6..d0da8cd51a 100644
--- a/ui/tests/acceptance/secrets/backend/ldap/overview-test.js
+++ b/ui/tests/acceptance/secrets/backend/ldap/overview-test.js
@@ -31,6 +31,7 @@ module('Acceptance | ldap | overview', function (hooks) {
`write ${backend}/config binddn=foo bindpass=bar url=http://localhost:8208`,
]);
};
+ this.expectedConfigEditRoute = 'ldap.configure';
return login();
});
diff --git a/ui/tests/acceptance/secrets/secrets-nav-test-helper.js b/ui/tests/acceptance/secrets/secrets-nav-test-helper.js
index ae24969167..69e488cbf2 100644
--- a/ui/tests/acceptance/secrets/secrets-nav-test-helper.js
+++ b/ui/tests/acceptance/secrets/secrets-nav-test-helper.js
@@ -11,15 +11,14 @@ import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
// To use this helper for configurable engines
-// define `this.mountAndConfig` in the beforeEach hook
+// define `this.mountAndConfig` and this.expectedConfigEditRoute in the beforeEach hook
// (see "Acceptance | ldap | overview" as an example)
const BASE_ROUTE = 'vault.cluster.secrets.backend';
export default (test, type) => {
const {
isConfigurable = false,
- configReadRoute = 'configuration.plugin-settings',
- configEditRoute = 'configuration.edit',
+ configRoute = 'configuration.plugin-settings',
engineRoute = 'list-root',
} = engineDisplayData(type);
@@ -35,7 +34,7 @@ export default (test, type) => {
await click(GENERAL.menuItem('View configuration'));
assert.strictEqual(
currentRouteName(),
- `${BASE_ROUTE}.${configEditRoute}`,
+ `${BASE_ROUTE}.${this.expectedConfigEditRoute}`,
'it navigates to the configure route from the list view'
);
@@ -57,7 +56,7 @@ export default (test, type) => {
await click(GENERAL.tabLink('plugin-settings'));
assert.strictEqual(
currentRouteName(),
- `${BASE_ROUTE}.${configEditRoute}`,
+ `${BASE_ROUTE}.${this.expectedConfigEditRoute}`,
'clicking plugin settings navigates to edit route when not configured'
);
assert
@@ -79,7 +78,7 @@ export default (test, type) => {
await click(GENERAL.menuItem('View configuration'));
assert.strictEqual(
currentRouteName(),
- `${BASE_ROUTE}.${configReadRoute}`,
+ `${BASE_ROUTE}.${configRoute}`,
'it navigates to the configure route from the list view'
);
@@ -102,7 +101,7 @@ export default (test, type) => {
// Confirm tabs after clicking plugin-settings
assert.strictEqual(
currentRouteName(),
- `${BASE_ROUTE}.${configReadRoute}`,
+ `${BASE_ROUTE}.${configRoute}`,
'it navigates to the read route when configured'
);
assert
@@ -114,7 +113,7 @@ export default (test, type) => {
await click(SES.configure);
assert.strictEqual(
currentRouteName(),
- `${BASE_ROUTE}.${configEditRoute}`,
+ `${BASE_ROUTE}.${this.expectedConfigEditRoute}`,
'it navigates to the edit route'
);
assert
@@ -135,7 +134,7 @@ export default (test, type) => {
await click(GENERAL.tabLink('plugin-settings'));
assert.strictEqual(
currentRouteName(),
- `${BASE_ROUTE}.${configReadRoute}`,
+ `${BASE_ROUTE}.${configRoute}`,
'it navigates to the read route when configured'
);
await click(GENERAL.button('Exit configuration'));
diff --git a/ui/tests/integration/components/mount/configure-tabs-test.js b/ui/tests/integration/components/mount/configure-tabs-test.js
new file mode 100644
index 0000000000..9a00db8a97
--- /dev/null
+++ b/ui/tests/integration/components/mount/configure-tabs-test.js
@@ -0,0 +1,58 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { click, render } from '@ember/test-helpers';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | mount/configure-tabs', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.configRoute = 'pki.configuration.create';
+ // This component also accepts @path but that's route related and irrelevant to an integration test
+ this.renderComponent = () => {
+ return render(
+ hbs``
+ );
+ };
+ });
+
+ test('it renders when args are undefined', async function (assert) {
+ this.configRoute = undefined;
+ this.displayName = undefined;
+ await this.renderComponent();
+ assert.dom(GENERAL.tab('general-settings')).exists().hasText('General settings');
+ });
+
+ test('it renders plugin settings tab when @configRoute provided', async function (assert) {
+ this.displayName = 'PKI';
+ await this.renderComponent();
+
+ assert.dom(GENERAL.tab('general-settings')).exists().hasText('General settings');
+ await click(GENERAL.tab('plugin-settings'));
+ assert.dom(GENERAL.tab('plugin-settings')).exists().hasText('PKI settings');
+ });
+
+ test('it renders fallback when @displayName not provided', async function (assert) {
+ this.displayName = '';
+ await this.renderComponent();
+ assert.dom(GENERAL.tab('plugin-settings')).exists().hasText('Plugin settings');
+ });
+
+ test('it hides plugin settings when there is no @configRoute', async function (assert) {
+ this.configRoute = '';
+ await this.renderComponent();
+
+ assert.dom(GENERAL.tab('general-settings')).exists().hasText('General settings');
+ assert.dom(GENERAL.tab('plugin-settings')).doesNotExist();
+ });
+});
diff --git a/ui/tests/integration/components/secret-engines/configure-tabs-test.js b/ui/tests/integration/components/secret-engines/configure-tabs-test.js
deleted file mode 100644
index b3caf4f469..0000000000
--- a/ui/tests/integration/components/secret-engines/configure-tabs-test.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2025
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-import { module, test } from 'qunit';
-import { setupRenderingTest } from 'ember-qunit';
-import { render } from '@ember/test-helpers';
-import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
-import { GENERAL } from 'vault/tests/helpers/general-selectors';
-
-import hbs from 'htmlbars-inline-precompile';
-import engineDisplayData from 'vault/helpers/engines-display-data';
-
-// The `configurable` array is hardcoded to validate that ALL_ENGINES metadata is correctly
-// defined to render the tabs correctly.
-const configurable = ['aws', 'azure', 'gcp', 'ldap', 'ssh'];
-
-module('Integration | Component | secret-engines/configure-tabs', function (hooks) {
- setupRenderingTest(hooks);
-
- hooks.beforeEach(function () {
- this.isConfigured = undefined;
- // This component accepts more args, but they are route related and instead asserted by acceptance tests
- this.renderComponent = (type) => {
- this.engineMetadata = type ? engineDisplayData(type) : undefined;
- return render(
- hbs``
- );
- };
- });
-
- test('it renders when args are undefined', async function (assert) {
- await this.renderComponent();
- assert.dom(GENERAL.tab('general-settings')).exists().hasText('General settings');
- });
-
- for (const { type } of filterEnginesByMountCategory({ mountCategory: 'secret' })) {
- if (configurable.includes(type)) {
- test(`${type} (configurable): it renders expected tabs when not configured`, async function (assert) {
- await this.renderComponent(type);
- assert.dom(GENERAL.tab('general-settings')).exists().hasText('General settings');
- assert
- .dom(GENERAL.tab('plugin-settings'))
- .exists()
- .hasText(`${this.engineMetadata.displayName} settings`);
- });
-
- test(`${type} (configurable): it renders expected tabs when configured`, async function (assert) {
- this.isConfigured = true;
- await this.renderComponent(type);
-
- assert.dom(GENERAL.tab('general-settings')).exists().hasText('General settings');
- assert
- .dom(GENERAL.tab('plugin-settings'))
- .exists()
- .hasText(`${this.engineMetadata.displayName} settings`);
- });
- } else {
- // NON-CONFIGURABLE ENGINES
- test(`${type} it hides plugin settings when not configurable`, async function (assert) {
- await this.renderComponent(type);
-
- assert.dom(GENERAL.tab('general-settings')).exists().hasText('General settings');
- assert.dom(GENERAL.tab('plugin-settings')).doesNotExist();
- });
- }
- }
-});