[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>
This commit is contained in:
Vault Automation 2026-01-29 12:53:35 -05:00 committed by GitHub
parent 2eb6905459
commit ad7cf8ca8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 273 additions and 102 deletions

View file

@ -3,7 +3,13 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title="License" />
<Page::Header @title="License">
<:breadcrumbs>
<Page::Breadcrumbs
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="License")}}
/>
</:breadcrumbs>
</Page::Header>
<section class="box is-sideless is-marginless is-shadowless is-fullwidth">
<span class="title is-5">Details</span>

View file

@ -2,7 +2,10 @@
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Breadcrumbs
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Vault usage")}}
class="has-top-margin-s has-top-padding-m has-left-padding-s"
/>
<VaultReporting::Views::Dashboard
@onFetchUsageData={{this.handleFetchUsageData}}
@onFetchNamespaceData={{this.handleFetchNamespaceData}}

View file

@ -3,6 +3,8 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Sidebar::Nav::Reporting />
<LicenseInfo
@startTime={{this.model.startTime}}
@expirationTime={{this.model.expirationTime}}

View file

@ -2,5 +2,6 @@
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Sidebar::Nav::Reporting />
<Usage::Page />

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Breadcrumb data-test-breadcrumbs>
<Hds::Breadcrumb data-test-breadcrumbs ...attributes>
{{#each @breadcrumbs as |breadcrumb|}}
<Hds::Breadcrumb::Item
data-test-breadcrumb={{breadcrumb.label}}

View file

@ -88,22 +88,22 @@
data-test-sidebar-nav-link="Client Count"
/>
{{/if}}
{{#if this.canAccessVaultUsageDashboard}}
<Nav.Link @route="vault.cluster.usage-reporting" @text="Vault Usage" data-test-sidebar-nav-link="Vault Usage" />
{{/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)
)
)
}}
<Nav.Link
@route="vault.cluster.license"
@model={{this.cluster.name}}
@text="License"
data-test-sidebar-nav-link="License"
@route={{if this.canAccessVaultUsageDashboard "vault.cluster.usage-reporting" "vault.cluster.license"}}
@text="Reporting"
data-test-sidebar-nav-link="Reporting"
@hasSubItems={{true}}
/>
{{/if}}
{{#if (and this.isRootNamespace (has-permission "status" routeParams="seal") (not this.cluster.dr.isSecondary))}}

View file

@ -0,0 +1,34 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::AppSideNav::Portal @ariaLabel="Reporting Navigation Links" data-test-sidebar-nav-panel="Reporting" as |Nav|>
<Nav.BackLink
@route="vault.cluster"
@current-when={{false}}
@icon="arrow-left"
@text="Back to main navigation"
data-test-sidebar-nav-link="Back to main navigation"
/>
<Nav.Title data-test-sidebar-nav-heading="Reporting">Reporting</Nav.Title>
{{#if this.canAccessVaultUsageDashboard}}
<Nav.Link @route="vault.cluster.usage-reporting" @text="Vault usage" data-test-sidebar-nav-link="Vault usage" />
{{/if}}
{{#if
(and
this.version.features
this.isRootNamespace
(has-permission "status" routeParams="license")
(not this.cluster.dr.isSecondary)
)
}}
<Nav.Link
@route="vault.cluster.license"
@model={{this.cluster.name}}
@text="License"
data-test-sidebar-nav-link="License"
/>
{{/if}}
</Hds::AppSideNav::Portal>

View file

@ -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<Args> {
@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;
}
}

View file

@ -0,0 +1,6 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/components/sidebar/nav/reporting';

View file

@ -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');
});
});

View file

@ -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) {

View file

@ -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'];

View file

@ -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`
<Sidebar::Frame @isVisible={{true}}>
<Sidebar::Nav::Reporting />
</Sidebar::Frame>
`);
};
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();
});
});