diff --git a/ui/app/app.js b/ui/app/app.js index b4b201e196..983b47072c 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -75,6 +75,7 @@ export default class App extends Application { services: [{ 'app-router': 'router' }, 'secret-mount-path', 'flash-messages', 'api', 'capabilities'], externalRoutes: { secrets: 'vault.cluster.secrets.backends', + secretsGeneralSettingsConfiguration: 'vault.cluster.secrets.backend.configuration.general-settings', }, }, }, diff --git a/ui/lib/kubernetes/addon/components/kubernetes-header.hbs b/ui/lib/kubernetes/addon/components/kubernetes-header.hbs new file mode 100644 index 0000000000..28343771ac --- /dev/null +++ b/ui/lib/kubernetes/addon/components/kubernetes-header.hbs @@ -0,0 +1,61 @@ +{{! + Copyright IBM Corp. 2016, 2025 + SPDX-License-Identifier: BUSL-1.1 +}} + + + <:breadcrumbs> + + + <:actions> + + + Configure + Delete + + + + +{{#if @configRoute}} + +{{else}} + +{{/if}} + + + {{#if @filterRoles}} + + + + {{/if}} + + {{yield}} + + \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/components/kubernetes-header.ts b/ui/lib/kubernetes/addon/components/kubernetes-header.ts new file mode 100644 index 0000000000..a6769c4fce --- /dev/null +++ b/ui/lib/kubernetes/addon/components/kubernetes-header.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; + +import type SecretsEngineResource from 'vault/resources/secrets/engine'; +import type RouterService from '@ember/routing/router-service'; +import type FlashMessageService from 'vault/services/flash-messages'; +import type ApiService from 'vault/services/api'; + +/** + * @module KubernetesHeader handles the ldap page header. + * + * @example + * + * + * @param {object} secretsEngine - A model contains a ldap secret engine resource. + * @param {object} config - A model contains the configuration of the ldap secret engine. + */ + +interface Args { + secretsEngine: SecretsEngineResource; + config: Record; +} + +export default class KubernetesHeader extends Component { + @service('app-router') declare readonly router: RouterService; + @service declare readonly api: ApiService; + @service declare readonly flashMessages: FlashMessageService; + + @tracked engineToDisable: SecretsEngineResource | undefined = undefined; + + @task + *disableEngine(engine: SecretsEngineResource) { + 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; + } + } +} diff --git a/ui/lib/kubernetes/addon/components/page/configuration.hbs b/ui/lib/kubernetes/addon/components/page/configuration.hbs index c4ec701abd..3f4deb68a8 100644 --- a/ui/lib/kubernetes/addon/components/page/configuration.hbs +++ b/ui/lib/kubernetes/addon/components/page/configuration.hbs @@ -3,13 +3,14 @@ SPDX-License-Identifier: BUSL-1.1 }} - +{{! TODO: VAULT-40884 Add @configRoute argument with value "configuration" so mount config tabs show }} + {{#if @config}} Edit configuration {{/if}} - + {{#if @config}} {{#if @config.disable_local_ca_jwt}} diff --git a/ui/lib/kubernetes/addon/components/page/configure.hbs b/ui/lib/kubernetes/addon/components/page/configure.hbs index 8f2ac71123..79c64b5d34 100644 --- a/ui/lib/kubernetes/addon/components/page/configure.hbs +++ b/ui/lib/kubernetes/addon/components/page/configure.hbs @@ -3,16 +3,12 @@ SPDX-License-Identifier: BUSL-1.1 }} - - +{{! TODO: VAULT-40884 Update Page::Header to use Kubernetes::Header and pass @configRoute argument with value "configure" for mount config tabs to show}} + + <:breadcrumbs> - - -

- Configure Kubernetes -

-
-
+ +
diff --git a/ui/lib/kubernetes/addon/components/page/overview.hbs b/ui/lib/kubernetes/addon/components/page/overview.hbs index 89fbd4f9b9..6593ed724b 100644 --- a/ui/lib/kubernetes/addon/components/page/overview.hbs +++ b/ui/lib/kubernetes/addon/components/page/overview.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 }} - + {{#if @promptConfig}} diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs index f4f52cc773..c327a4c553 100644 --- a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs +++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs @@ -3,23 +3,15 @@ SPDX-License-Identifier: BUSL-1.1 }} - - + + <:breadcrumbs> - - -

- {{if @form.isNew "Create Role" "Edit Role"}} -

-
-
- -
- -

- A role in Vault dictates what will be generated for Kubernetes and what kind of rules will be used to do so. It is not a - Kubernetes role. -

+ +
{{#each this.generationPreferences as |pref|}} diff --git a/ui/lib/kubernetes/addon/components/page/role/details.hbs b/ui/lib/kubernetes/addon/components/page/role/details.hbs index 2d3a505f5b..1923b2c222 100644 --- a/ui/lib/kubernetes/addon/components/page/role/details.hbs +++ b/ui/lib/kubernetes/addon/components/page/role/details.hbs @@ -3,41 +3,36 @@ SPDX-License-Identifier: BUSL-1.1 }} - - + + <:breadcrumbs> - - -

- {{@role.name}} -

-
-
- - - - {{#if @capabilities.canDelete}} - -
- {{/if}} - {{#if @capabilities.canGenerateCreds}} - - Generate credentials - - {{/if}} - {{#if @capabilities.canUpdate}} - - Edit role - - {{/if}} -
-
+ + <:actions> + + + {{#if @capabilities.canUpdate}} + + Edit role + + {{/if}} + {{#if @capabilities.canGenerateCreds}} + + Generate credentials + + {{/if}} + {{#if @capabilities.canDelete}} + + Delete role + + {{/if}} + + + {{#each this.displayFields as |field|}} {{/each-in}}
-{{/each}} \ No newline at end of file +{{/each}} + +{{#if this.showConfirmDeleteModal}} + +{{/if}} \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/components/page/role/details.ts b/ui/lib/kubernetes/addon/components/page/role/details.ts index 4ed8a5b722..47bb39c4a8 100644 --- a/ui/lib/kubernetes/addon/components/page/role/details.ts +++ b/ui/lib/kubernetes/addon/components/page/role/details.ts @@ -6,6 +6,7 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; import { service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; import type { KubernetesRole } from 'vault/vault/secrets/kubernetes'; import type { Breadcrumb } from 'vault/app-types'; @@ -34,6 +35,8 @@ export default class RoleDetailsPageComponent extends Component { @service declare readonly api: ApiService; @service declare readonly secretMountPath: SecretMountPath; + @tracked showConfirmDeleteModal = false; + label = (field: string) => { return ( { diff --git a/ui/lib/kubernetes/addon/components/page/roles.hbs b/ui/lib/kubernetes/addon/components/page/roles.hbs index 1aa94b1ece..cdaad1df74 100644 --- a/ui/lib/kubernetes/addon/components/page/roles.hbs +++ b/ui/lib/kubernetes/addon/components/page/roles.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 }} - {{/unless}} - +
{{#if @promptConfig}} diff --git a/ui/lib/kubernetes/addon/components/tab-page-header.hbs b/ui/lib/kubernetes/addon/components/tab-page-header.hbs deleted file mode 100644 index 5debc44b2c..0000000000 --- a/ui/lib/kubernetes/addon/components/tab-page-header.hbs +++ /dev/null @@ -1,43 +0,0 @@ -{{! - Copyright IBM Corp. 2016, 2025 - SPDX-License-Identifier: BUSL-1.1 -}} - - - - - - -

- - {{@model.id}} -

-
-
- -
- -
- - - {{#if @filterRoles}} - - - - {{/if}} - - {{yield}} - - \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/engine.js b/ui/lib/kubernetes/addon/engine.js index 30b590f1ad..35c449486c 100644 --- a/ui/lib/kubernetes/addon/engine.js +++ b/ui/lib/kubernetes/addon/engine.js @@ -17,7 +17,7 @@ export default class KubernetesEngine extends Engine { Resolver = Resolver; dependencies = { services: ['app-router', 'secret-mount-path', 'flash-messages', 'api', 'capabilities'], - externalRoutes: ['secrets'], + externalRoutes: ['secrets', 'secretsGeneralSettingsConfiguration'], }; } diff --git a/ui/lib/kubernetes/addon/routes/configuration.ts b/ui/lib/kubernetes/addon/routes/configuration.ts index 23a5ac7f63..2f1ed9c15d 100644 --- a/ui/lib/kubernetes/addon/routes/configuration.ts +++ b/ui/lib/kubernetes/addon/routes/configuration.ts @@ -23,12 +23,14 @@ export default class KubernetesConfigureRoute extends Route { @service declare readonly secretMountPath: SecretMountPath; model() { - const { config, configError, secretsEngine } = this.modelFor('application') as KubernetesApplicationModel; + const { config, configError, secretsEngine, promptConfig } = this.modelFor( + 'application' + ) as KubernetesApplicationModel; // in case of any error other than 404 we want to display that to the user if (configError) { throw configError; } - return { secretsEngine, config }; + return { secretsEngine, config, promptConfig }; } setupController(controller: RouteController, resolvedModel: KubernetesConfigureModel) { diff --git a/ui/lib/kubernetes/addon/templates/configuration.hbs b/ui/lib/kubernetes/addon/templates/configuration.hbs index 3815297e2d..ec96eae17b 100644 --- a/ui/lib/kubernetes/addon/templates/configuration.hbs +++ b/ui/lib/kubernetes/addon/templates/configuration.hbs @@ -6,5 +6,6 @@ \ No newline at end of file diff --git a/ui/lib/kubernetes/addon/templates/error.hbs b/ui/lib/kubernetes/addon/templates/error.hbs index 338fce08a9..7c3edc9cd4 100644 --- a/ui/lib/kubernetes/addon/templates/error.hbs +++ b/ui/lib/kubernetes/addon/templates/error.hbs @@ -3,6 +3,6 @@ SPDX-License-Identifier: BUSL-1.1 }} - + \ No newline at end of file diff --git a/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js b/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js index 9dec6a691e..09f3907d2f 100644 --- a/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js +++ b/ui/tests/acceptance/secrets/backend/kubernetes/roles-test.js @@ -95,14 +95,17 @@ module('Acceptance | kubernetes | roles', function (hooks) { assert.expect(3); await this.visitRoles(); await click('[data-test-list-item-link]'); - await click('[data-test-generate-credentials]'); + await click(GENERAL.dropdownToggle('Manage')); + await click(GENERAL.menuItem('Generate credentials')); this.validateRoute(assert, 'roles.role.credentials', 'Transitions to credentials route'); - await click('[data-test-breadcrumbs] li:nth-child(3) a'); - await click('[data-test-edit]'); + await click(GENERAL.breadcrumbAtIdx(2)); + await click(GENERAL.dropdownToggle('Manage')); + await click(GENERAL.menuItem('Edit role')); this.validateRoute(assert, 'roles.role.edit', 'Transitions to edit route'); await click('[data-test-cancel]'); await click('[data-test-list-item-link]'); - await click('[data-test-delete]'); + await click(GENERAL.dropdownToggle('Manage')); + await click(GENERAL.menuItem('Delete role')); await click(GENERAL.confirmButton); assert .dom('[data-test-list-item-link]') @@ -113,7 +116,8 @@ module('Acceptance | kubernetes | roles', function (hooks) { assert.expect(1); await this.visitRoles(); await click('[data-test-list-item-link]'); - await click('[data-test-generate-credentials]'); + await click(GENERAL.dropdownToggle('Manage')); + await click(GENERAL.menuItem('Generate credentials')); await fillIn('[data-test-kubernetes-namespace]', 'test-namespace'); await click('[data-test-generate-credentials-button]'); await click('[data-test-generate-credentials-done]'); diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index 5b70f59253..1c99a72ee1 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -10,6 +10,7 @@ export const GENERAL = { breadcrumb: '[data-test-breadcrumbs] li', breadcrumbAtIdx: (idx: string) => `[data-test-breadcrumbs] li:nth-child(${idx + 1}) a`, breadcrumbLink: (label: string) => `[data-test-breadcrumb="${label}"] a`, + currentBreadcrumb: (label: string) => `[data-test-breadcrumb="${label}"]`, breadcrumbs: '[data-test-breadcrumbs]', headerContainer: 'header.page-header', title: '[data-test-page-title]', diff --git a/ui/tests/integration/components/kubernetes/tab-page-header-test.js b/ui/tests/integration/components/kubernetes/kubernetes-header-test.js similarity index 69% rename from ui/tests/integration/components/kubernetes/tab-page-header-test.js rename to ui/tests/integration/components/kubernetes/kubernetes-header-test.js index 7d56439a79..29d7be06e5 100644 --- a/ui/tests/integration/components/kubernetes/tab-page-header-test.js +++ b/ui/tests/integration/components/kubernetes/kubernetes-header-test.js @@ -12,7 +12,7 @@ import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import sinon from 'sinon'; -module('Integration | Component | kubernetes | TabPageHeader', function (hooks) { +module('Integration | Component | kubernetes | KubernetesHeader', function (hooks) { setupRenderingTest(hooks); setupEngine(hooks, 'kubernetes'); setupMirage(hooks); @@ -37,7 +37,7 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks) test('it should render breadcrumbs', async function (assert) { await render( - hbs``, + hbs``, { owner: this.engine, } @@ -51,20 +51,20 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks) test('it should render title', async function (assert) { await render( - hbs``, + hbs``, { owner: this.engine, } ); assert - .dom('[data-test-header-title] svg') + .dom(GENERAL.icon('kubernetes-color')) .hasClass('hds-icon-kubernetes-color', 'Correct icon renders in title'); - assert.dom('[data-test-header-title]').hasText(this.mount, 'Mount path renders in title'); + assert.dom(GENERAL.hdsPageHeaderTitle).hasText(this.mount, 'Mount path renders in title'); }); test('it should render tabs', async function (assert) { await render( - hbs``, + hbs``, { owner: this.engine, } @@ -76,7 +76,7 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks) test('it should render filter for roles', async function (assert) { await render( - hbs``, + hbs``, { owner: this.engine } ); assert.dom(GENERAL.filterInputExplicit).hasValue('test', 'Filter renders with provided value'); @@ -85,9 +85,9 @@ module('Integration | Component | kubernetes | TabPageHeader', function (hooks) test('it should yield block for toolbar actions', async function (assert) { await render( hbs` - + It yields! - + `, { owner: this.engine } ); diff --git a/ui/tests/integration/components/kubernetes/page/configuration-test.js b/ui/tests/integration/components/kubernetes/page/configuration-test.js index 4a4d0cefa5..4f3ca2da76 100644 --- a/ui/tests/integration/components/kubernetes/page/configuration-test.js +++ b/ui/tests/integration/components/kubernetes/page/configuration-test.js @@ -51,8 +51,10 @@ module('Integration | Component | kubernetes | Page::Configuration', function (h test('it should render tab page header, config cta and mount config', async function (assert) { await this.renderComponent(); - assert.dom('.title svg').hasClass('hds-icon-kubernetes-color', 'Kubernetes icon renders in title'); - assert.dom('.title').hasText('kubernetes-test', 'Mount path renders in title'); + assert + .dom(GENERAL.icon('kubernetes-color')) + .hasClass('hds-icon-kubernetes-color', 'Kubernetes icon renders in title'); + assert.dom(GENERAL.hdsPageHeaderTitle).hasText('kubernetes-test', 'Mount path renders in title'); assert.dom(SES.configure).doesNotExist('Toolbar action does not render when engine is not configured'); assert.dom(GENERAL.emptyStateTitle).hasText('Kubernetes not configured'); assert.dom(GENERAL.emptyStateActions).hasText('Configure Kubernetes'); diff --git a/ui/tests/integration/components/kubernetes/page/role/details-test.js b/ui/tests/integration/components/kubernetes/page/role/details-test.js index 6f1dd502b2..3930ea75ca 100644 --- a/ui/tests/integration/components/kubernetes/page/role/details-test.js +++ b/ui/tests/integration/components/kubernetes/page/role/details-test.js @@ -79,17 +79,15 @@ module('Integration | Component | kubernetes | Page::Role::Details', function (h test('it should render header with role name and breadcrumbs', async function (assert) { await this.renderComponent(); - assert.dom('[data-test-header-title]').hasText(this.role.name, 'Role name renders in header'); + assert.dom(GENERAL.hdsPageHeaderTitle).hasText(this.role.name, 'Role name renders in header'); + assert.dom(GENERAL.breadcrumbAtIdx(0)).containsText(this.backend, 'Overview breadcrumb renders'); + assert.dom(GENERAL.breadcrumbAtIdx(1)).containsText('Roles', 'Roles breadcrumb renders'); assert - .dom('[data-test-breadcrumbs] li:nth-child(1)') - .containsText(this.backend, 'Overview breadcrumb renders'); - assert.dom('[data-test-breadcrumbs] li:nth-child(2) a').containsText('Roles', 'Roles breadcrumb renders'); - assert - .dom('[data-test-breadcrumbs] li:nth-child(3)') - .containsText(this.role.name, 'Role breadcrumb renders'); + .dom(GENERAL.currentBreadcrumb(this.role.name)) + .containsText(this.role.name, 'Role name breadcrumb renders'); }); - test('it should render toolbar actions', async function (assert) { + test('it should render role page header dropdown', async function (assert) { assert.expect(5); const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); @@ -98,14 +96,13 @@ module('Integration | Component | kubernetes | Page::Role::Details', function (h .resolves(); await this.renderComponent(); - - assert.dom('[data-test-delete]').hasText('Delete role', 'Delete action renders'); + await click(GENERAL.dropdownToggle('Manage')); + assert.dom(GENERAL.menuItem('Delete role')).hasText('Delete role', 'Delete action renders in dropdown'); assert - .dom('[data-test-generate-credentials]') + .dom(GENERAL.menuItem('Generate credentials')) .hasText('Generate credentials', 'Generate credentials action renders'); - assert.dom('[data-test-edit]').hasText('Edit role', 'Edit action renders'); - - await click('[data-test-delete]'); + assert.dom(GENERAL.menuItem('Edit role')).hasText('Edit role', 'Edit action renders'); + await click(GENERAL.menuItem('Delete role')); await click(GENERAL.confirmButton); assert.true(deleteStub.calledWith(this.role.name, this.backend), 'Request made to delete role'); diff --git a/ui/tests/integration/components/kubernetes/page/roles-test.js b/ui/tests/integration/components/kubernetes/page/roles-test.js index cfadfc8f64..2ef2419ae4 100644 --- a/ui/tests/integration/components/kubernetes/page/roles-test.js +++ b/ui/tests/integration/components/kubernetes/page/roles-test.js @@ -55,8 +55,10 @@ module('Integration | Component | kubernetes | Page::Roles', function (hooks) { test('it should render tab page header and config cta', async function (assert) { this.promptConfig = true; await this.renderComponent(); - assert.dom('.title svg').hasClass('hds-icon-kubernetes-color', 'Kubernetes icon renders in title'); - assert.dom('.title').hasText('kubernetes-test', 'Mount path renders in title'); + assert + .dom(GENERAL.icon('kubernetes-color')) + .hasClass('hds-icon-kubernetes-color', 'Kubernetes icon renders in title'); + assert.dom(GENERAL.hdsPageHeaderTitle).hasText('kubernetes-test', 'Mount path renders in title'); assert .dom('[data-test-toolbar-roles-action]') .doesNotExist('Create role', 'Toolbar action does not render when not configured');