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(); - }); - } - } -});