mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 20:36:26 +02:00
Co-authored-by: Eren Tantekin <eren.tantekin@hashicorp.com> Co-authored-by: Jim Wright <jim.wright@hashicorp.com>
This commit is contained in:
parent
64fd8225bc
commit
f17451d675
@ -5,5 +5,6 @@
|
||||
|
||||
<VaultReporting::Views::Dashboard
|
||||
@onFetchUsageData={{this.handleFetchUsageData}}
|
||||
@onFetchNamespaceData={{this.handleFetchNamespaceData}}
|
||||
@isVaultDedicated={{this.flags.isHvdManaged}}
|
||||
/>
|
||||
@ -8,12 +8,20 @@ import { service } from '@ember/service';
|
||||
|
||||
import type FlagsService from 'vault/services/flags';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type NamespaceService from 'vault/services/namespace';
|
||||
import type AuthService from 'vault/vault/services/auth';
|
||||
import type {
|
||||
getUsageDataFunction,
|
||||
getNamespaceDataFunction,
|
||||
UsageDashboardData,
|
||||
} from '@hashicorp-internal/vault-reporting/types/index';
|
||||
import type { UtilizationReport } from 'vault/usage';
|
||||
|
||||
interface NamespaceOption {
|
||||
path: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @module UsagePage
|
||||
* @description This component is responsible for fetching usage data and mounting the vault-reporting dashboard view.
|
||||
@ -31,15 +39,21 @@ import type { UtilizationReport } from 'vault/usage';
|
||||
export default class UsagePage extends Component {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flags: FlagsService;
|
||||
@service declare readonly namespace: NamespaceService;
|
||||
@service declare readonly auth: AuthService;
|
||||
|
||||
handleFetchUsageData: getUsageDataFunction = async () => {
|
||||
handleFetchUsageData: getUsageDataFunction = async (namespace?: string) => {
|
||||
/**
|
||||
* We get a partially typed response from the API client, but only 1 level deep.
|
||||
* Casting the nested types here and falling back to defaults in the mappings.
|
||||
* We should get typescript errors if top level interfaces in the API client or
|
||||
* the vault-reporting addon change.
|
||||
*/
|
||||
const response = (await this.api.sys.generateUtilizationReport()) as UtilizationReport;
|
||||
|
||||
// Convert "root" display value back to empty string for API calls
|
||||
const apiNamespace = namespace === 'root' ? undefined : namespace;
|
||||
const response = (await this.api.sys.generateUtilizationReport(apiNamespace)) as UtilizationReport;
|
||||
|
||||
const { lease_count_quotas, replication_status, pki, secret_sync } = response;
|
||||
|
||||
const data: UsageDashboardData = {
|
||||
@ -74,4 +88,49 @@ export default class UsagePage extends Component {
|
||||
};
|
||||
return data;
|
||||
};
|
||||
|
||||
handleFetchNamespaceData: getNamespaceDataFunction = async () => {
|
||||
await this.namespace?.findNamespacesForUser?.perform();
|
||||
const options = this.getOptions(this.namespace?.accessibleNamespaces);
|
||||
const data = {
|
||||
keys: options.map((option) => option.label),
|
||||
};
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* getOptions from ui/app/components/namespace-picker.ts
|
||||
* We might consider moving this into a util function and sharing it across both files
|
||||
*/
|
||||
private getOptions(accessibleNamespaces: string[]): NamespaceOption[] {
|
||||
/* Each namespace option has 2 properties: { path and label }
|
||||
* - path: full namespace path (used to navigate to the namespace)
|
||||
* - label: text displayed inside the namespace picker dropdown (if root, then path is "", else label = path)
|
||||
*
|
||||
* Example:
|
||||
* | path | label |
|
||||
* | ---- | ----- |
|
||||
* | '' | 'root' |
|
||||
* | 'parent' | 'parent' |
|
||||
* | 'parent/child' | 'parent/child' |
|
||||
*/
|
||||
const options = (accessibleNamespaces || []).map((ns: string) => ({ path: ns, label: ns }));
|
||||
|
||||
// Add the user's root namespace because `sys/internal/ui/namespaces` does not include it.
|
||||
const userRootNamespace = this.auth.authData?.userRootNamespace;
|
||||
if (!options?.find((o) => o.path === userRootNamespace)) {
|
||||
// the 'root' namespace is technically an empty string so we manually add the 'root' label.
|
||||
const label = userRootNamespace === '' ? 'root' : userRootNamespace;
|
||||
options.unshift({ path: userRootNamespace, label });
|
||||
}
|
||||
|
||||
// If there are no namespaces returned by the internal endpoint, add the current namespace
|
||||
// to the list of options. This is a fallback for when the user has access to a single namespace.
|
||||
if (options.length === 0) {
|
||||
// 'path' defined in the namespace service is the full namespace path
|
||||
options.push({ path: this.namespace.path, label: this.namespace.path });
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { visit, currentURL, waitFor, click } from '@ember/test-helpers';
|
||||
import { visit, currentURL, waitFor, click, fillIn } from '@ember/test-helpers';
|
||||
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';
|
||||
@ -265,4 +265,82 @@ module('Acceptance | enterprise vault-reporting', function (hooks) {
|
||||
.dom('[data-test-vault-reporting-dashboard-lease-count]')
|
||||
.includesText('Global lease count quota', 'Lease quota empty state: docs link is shown');
|
||||
});
|
||||
|
||||
test('namespace lookup functionality', async function (assert) {
|
||||
this.server.get('sys/internal/ui/namespaces', {
|
||||
data: {
|
||||
keys: ['child-ns1/', 'child-ns2/'],
|
||||
},
|
||||
});
|
||||
|
||||
// Mock different responses for different namespaces
|
||||
const defaultMockResponse = mockedResponseWithData;
|
||||
const childNs1MockResponse = {
|
||||
data: {
|
||||
...mockedResponseWithData.data,
|
||||
kvv2_secrets: 200,
|
||||
kvv1_secrets: 50,
|
||||
},
|
||||
};
|
||||
|
||||
// Initially load default data
|
||||
this.server.get('sys/utilization-report', () => defaultMockResponse);
|
||||
|
||||
const namespaceService = this.owner.lookup('service:namespace');
|
||||
namespaceService.set('accessibleNamespaces', ['child-ns1/', 'child-ns2/']);
|
||||
namespaceService.set('path', 'parent-ns');
|
||||
|
||||
await visit('/vault/usage-reporting');
|
||||
|
||||
// Verify initial KV secrets count (should be 40 from default mock)
|
||||
await waitFor('[data-test-vault-reporting-counter="KV secrets"]');
|
||||
assert
|
||||
.dom('[data-test-vault-reporting-counter="KV secrets"]')
|
||||
.includesText('100', 'Initial KV secrets count is 100');
|
||||
|
||||
// Update mock to return different data for child-ns1
|
||||
this.server.get('sys/utilization-report', () => childNs1MockResponse);
|
||||
|
||||
// Click the namespace picker dropdown to open it
|
||||
await click('.hds-dropdown.ssu-namespace-picker button');
|
||||
|
||||
// Verify all namespaces are visible initially
|
||||
assert.dom('[data-test-vault-reporting-namespace-menu-item="root"]').exists('root namespace is visible');
|
||||
assert
|
||||
.dom('[data-test-vault-reporting-namespace-menu-item="child-ns1"]')
|
||||
.exists('child-ns1 namespace is visible');
|
||||
assert
|
||||
.dom('[data-test-vault-reporting-namespace-menu-item="child-ns2"]')
|
||||
.exists('child-ns2 namespace is visible');
|
||||
|
||||
// Use the search bar to search for "ns1"
|
||||
await fillIn('[data-test-vault-reporting-namespace-search]', 'ns1');
|
||||
|
||||
// Verify only child-ns1 is visible after search
|
||||
assert
|
||||
.dom('[data-test-vault-reporting-namespace-menu-item="child-ns1"]')
|
||||
.exists('child-ns1 is visible after search');
|
||||
|
||||
// Verify root and child-ns2 are filtered out
|
||||
assert
|
||||
.dom('[data-test-vault-reporting-namespace-menu-item="root"]')
|
||||
.doesNotExist('root namespace is filtered out');
|
||||
assert
|
||||
.dom('[data-test-vault-reporting-namespace-menu-item="child-ns2"]')
|
||||
.doesNotExist('child-ns2 namespace is filtered out');
|
||||
|
||||
// Click on child-ns1 to select it
|
||||
await click('[data-test-vault-reporting-namespace-menu-item="child-ns1"]');
|
||||
|
||||
// Verify that child-ns1 is now displayed as the selected namespace in the closed dropdown
|
||||
assert
|
||||
.dom('.hds-dropdown.ssu-namespace-picker')
|
||||
.includesText('child-ns1', 'child-ns1 is displayed as the selected namespace');
|
||||
|
||||
// Wait for data to be refetched and verify the KV secrets count has changed
|
||||
await waitFor('[data-test-vault-reporting-counter="KV secrets"]');
|
||||
assert
|
||||
.dom('[data-test-vault-reporting-counter="KV secrets"]')
|
||||
.includesText('250', 'KV secrets count updated to 250 after selecting child-ns1');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user