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