From ad7cf8ca8dc331b99eeed4803678af6f12d2be24 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 29 Jan 2026 12:53:35 -0500 Subject: [PATCH] [UI] VAULT-41963 add reporting sidenav (#12011) (#12055) * VAULT-41963 add reporting sidenav * Fix failing tests and add new tests * Add copywrite headers * Update checks for reporting * Update vault reporting acceptance tests * Update tests to use test helpers * Update sidebar enterprise test selectors * Update vault reporting title test Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/app/components/license-info.hbs | 8 +- ui/app/components/usage/page.hbs | 5 +- ui/app/templates/vault/cluster/license.hbs | 2 + .../vault/cluster/usage-reporting.hbs | 1 + .../addon/components/page/breadcrumbs.hbs | 2 +- .../addon/components/sidebar/nav/cluster.hbs | 24 ++-- .../components/sidebar/nav/reporting.hbs | 34 +++++ .../addon/components/sidebar/nav/reporting.ts | 53 ++++++++ .../app/components/sidebar/nav/reporting.js | 6 + .../acceptance/enterprise-sidebar-nav-test.js | 58 +++++---- .../acceptance/vault-reporting/index-test.js | 8 +- .../components/sidebar/nav/cluster-test.js | 58 +-------- .../components/sidebar/nav/reporting-test.js | 116 ++++++++++++++++++ 13 files changed, 273 insertions(+), 102 deletions(-) create mode 100644 ui/lib/core/addon/components/sidebar/nav/reporting.hbs create mode 100644 ui/lib/core/addon/components/sidebar/nav/reporting.ts create mode 100644 ui/lib/core/app/components/sidebar/nav/reporting.js create mode 100644 ui/tests/integration/components/sidebar/nav/reporting-test.js diff --git a/ui/app/components/license-info.hbs b/ui/app/components/license-info.hbs index 7799eccb3e..3780ff3850 100644 --- a/ui/app/components/license-info.hbs +++ b/ui/app/components/license-info.hbs @@ -3,7 +3,13 @@ SPDX-License-Identifier: BUSL-1.1 }} - + + <:breadcrumbs> + + +
Details diff --git a/ui/app/components/usage/page.hbs b/ui/app/components/usage/page.hbs index fe4018f97e..d39575e355 100644 --- a/ui/app/components/usage/page.hbs +++ b/ui/app/components/usage/page.hbs @@ -2,7 +2,10 @@ Copyright IBM Corp. 2016, 2025 SPDX-License-Identifier: BUSL-1.1 }} - + + \ No newline at end of file diff --git a/ui/lib/core/addon/components/page/breadcrumbs.hbs b/ui/lib/core/addon/components/page/breadcrumbs.hbs index 31b7f59944..52b0709af4 100644 --- a/ui/lib/core/addon/components/page/breadcrumbs.hbs +++ b/ui/lib/core/addon/components/page/breadcrumbs.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 }} - + {{#each @breadcrumbs as |breadcrumb|}} {{/if}} - {{#if this.canAccessVaultUsageDashboard}} - - {{/if}} {{#if - (and - this.version.features - this.isRootNamespace - (has-permission "status" routeParams="license") - (not this.cluster.dr.isSecondary) + (or + this.canAccessVaultUsageDashboard + (and + this.version.features + this.isRootNamespace + (has-permission "status" routeParams="license") + (not this.cluster.dr.isSecondary) + ) ) }} {{/if}} {{#if (and this.isRootNamespace (has-permission "status" routeParams="seal") (not this.cluster.dr.isSecondary))}} diff --git a/ui/lib/core/addon/components/sidebar/nav/reporting.hbs b/ui/lib/core/addon/components/sidebar/nav/reporting.hbs new file mode 100644 index 0000000000..26b722f9cf --- /dev/null +++ b/ui/lib/core/addon/components/sidebar/nav/reporting.hbs @@ -0,0 +1,34 @@ +{{! + Copyright IBM Corp. 2016, 2025 + SPDX-License-Identifier: BUSL-1.1 +}} + + + + + Reporting + {{#if this.canAccessVaultUsageDashboard}} + + {{/if}} + {{#if + (and + this.version.features + this.isRootNamespace + (has-permission "status" routeParams="license") + (not this.cluster.dr.isSecondary) + ) + }} + + {{/if}} + \ No newline at end of file diff --git a/ui/lib/core/addon/components/sidebar/nav/reporting.ts b/ui/lib/core/addon/components/sidebar/nav/reporting.ts new file mode 100644 index 0000000000..5fc6d516ab --- /dev/null +++ b/ui/lib/core/addon/components/sidebar/nav/reporting.ts @@ -0,0 +1,53 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { service } from '@ember/service'; + +import type CurrentClusterService from 'vault/services/current-cluster'; +import type VersionService from 'vault/services/version'; +import type NamespaceService from 'vault/services/namespace'; +import type ClusterModel from 'vault/models/cluster'; +import type PermissionsService from 'vault/services/permissions'; + +interface Args { + isEngine?: boolean; +} + +export default class SidebarNavReportingComponent extends Component { + @service declare readonly currentCluster: CurrentClusterService; + @service declare readonly version: VersionService; + @service declare readonly namespace: NamespaceService; + @service declare readonly permissions: PermissionsService; + + get cluster() { + return this.currentCluster.cluster as ClusterModel | null; + } + + get hasChrootNamespace() { + return this.cluster?.hasChrootNamespace; + } + + get isRootNamespace() { + // should only return true if we're in the true root namespace + return this.namespace.inRootNamespace && !this.hasChrootNamespace; + } + + get canAccessVaultUsageDashboard() { + /* + A user can access Vault Usage if they satisfy the following conditions: + 1) They have access to sys/v1/utilization-report endpoint + 2) They are either + a) enterprise cluster and root namespace + b) hvd cluster and /admin namespace + */ + + const hasPermission = this.permissions.hasNavPermission('monitoring'); + const isEnterprise = this.version.isEnterprise; + const isCorrectNamespace = this.isRootNamespace || this.namespace.inHvdAdminNamespace; + + return hasPermission && isEnterprise && isCorrectNamespace; + } +} diff --git a/ui/lib/core/app/components/sidebar/nav/reporting.js b/ui/lib/core/app/components/sidebar/nav/reporting.js new file mode 100644 index 0000000000..64221981d6 --- /dev/null +++ b/ui/lib/core/app/components/sidebar/nav/reporting.js @@ -0,0 +1,6 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +export { default } from 'core/components/sidebar/nav/reporting'; diff --git a/ui/tests/acceptance/enterprise-sidebar-nav-test.js b/ui/tests/acceptance/enterprise-sidebar-nav-test.js index e0d0437f0b..1375025892 100644 --- a/ui/tests/acceptance/enterprise-sidebar-nav-test.js +++ b/ui/tests/acceptance/enterprise-sidebar-nav-test.js @@ -8,8 +8,8 @@ import { setupApplicationTest } from 'ember-qunit'; import { click, currentURL } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; +import { GENERAL } from '../helpers/general-selectors'; -const link = (label) => `[data-test-sidebar-nav-link="${label}"]`; const panel = (label) => `[data-test-sidebar-nav-panel="${label}"]`; module('Acceptance | Enterprise | sidebar navigation', function (hooks) { @@ -24,55 +24,53 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) { test(`it should render enterprise only navigation links`, async function (assert) { assert.dom(panel('Cluster')).exists('Cluster nav panel renders'); - await click(link('Secrets Sync')); + await click(GENERAL.navLink('Secrets Sync')); assert.strictEqual(currentURL(), '/vault/sync/secrets/overview', 'Sync route renders'); - await click(link('Replication')); + await click(GENERAL.navLink('Replication')); assert.strictEqual(currentURL(), '/vault/replication', 'Replication route renders'); assert.dom(panel('Replication')).exists(`Replication nav panel renders`); - assert.dom(link('Overview')).hasClass('active', 'Overview link is active'); - assert.dom(link('Performance')).exists('Performance link exists'); - assert.dom(link('Disaster Recovery')).exists('DR link exists'); + assert.dom(GENERAL.navLink('Overview')).hasClass('active', 'Overview link is active'); + assert.dom(GENERAL.navLink('Performance')).exists('Performance link exists'); + assert.dom(GENERAL.navLink('Disaster Recovery')).exists('DR link exists'); - await click(link('Performance')); + await click(GENERAL.navLink('Performance')); assert.strictEqual( currentURL(), '/vault/replication/performance', 'Replication performance route renders' ); - await click(link('Disaster Recovery')); + await click(GENERAL.navLink('Disaster Recovery')); assert.strictEqual(currentURL(), '/vault/replication/dr', 'Replication DR route renders'); - await click(link('Back to main navigation')); + await click(GENERAL.navLink('Back to main navigation')); - await click(link('Client Count')); + await click(GENERAL.navLink('Client Count')); assert.dom(panel('Client Count')).exists('Client Count nav panel renders'); - assert.dom(link('Client Usage')).hasClass('active', 'Client Usage link is active'); + assert.dom(GENERAL.navLink('Client Usage')).hasClass('active', 'Client Usage link is active'); assert.strictEqual(currentURL(), '/vault/clients/counts/overview', 'Client counts route renders'); - await click(link('Back to main navigation')); + await click(GENERAL.navLink('Back to main navigation')); - await click(link('License')); - assert.strictEqual(currentURL(), '/vault/license', 'License route renders'); - await click(link('Access')); - await click(link('Approval workflow')); + await click(GENERAL.navLink('Access')); + await click(GENERAL.navLink('Approval workflow')); assert.strictEqual(currentURL(), '/vault/access/control-groups', 'Approval workflow route renders'); - await click(link('Namespaces')); + await click(GENERAL.navLink('Namespaces')); assert.strictEqual(currentURL(), '/vault/access/namespaces?page=1', 'Replication route renders'); - await click(link('Back to main navigation')); - await click(link('Access')); - await click(link('Role governing policies')); + await click(GENERAL.navLink('Back to main navigation')); + await click(GENERAL.navLink('Access')); + await click(GENERAL.navLink('Role governing policies')); assert.strictEqual(currentURL(), '/vault/policies/rgp', 'Role governing policies route renders'); - await click(link('Endpoint governing policies')); + await click(GENERAL.navLink('Endpoint governing policies')); assert.strictEqual(currentURL(), '/vault/policies/egp', 'Endpoint governing policies route renders'); }); test('it should link to correct routes at the access level', async function (assert) { assert.expect(12); - await click(link('Access')); + await click(GENERAL.navLink('Access')); assert.dom(panel('Access')).exists('Access nav panel renders'); const links = [ @@ -90,19 +88,25 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) { ]; for (const l of links) { - await click(link(l.label)); + await click(GENERAL.navLink(l.label)); assert.ok(currentURL().includes(l.route), `${l.label} route renders`); } }); test('it should navigate to the correct links from Operational tools > Custom messages ember engine (enterprise)', async function (assert) { - await click(link('Operational tools')); + await click(GENERAL.navLink('Operational tools')); assert.strictEqual(currentURL(), '/vault/tools/wrap', 'Tool route renders'); - await click(link('Custom messages')); + await click(GENERAL.navLink('Custom messages')); assert.strictEqual(currentURL(), '/vault/config-ui/messages', 'Custom messages route renders'); - await click(link('Lookup')); + await click(GENERAL.navLink('Lookup')); assert.strictEqual(currentURL(), '/vault/tools/lookup', 'Lookup route renders'); - await click(link('UI login settings')); + await click(GENERAL.navLink('UI login settings')); assert.strictEqual(currentURL(), '/vault/config-ui/login-settings', 'UI login settings route renders'); }); + + test('it should navigate to the Licenses from Reporting level (enterprise)', async function (assert) { + await click(GENERAL.navLink('Reporting')); + await click(GENERAL.navLink('License')); + assert.strictEqual(currentURL(), '/vault/license', 'License route renders'); + }); }); diff --git a/ui/tests/acceptance/vault-reporting/index-test.js b/ui/tests/acceptance/vault-reporting/index-test.js index 27ea5e30b5..26134462ad 100644 --- a/ui/tests/acceptance/vault-reporting/index-test.js +++ b/ui/tests/acceptance/vault-reporting/index-test.js @@ -10,6 +10,7 @@ import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { mockedResponseWithData, mockedEmptyResponse } from 'vault/tests/helpers/vault-usage/mocks'; import { createPolicyCmd, createTokenCmd, runCmd } from 'vault/tests/helpers/commands'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; const loginWithReportingToken = async (capability = 'read') => { const policyName = 'show-vault-reporting'; @@ -37,15 +38,16 @@ module('Acceptance | enterprise vault-reporting', function (hooks) { // Log in with lower privileged token await loginWithReportingToken('read'); await visit('/vault/dashboard'); - await click('[data-test-sidebar-nav-link="Vault Usage"]'); + await click(GENERAL.navLink('Reporting')); + await click(GENERAL.navLink('Vault usage')); assert.strictEqual(currentURL(), '/vault/usage-reporting', 'navigates to usage reporting dashboard'); - assert.dom('.hds-page-header').includesText('Vault Usage', 'renders the "Vault Usage" header'); + assert.dom(GENERAL.hdsPageHeaderTitle).includesText('Vault Usage', 'renders the "Vault Usage" header'); }); test('it hides the nav item if policy does not allow access to sys/utilization-report', async function (assert) { await loginWithReportingToken('deny'); await visit('/vault/dashboard'); - assert.dom('[data-test-sidebar-nav-link="Vault Usage"]').doesNotExist('sidebar nav link is hidden'); + assert.dom(GENERAL.navLink('Vault usage')).doesNotExist('sidebar nav link is hidden'); }); test('it renders the counters dashboard block with all expected counters', async function (assert) { diff --git a/ui/tests/integration/components/sidebar/nav/cluster-test.js b/ui/tests/integration/components/sidebar/nav/cluster-test.js index 1a9c4f1847..0e9305bf17 100644 --- a/ui/tests/integration/components/sidebar/nav/cluster-test.js +++ b/ui/tests/integration/components/sidebar/nav/cluster-test.js @@ -64,10 +64,9 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) { 'Access', 'Operational tools', 'Replication', + 'Reporting', 'Raft Storage', 'Client Count', - 'Vault Usage', - 'License', 'Seal Vault', ]; // do not add PKI-only Secrets feature as it hides Client Count nav link @@ -201,61 +200,6 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) { assert.dom(GENERAL.navLink('Secrets Sync')).exists(); }); - test('it shows Vault Usage when user is enterprise and in root namespace', async function (assert) { - stubFeaturesAndPermissions(this.owner, true); - await renderComponent(); - assert.dom(GENERAL.navLink('Vault Usage')).exists(); - }); - - test('it does NOT show Vault Usage when user is user is on CE || OSS || community', async function (assert) { - stubFeaturesAndPermissions(this.owner, false); - await renderComponent(); - assert.dom(GENERAL.navLink('Vault Usage')).doesNotExist(); - }); - - test('it does NOT show Vault Usage when user is enterprise but not in root namespace', async function (assert) { - stubFeaturesAndPermissions(this.owner, true); - - this.owner.lookup('service:namespace').set('path', 'foo'); - - await renderComponent(); - assert.dom(GENERAL.navLink('Vault Usage')).doesNotExist(); - }); - - test('it does NOT show Vault Usage when user lacks the necessary permission', async function (assert) { - // no permissions - stubFeaturesAndPermissions(this.owner, true, false, [], false); - - await renderComponent(); - assert.dom(GENERAL.navLink('Vault Usage')).doesNotExist(); - }); - - test('it does NOT Vault Usage if the user has the necessary permission but user is on CE || OSS || community', async function (assert) { - // no permissions - const stubs = stubFeaturesAndPermissions(this.owner, false, false, [], false); - - // allow the route - stubs.hasNavPermission.callsFake((route) => route === 'monitoring'); - - await renderComponent(); - - assert.dom(GENERAL.navLink('Vault Usage')).doesNotExist(); - }); - - test('it shows Vault Usage when user is in HVD admin namespace', async function (assert) { - const stubs = stubFeaturesAndPermissions(this.owner, true, false, [], false); - stubs.hasNavPermission.callsFake((route) => route === 'monitoring'); - - this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; - - const namespace = this.owner.lookup('service:namespace'); - namespace.setNamespace('admin'); - - await renderComponent(); - - assert.dom(GENERAL.navLink('Vault Usage')).exists(); - }); - test('it does NOT show Secrets Recovery when user is in HVD admin namespace', async function (assert) { this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; diff --git a/ui/tests/integration/components/sidebar/nav/reporting-test.js b/ui/tests/integration/components/sidebar/nav/reporting-test.js new file mode 100644 index 0000000000..60c916ad90 --- /dev/null +++ b/ui/tests/integration/components/sidebar/nav/reporting-test.js @@ -0,0 +1,116 @@ +/** + * 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 hbs from 'htmlbars-inline-precompile'; +import { stubFeaturesAndPermissions } from 'vault/tests/helpers/components/sidebar-nav'; +import { capitalize } from '@ember/string'; +import { setRunOptions } from 'ember-a11y-testing/test-support'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; + +const renderComponent = () => { + return render(hbs` + + + + `); +}; + +module('Integration | Component | sidebar-nav-reporting', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.flags = this.owner.lookup('service:flags'); + + setRunOptions({ + rules: { + // This is an issue with Hds::AppHeader::HomeLink + 'aria-prohibited-attr': { enabled: false }, + // TODO: fix use Dropdown on user-menu + 'nested-interactive': { enabled: false }, + }, + }); + }); + + test('it should hide links user does not have access to', async function (assert) { + await renderComponent(); + stubFeaturesAndPermissions(this.owner); + assert + .dom(GENERAL.navLink()) + .exists({ count: 1 }, 'Nav links are hidden other than back link and license'); + }); + + test('it should render nav headings and links', async function (assert) { + const links = ['Back to main navigation', 'Vault usage', 'License']; + stubFeaturesAndPermissions(this.owner, true); + await renderComponent(); + + assert.dom(GENERAL.navHeading()).exists({ count: 1 }, 'Correct number of headings render'); + assert.dom(GENERAL.navHeading('Reporting')).hasText('Reporting', 'Reporting heading renders'); + + assert.dom(GENERAL.navLink()).exists({ count: links.length }, 'Correct number of links render'); + links.forEach((link) => { + const name = capitalize(link); + assert.dom(GENERAL.navLink(name)).hasText(name, `${name} link renders`); + }); + }); + + test('it shows Vault Usage when user is enterprise and in root namespace', async function (assert) { + stubFeaturesAndPermissions(this.owner, true); + await renderComponent(); + assert.dom(GENERAL.navLink('Vault usage')).exists(); + }); + + test('it does NOT show Vault Usage when user is user is on CE || OSS || community', async function (assert) { + stubFeaturesAndPermissions(this.owner, false); + await renderComponent(); + assert.dom(GENERAL.navLink('Vault usage')).doesNotExist(); + }); + + test('it does NOT show Vault Usage when user is enterprise but not in root namespace', async function (assert) { + stubFeaturesAndPermissions(this.owner, true); + + this.owner.lookup('service:namespace').set('path', 'foo'); + + await renderComponent(); + assert.dom(GENERAL.navLink('Vault usage')).doesNotExist(); + }); + + test('it does NOT show Vault Usage when user lacks the necessary permission', async function (assert) { + // no permissions + stubFeaturesAndPermissions(this.owner, true, false, [], false); + + await renderComponent(); + assert.dom(GENERAL.navLink('Vault usage')).doesNotExist(); + }); + + test('it does NOT Vault Usage if the user has the necessary permission but user is on CE || OSS || community', async function (assert) { + // no permissions + const stubs = stubFeaturesAndPermissions(this.owner, false, false, [], false); + + // allow the route + stubs.hasNavPermission.callsFake((route) => route === 'monitoring'); + + await renderComponent(); + + assert.dom(GENERAL.navLink('Vault usage')).doesNotExist(); + }); + + test('it shows Vault Usage when user is in HVD admin namespace', async function (assert) { + const stubs = stubFeaturesAndPermissions(this.owner, true, false, [], false); + stubs.hasNavPermission.callsFake((route) => route === 'monitoring'); + + this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; + + const namespace = this.owner.lookup('service:namespace'); + namespace.setNamespace('admin'); + + await renderComponent(); + + assert.dom(GENERAL.navLink('Vault usage')).exists(); + }); +});