From 5ead15b8f29a989b29c9cba3d8c93cda7ce9b57f Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 10 Sep 2025 12:09:20 -0600 Subject: [PATCH] UI: Add month filtering to client count dashboard (#9148) (#9255) * delete activity component, convert date-formatters to ts * add "month" filter to overview tab * add test coverage for date range dropdown * add month filtering to client-list * remove old comment * wire up clients to route filters for client-list * adds changelog * only link to client-list for enterprise versions * add refresh page link * render all tabs, add custom empty state for secret sycn clients * cleanup unused service imports * revert billing periods as first of the month * first round of test updates * update client count utils test * fix comment typo * organize tests Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> --- changelog/_9148.txt | 3 + ui/app/components/clients/activity.ts | 35 -- ui/app/components/clients/date-range.ts | 21 +- ui/app/components/clients/filter-toolbar.hbs | 4 +- ui/app/components/clients/filter-toolbar.ts | 40 ++- ui/app/components/clients/page-header.hbs | 15 +- ui/app/components/clients/page-header.js | 12 +- .../components/clients/page/client-list.hbs | 44 ++- ui/app/components/clients/page/client-list.ts | 53 ++- ui/app/components/clients/page/counts.hbs | 6 + ui/app/components/clients/page/counts.ts | 4 - ui/app/components/clients/page/overview.hbs | 38 +- ui/app/components/clients/page/overview.ts | 48 ++- ui/app/components/clients/table.hbs | 7 + ui/app/components/clients/table.ts | 11 + .../vault/cluster/clients/counts.ts | 12 + ui/app/routes/vault/cluster/clients/counts.ts | 7 +- .../cluster/clients/counts/client-list.ts | 1 - .../cluster/clients/counts/client-list.hbs | 9 +- .../vault/cluster/clients/counts/overview.hbs | 6 +- ui/lib/core/addon/utils/client-count-utils.ts | 22 +- ui/lib/core/addon/utils/date-formatters.js | 31 -- ui/lib/core/addon/utils/date-formatters.ts | 64 ++++ ui/mirage/handlers/clients.js | 27 +- .../clients/counts/overview-test.js | 167 +++++---- .../helpers/clients/client-count-helpers.js | 327 +++++++++--------- .../helpers/clients/client-count-selectors.ts | 5 +- .../components/clients/date-range-test.js | 33 +- .../components/clients/filter-toolbar-test.js | 8 +- .../clients/page/client-list-test.js | 15 +- .../components/clients/page/counts-test.js | 8 +- .../components/clients/page/overview-test.js | 116 ++----- .../utils/client-count-utils-test.js | 60 ++-- .../integration/utils/date-formatters-test.js | 102 +++++- 34 files changed, 792 insertions(+), 569 deletions(-) create mode 100644 changelog/_9148.txt delete mode 100644 ui/app/components/clients/activity.ts delete mode 100644 ui/lib/core/addon/utils/date-formatters.js create mode 100644 ui/lib/core/addon/utils/date-formatters.ts diff --git a/changelog/_9148.txt b/changelog/_9148.txt new file mode 100644 index 0000000000..24bb9715fb --- /dev/null +++ b/changelog/_9148.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui/activity: Adds filtering by month to the Client Count dashboard to link client counts to specific client IDs from the export API +``` \ No newline at end of file diff --git a/ui/app/components/clients/activity.ts b/ui/app/components/clients/activity.ts deleted file mode 100644 index cc262d9a66..0000000000 --- a/ui/app/components/clients/activity.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -// base component for counts child routes that can be extended as needed -// contains getters that filter and extract data from activity model for use in charts - -import Component from '@glimmer/component'; -import { action } from '@ember/object'; - -import type ClientsActivityModel from 'vault/models/clients/activity'; -import type { ActivityExportData, ClientFilterTypes } from 'core/utils/client-count-utils'; - -/* This component does not actually render and is the base class to house - shared computations between the Clients::Page::Overview and Clients::Page::List components */ -export interface Args { - activity: ClientsActivityModel; - exportData: ActivityExportData[]; - onFilterChange: CallableFunction; - filterQueryParams: Record; -} - -export default class ClientsActivityComponent extends Component { - @action - handleFilter(filters: Record) { - const { namespace_path, mount_path, mount_type } = filters; - this.args.onFilterChange({ namespace_path, mount_path, mount_type }); - } - - @action - resetFilters() { - this.handleFilter({ namespace_path: '', mount_path: '', mount_type: '' }); - } -} diff --git a/ui/app/components/clients/date-range.ts b/ui/app/components/clients/date-range.ts index ad181cbdfc..06dcb7e317 100644 --- a/ui/app/components/clients/date-range.ts +++ b/ui/app/components/clients/date-range.ts @@ -7,9 +7,10 @@ import { action } from '@ember/object'; import { service } from '@ember/service'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { parseAPITimestamp } from 'core/utils/date-formatters'; +import { buildISOTimestamp, parseAPITimestamp } from 'core/utils/date-formatters'; import timestamp from 'core/utils/timestamp'; import { format } from 'date-fns'; + import type VersionService from 'vault/services/version'; import type { HTMLElementEvent } from 'forms'; @@ -67,16 +68,11 @@ export default class ClientsDateRangeComponent extends Component { const periods: string[] = []; for (let i = 1; i <= count; i++) { - const startDate = new Date(this.args.billingStartTime); - const utcMonth = startDate.getUTCMonth(); + const startDate = parseAPITimestamp(this.args.billingStartTime) as Date; const utcYear = startDate.getUTCFullYear() - i; - startDate.setUTCFullYear(utcYear); - startDate.setUTCMonth(utcMonth); - periods.push(startDate.toISOString()); } - return periods; } @@ -128,18 +124,11 @@ export default class ClientsDateRangeComponent extends Component { } // HELPERS - formatModalTimestamp(modalValue: string, isEnd: boolean) { + formatModalTimestamp(modalValue: string, isEndDate: boolean) { const [yearString, month] = modalValue.split('-'); const monthIdx = Number(month) - 1; const year = Number(yearString); - // day = 0 for Date.UTC(year, month, day) returns the last day of the previous month, - // which is why the monthIdx is increased by one for end dates. - // Date.UTC() also returns December if -1 is passed (which happens when January is selected) - const utc = isEnd - ? new Date(Date.UTC(year, monthIdx + 1, 0, 23, 59, 59)) - : new Date(Date.UTC(year, monthIdx, 1)); - - return utc.toISOString(); + return buildISOTimestamp({ monthIdx, year, isEndDate }); } setTrackedFromArgs() { diff --git a/ui/app/components/clients/filter-toolbar.hbs b/ui/app/components/clients/filter-toolbar.hbs index 514d58bd55..1a19fa521f 100644 --- a/ui/app/components/clients/filter-toolbar.hbs +++ b/ui/app/components/clients/filter-toolbar.hbs @@ -31,7 +31,7 @@ @selected={{eq item (get this filterProperty)}} data-test-dropdown-item={{item}} > - {{item}} + {{if (eq filterProperty "month") (this.formatTimestamp item) item}} {{else}} ; - // Dataset objects technically have more keys than the client filter types, but at minimum they contain ClientFilterTypes - dataset: Record[]; + dataset: ActivityExportData[] | MountClients[]; onFilter: CallableFunction; + dropdownMonths?: string[]; } // Correspond to each search input's tracked variable in the component class @@ -29,18 +35,21 @@ export default class ClientsFilterToolbar extends Component { @tracked namespace_path: string; @tracked mount_path: string; @tracked mount_type: string; + @tracked month: string; // Tracked search inputs @tracked namespacePathSearch = ''; @tracked mountPathSearch = ''; @tracked mountTypeSearch = ''; + @tracked monthSearch = ''; constructor(owner: unknown, args: Args) { super(owner, args); - const { namespace_path, mount_path, mount_type } = this.args.filterQueryParams; + const { namespace_path, mount_path, mount_type, month } = this.args.filterQueryParams; this.namespace_path = namespace_path || ''; this.mount_path = mount_path || ''; this.mount_type = mount_type || ''; + this.month = month || ''; } get anyFilters() { @@ -52,6 +61,7 @@ export default class ClientsFilterToolbar extends Component { const namespacePaths = new Set(); const mountPaths = new Set(); const mountTypes = new Set(); + const months = new Set(); // iterate over dataset once to get dropdown items this.args.dataset.forEach((d) => { @@ -60,12 +70,24 @@ export default class ClientsFilterToolbar extends Component { if (namespace) namespacePaths.add(namespace); if (d.mount_path) mountPaths.add(d.mount_path); if (d.mount_type) mountTypes.add(d.mount_type); + // `client_first_used_time` only exists for the dataset rendered in the "Client list" tab (ActivityExportData) + // the "Overview tab" manually passes an array of months + if ('client_first_used_time' in d && d.client_first_used_time) { + // for now, we're only concerned with month granularity so we want the dropdown filter to contain an ISO timestamp + // of the first of the month for each client_first_used_time + const date = parseAPITimestamp(d.client_first_used_time) as Date; + const year = date.getUTCFullYear(); + const monthIdx = date.getUTCMonth(); + const timestamp = buildISOTimestamp({ year, monthIdx, isEndDate: false }); + months.add(timestamp); + } }); return { [ClientFilters.NAMESPACE]: [...namespacePaths], [ClientFilters.MOUNT_PATH]: [...mountPaths], [ClientFilters.MOUNT_TYPE]: [...mountTypes], + [ClientFilters.MONTH]: this.args.dropdownMonths || [...months], }; } @@ -87,6 +109,11 @@ export default class ClientsFilterToolbar extends Component { dropdownItems: this.dropdownItems[ClientFilters.MOUNT_TYPE], searchProperty: 'mountTypeSearch', }, + [ClientFilters.MONTH]: { + label: 'month', + dropdownItems: this.dropdownItems[ClientFilters.MONTH], + searchProperty: 'monthSearch', + }, }; } @@ -98,6 +125,8 @@ export default class ClientsFilterToolbar extends Component { .flatMap((f: ClientFilters) => { const filterValue = this.filterProps[f]; const inDropdown = this.dropdownItems[f].includes(filterValue); + // Don't show an alert for the "Month" filter because it doesn't match dataset values one to one + if (ClientFilters.MONTH === f) return []; return !inDropdown && filterValue ? [alert(this.dropdownConfig[f].label, filterValue)] : []; }) .join(' '); @@ -137,6 +166,7 @@ export default class ClientsFilterToolbar extends Component { this.namespace_path = ''; this.mount_path = ''; this.mount_type = ''; + this.month = ''; } this.applyFilters(); } @@ -159,6 +189,8 @@ export default class ClientsFilterToolbar extends Component { } // TEMPLATE HELPERS + formatTimestamp = (isoTimestamp: string) => parseAPITimestamp(isoTimestamp, 'MMMM yyyy'); + searchDropdown = (dropdownItems: string[], searchProperty: SearchProperty) => { const searchInput = this[searchProperty]; return searchInput diff --git a/ui/app/components/clients/page-header.hbs b/ui/app/components/clients/page-header.hbs index 807a440dc1..a397adc7ce 100644 --- a/ui/app/components/clients/page-header.hbs +++ b/ui/app/components/clients/page-header.hbs @@ -10,8 +10,17 @@ {{#if @activityTimestamp}} - Last Updated: + Dashboard last updated: {{date-format @activityTimestamp "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}} + {{/if}} {{#if this.version.isEnterprise}} @@ -25,10 +34,6 @@ {{/if}} - This is the dashboard for your overall client count usages. Review Vault's - client counting documentation - for more information. - {{#if this.showCommunity}}
Client counting period diff --git a/ui/app/components/clients/page-header.js b/ui/app/components/clients/page-header.js index 5496989f2c..936b784029 100644 --- a/ui/app/components/clients/page-header.js +++ b/ui/app/components/clients/page-header.js @@ -33,6 +33,7 @@ import { task } from 'ember-concurrency'; export default class ClientsPageHeaderComponent extends Component { @service download; @service namespace; + @service router; @service store; @service version; @@ -120,9 +121,8 @@ export default class ClientsPageHeaderComponent extends Component { }); @action - setExportFormat(evt) { - const { value } = evt.target; - this.exportFormat = value; + refreshRoute() { + this.router.refresh(); } @action @@ -136,6 +136,12 @@ export default class ClientsPageHeaderComponent extends Component { this.showEditModal = visible; } + @action + setExportFormat(evt) { + const { value } = evt.target; + this.exportFormat = value; + } + // LOCAL TEMPLATE HELPERS parseAPITimestamp = (timestamp, format) => { return parseAPITimestamp(timestamp, format); diff --git a/ui/app/components/clients/page/client-list.hbs b/ui/app/components/clients/page/client-list.hbs index 4d2350124f..abf1af125e 100644 --- a/ui/app/components/clients/page/client-list.hbs +++ b/ui/app/components/clients/page/client-list.hbs @@ -3,6 +3,18 @@ SPDX-License-Identifier: BUSL-1.1 }} +The client list data below + comes from the + + Activity Export API + . It may take up to ten minutes for new client IDs to appear in the export data. + + <:subheader> {{tabName}}
- {{#if this.anyFilters}} + {{#if this.filtersAreApplied}} Summary: {{pluralize tableData.length "client"}} @@ -38,11 +50,33 @@ @setPageSize={{50}} @showPaginationSizeSelector={{true}} > + <:emptyState> - - - - + {{#if (and (eq tabName "Secret sync") (not this.flags.secretsSyncIsActivated))}} + + + + + + + + {{else}} + + + + + {{/if}} {{/if}} diff --git a/ui/app/components/clients/page/client-list.ts b/ui/app/components/clients/page/client-list.ts index a93d8a77b6..fec80ea72a 100644 --- a/ui/app/components/clients/page/client-list.ts +++ b/ui/app/components/clients/page/client-list.ts @@ -3,12 +3,18 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import ActivityComponent, { Args } from '../activity'; +import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { HTMLElementEvent } from 'vault/forms'; - -import { filterIsSupported, filterTableData, type ActivityExportData } from 'core/utils/client-count-utils'; +import { + filterIsSupported, + filterTableData, + type ClientFilterTypes, + type ActivityExportData, +} from 'core/utils/client-count-utils'; +import { service } from '@ember/service'; +import FlagsService from 'vault/services/flags'; // Define the base mapping to derive types from const CLIENT_TYPE_MAP = { @@ -21,24 +27,30 @@ const CLIENT_TYPE_MAP = { // Dynamically derive the tab values from the mapping type ClientListTabs = (typeof CLIENT_TYPE_MAP)[keyof typeof CLIENT_TYPE_MAP]; -export default class ClientsClientListPageComponent extends ActivityComponent { - @tracked selectedTab: ClientListTabs; - @tracked exportDataByTab; +export interface Args { + exportData: ActivityExportData[]; + onFilterChange: CallableFunction; + filterQueryParams: Record; +} + +export default class ClientsClientListPageComponent extends Component { + @service declare readonly flags: FlagsService; + + @tracked selectedTab: ClientListTabs = 'Entity'; + @tracked exportDataByTab: Record = { + Entity: [], + 'Non-entity': [], + ACME: [], + 'Secret sync': [], + }; constructor(owner: unknown, args: Args) { super(owner, args); - this.exportDataByTab = this.args.exportData.reduce( - (obj, data) => { - const clientLabel = CLIENT_TYPE_MAP[data.client_type]; - if (!obj[clientLabel]) { - obj[clientLabel] = []; - } - obj[clientLabel].push(data); - return obj; - }, - {} as Record - ); + this.args.exportData.forEach((data: ActivityExportData) => { + const tabName = CLIENT_TYPE_MAP[data.client_type]; + this.exportDataByTab[tabName].push(data); + }); const firstTab = Object.keys(this.exportDataByTab)[0] as ClientListTabs; this.selectedTab = firstTab; @@ -53,13 +65,18 @@ export default class ClientsClientListPageComponent extends ActivityComponent { return Object.keys(this.exportDataByTab) as ClientListTabs[]; } + @action + handleFilter(filters: Record) { + this.args.onFilterChange(filters); + } + @action onClickTab(_event: HTMLElementEvent, idx: number) { const tab = this.tabs[idx]; this.selectedTab = tab ?? this.tabs[0]!; } - get anyFilters() { + get filtersAreApplied() { return ( Object.keys(this.args.filterQueryParams).every((f) => filterIsSupported(f)) && Object.values(this.args.filterQueryParams).some((v) => !!v) diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index b38c9bd5a4..451031c2bb 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -16,6 +16,12 @@
+ + This is the dashboard for your overall client count usages. Review Vault's + + client counting documentation + for more information. + {{#if (eq @activity.id "no-data")}} diff --git a/ui/app/components/clients/page/counts.ts b/ui/app/components/clients/page/counts.ts index 0f7d449bf5..504b2e3c74 100644 --- a/ui/app/components/clients/page/counts.ts +++ b/ui/app/components/clients/page/counts.ts @@ -10,8 +10,6 @@ import { parseAPITimestamp } from 'core/utils/date-formatters'; import { filterVersionHistory } from 'core/utils/client-count-utils'; import type AdapterError from '@ember-data/adapter/error'; -import type FlagsService from 'vault/services/flags'; -import type Store from '@ember-data/store'; import type VersionService from 'vault/services/version'; import type ClientsActivityModel from 'vault/models/clients/activity'; import type ClientsConfigModel from 'vault/models/clients/config'; @@ -28,9 +26,7 @@ interface Args { } export default class ClientsCountsPageComponent extends Component { - @service declare readonly flags: FlagsService; @service declare readonly version: VersionService; - @service declare readonly store: Store; get formattedStartDate() { return this.args.startTimestamp ? parseAPITimestamp(this.args.startTimestamp, 'MMMM yyyy') : null; diff --git a/ui/app/components/clients/page/overview.hbs b/ui/app/components/clients/page/overview.hbs index f2dfdba552..cc8c491702 100644 --- a/ui/app/components/clients/page/overview.hbs +++ b/ui/app/components/clients/page/overview.hbs @@ -12,35 +12,15 @@ @description="Select a month to view the client count per mount for that month." > <:subheader> - - - - {{#each this.months as |m|}} - - {{/each}} - - - - {{#if this.selectedMonth}} -
- Use the filters - to view the clients attributed by path. - - -
- {{/if}} + Use the filters to + view the clients attributed by path. + + <:table> diff --git a/ui/app/components/clients/page/overview.ts b/ui/app/components/clients/page/overview.ts index b11035c309..0c58b9bdde 100644 --- a/ui/app/components/clients/page/overview.ts +++ b/ui/app/components/clients/page/overview.ts @@ -3,21 +3,20 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import ActivityComponent from '../activity'; -import { service } from '@ember/service'; +import Component from '@glimmer/component'; import { cached, tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -import { HTMLElementEvent } from 'vault/forms'; -import { parseAPITimestamp } from 'core/utils/date-formatters'; -import { filterTableData, flattenMounts } from 'core/utils/client-count-utils'; +import { filterTableData, flattenMounts, type ClientFilterTypes } from 'core/utils/client-count-utils'; -import type FlagsService from 'vault/services/flags'; -import type RouterService from '@ember/routing/router-service'; +import type ClientsActivityModel from 'vault/vault/models/clients/activity'; -export default class ClientsOverviewPageComponent extends ActivityComponent { - @service declare readonly flags: FlagsService; - @service('app-router') declare readonly router: RouterService; +export interface Args { + activity: ClientsActivityModel; + onFilterChange: CallableFunction; + filterQueryParams: Record; +} +export default class ClientsOverviewPageComponent extends Component { @tracked selectedMonth = ''; @cached @@ -25,13 +24,12 @@ export default class ClientsOverviewPageComponent extends ActivityComponent { return this.args.activity.byMonth?.map((m) => m?.new_clients) || []; } - @cached - // Supplies data passed to dropdown filters + // Supplies data passed to dropdown filters (except months which is computed below ) get activityData() { - // Find the namespace data for the selected month - // If no month is selected the table displays all of the activity for the queried date range - const namespaceData = this.selectedMonth - ? this.byMonthNewClients.find((m) => m.timestamp === this.selectedMonth)?.namespaces + // If no month is selected the table displays all of the activity for the queried date range. + const selectedMonth = this.args.filterQueryParams.month; + const namespaceData = selectedMonth + ? this.byMonthNewClients.find((m) => m.timestamp === selectedMonth)?.namespaces : this.args.activity.byNamespace; // Get the array of "mounts" data nested in each namespace object and flatten @@ -40,14 +38,16 @@ export default class ClientsOverviewPageComponent extends ActivityComponent { @cached get months() { - return this.byMonthNewClients - .reverse() - .map((m) => ({ timestamp: m.timestamp, display: parseAPITimestamp(m.timestamp, 'MMMM yyyy') })); + return this.byMonthNewClients.reverse().map((m) => m.timestamp); } get tableData() { if (this.activityData?.length) { - return filterTableData(this.activityData, this.args.filterQueryParams); + // Reset the `month` query param because it determines which dataset (see this.activityData) + // is passed to the table and is does not filter for key/value pairs within this dataset. + const filters = { ...this.args.filterQueryParams }; + filters.month = ''; + return filterTableData(this.activityData, filters); } return null; } @@ -62,11 +62,7 @@ export default class ClientsOverviewPageComponent extends ActivityComponent { } @action - selectMonth(e: HTMLElementEvent) { - this.selectedMonth = e.target.value; - // Reset filters when no month is selected - if (this.selectedMonth === '') { - this.resetFilters(); - } + handleFilter(filters: Record) { + this.args.onFilterChange(filters); } } diff --git a/ui/app/components/clients/table.hbs b/ui/app/components/clients/table.hbs index bef8bc7f34..989371682b 100644 --- a/ui/app/components/clients/table.hbs +++ b/ui/app/components/clients/table.hbs @@ -25,6 +25,13 @@ + {{else if (and (eq key "clients") this.version.isEnterprise)}} + + {{value}} + {{else}} {{! stringify value if it is an array or object, otherwise render directly }} diff --git a/ui/app/components/clients/table.ts b/ui/app/components/clients/table.ts index 5f4d72e43f..eee2afb9a8 100644 --- a/ui/app/components/clients/table.ts +++ b/ui/app/components/clients/table.ts @@ -9,6 +9,10 @@ import { cached, tracked } from '@glimmer/tracking'; import { paginate } from 'core/utils/paginate-list'; import { next } from '@ember/runloop'; +import type { ClientFilterTypes } from 'core/utils/client-count-utils'; +import { service } from '@ember/service'; +import VersionService from 'vault/services/version'; + /** * @module ClientsTable * ClientsTable renders a paginated table for a passed dataset. HDS table components handle basic sorting @@ -48,6 +52,8 @@ interface Args { } export default class ClientsTable extends Component { + @service declare readonly version: VersionService; + @tracked currentPage = 1; @tracked pageSize = 5; // Can be overridden by @setPageSize @tracked sortColumn = ''; @@ -132,4 +138,9 @@ export default class ClientsTable extends Component { // TEMPLATE HELPERS isObject = (value: any) => typeof value === 'object'; + + generateQueryParams = (datum: Record) => { + const { namespace_path = '', mount_path = '', mount_type = '' } = datum; + return { namespace_path, mount_path, mount_type }; + }; } diff --git a/ui/app/controllers/vault/cluster/clients/counts.ts b/ui/app/controllers/vault/cluster/clients/counts.ts index 40af25c27e..791ee95501 100644 --- a/ui/app/controllers/vault/cluster/clients/counts.ts +++ b/ui/app/controllers/vault/cluster/clients/counts.ts @@ -5,6 +5,7 @@ import Controller from '@ember/controller'; import { action, set } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; import { ClientFilters } from 'core/utils/client-count-utils'; import type { ClientsCountsRouteParams } from 'vault/routes/vault/cluster/clients/counts'; @@ -22,6 +23,17 @@ export default class ClientsCountsController extends Controller { namespace_path = ''; mount_path = ''; mount_type = ''; + // Tracked because clients/page/overview.ts has a getter that needs to recompute when this changes + @tracked month = ''; + + get filterQueryParams() { + return { + namespace_path: this.namespace_path, + mount_path: this.mount_path, + mount_type: this.mount_type, + month: this.month, + }; + } // using router.transitionTo to update the query params results in the model hook firing each time // this happens when the queryParams object is not added to the route or refreshModel is explicitly set to false diff --git a/ui/app/routes/vault/cluster/clients/counts.ts b/ui/app/routes/vault/cluster/clients/counts.ts index ac93d5f1c0..858a9cd1a2 100644 --- a/ui/app/routes/vault/cluster/clients/counts.ts +++ b/ui/app/routes/vault/cluster/clients/counts.ts @@ -21,6 +21,7 @@ export interface ClientsCountsRouteParams { namespace_path?: string; mount_path?: string; mount_type?: string; + month?: string; } interface ActivityAdapterQuery { @@ -44,6 +45,7 @@ export default class ClientsCountsRoute extends Route { namespace_path: { refreshModel: false, replace: true }, mount_path: { refreshModel: false, replace: true }, mount_type: { refreshModel: false, replace: true }, + month: { refreshModel: false, replace: true }, }; beforeModel() { @@ -92,11 +94,12 @@ export default class ClientsCountsRoute extends Route { resetController(controller: ClientsCountsController, isExiting: boolean) { if (isExiting) { controller.setProperties({ - start_time: undefined, - end_time: undefined, + start_time: '', + end_time: '', namespace_path: '', mount_path: '', mount_type: '', + month: '', }); } } diff --git a/ui/app/routes/vault/cluster/clients/counts/client-list.ts b/ui/app/routes/vault/cluster/clients/counts/client-list.ts index c769d556cd..01a2d69a37 100644 --- a/ui/app/routes/vault/cluster/clients/counts/client-list.ts +++ b/ui/app/routes/vault/cluster/clients/counts/client-list.ts @@ -13,7 +13,6 @@ import type Store from '@ember-data/store'; export default class ClientsCountsClientListRoute extends Route { @service declare readonly store: Store; - // TODO - will there always be a start/end timestamp? What's the error scenario async fetchAndFormatExportData(startTimestamp: string | undefined, endTimestamp: string | undefined) { const adapter = this.store.adapterFor('clients/activity'); let exportData, exportError; diff --git a/ui/app/templates/vault/cluster/clients/counts/client-list.hbs b/ui/app/templates/vault/cluster/clients/counts/client-list.hbs index ba299c5a23..31b0ee9f6a 100644 --- a/ui/app/templates/vault/cluster/clients/counts/client-list.hbs +++ b/ui/app/templates/vault/cluster/clients/counts/client-list.hbs @@ -18,7 +18,8 @@ @icon="docs-link" @iconPosition="trailing" @text="Client Export Documentation" - @href={{doc-link "/vault/api-docs/secret/databases"}} + @isHrefExternal={{true}} + @href={{doc-link "/vault/api-docs/system/internal-counters#activity-export"}} /> {{/if}} @@ -28,10 +29,6 @@ {{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/clients/counts/overview.hbs b/ui/app/templates/vault/cluster/clients/counts/overview.hbs index b8ac4fccc2..c3d95eeec6 100644 --- a/ui/app/templates/vault/cluster/clients/counts/overview.hbs +++ b/ui/app/templates/vault/cluster/clients/counts/overview.hbs @@ -6,9 +6,5 @@ \ No newline at end of file diff --git a/ui/lib/core/addon/utils/client-count-utils.ts b/ui/lib/core/addon/utils/client-count-utils.ts index 276e68788b..33e2372044 100644 --- a/ui/lib/core/addon/utils/client-count-utils.ts +++ b/ui/lib/core/addon/utils/client-count-utils.ts @@ -3,8 +3,10 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { parseAPITimestamp } from 'core/utils/date-formatters'; +import { isSameMonthUTC, parseAPITimestamp } from 'core/utils/date-formatters'; import { compareAsc, isWithinInterval } from 'date-fns'; +import { ROOT_NAMESPACE } from 'vault/services/namespace'; +import { sanitizePath } from './sanitize-path'; import type ClientsVersionHistoryModel from 'vault/vault/models/clients/version-history'; @@ -31,6 +33,8 @@ export enum ClientFilters { NAMESPACE = 'namespace_path', MOUNT_PATH = 'mount_path', MOUNT_TYPE = 'mount_type', + // this filter/query param does not map to a key in either API response and is handled ~special~ + MONTH = 'month', } export type ClientFilterTypes = (typeof ClientFilters)[keyof typeof ClientFilters]; @@ -126,7 +130,8 @@ export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByN label: m.mount_path, namespace_path: nsLabel, mount_path: m.mount_path, - mount_type: m.mount_type, + // sanitized so it matches activity export data because mount_type there does NOT have a trailing slash + mount_type: sanitizePath(m.mount_type), ...destructureClientCounts(m.counts), })); } @@ -185,17 +190,24 @@ export function filterTableData( } const matchesFilter = ( - datum: MountClients | ActivityExportData, + datum: ActivityExportData | MountClients, filterKey: ClientFilterTypes, filterValue: string ) => { + // Only ActivityExportData data is ever filtered by 'client_first_used_time' (not MountClients) + if (filterKey === ClientFilters.MONTH) { + return 'client_first_used_time' in datum + ? isSameMonthUTC(datum.client_first_used_time, filterValue) + : false; + } + const datumValue = datum[filterKey]; // The API returns and empty string as the namespace_path for the "root" namespace. // When a user selects "root" as a namespace filter we need to match the datum value // as either an empty string (for the activity export data) OR as "root" // (the by_namespace data is serialized to make "root" the namespace_path). - if (filterKey === 'namespace_path' && filterValue === 'root') { - return datumValue === '' || datumValue === filterValue; + if (filterKey === ClientFilters.NAMESPACE && filterValue === 'root') { + return datumValue === ROOT_NAMESPACE || datumValue === filterValue; } return datumValue === filterValue; }; diff --git a/ui/lib/core/addon/utils/date-formatters.js b/ui/lib/core/addon/utils/date-formatters.js deleted file mode 100644 index 54544ad585..0000000000 --- a/ui/lib/core/addon/utils/date-formatters.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { format, parseISO } from 'date-fns'; - -export const datetimeLocalStringFormat = "yyyy-MM-dd'T'HH:mm"; - -export const ARRAY_OF_MONTHS = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', -]; - -// convert API timestamp ( '2021-03-21T00:00:00Z' ) to date object, optionally format -export const parseAPITimestamp = (timestamp, style) => { - if (typeof timestamp !== 'string') return timestamp; - const date = parseISO(timestamp.split('T')[0]); - if (!style) return date; - return format(date, style); -}; diff --git a/ui/lib/core/addon/utils/date-formatters.ts b/ui/lib/core/addon/utils/date-formatters.ts new file mode 100644 index 0000000000..3b6607e140 --- /dev/null +++ b/ui/lib/core/addon/utils/date-formatters.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { format, parse, parseISO } from 'date-fns'; +import isValid from 'date-fns/isValid'; + +export const datetimeLocalStringFormat = "yyyy-MM-dd'T'HH:mm"; + +export const ARRAY_OF_MONTHS = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; + +// convert API timestamp ( '2021-03-21T00:00:00Z' ) to date object, optionally format +export const parseAPITimestamp = (timestamp: string, style?: string): Date | string | null => { + if (!timestamp || typeof timestamp !== 'string') return null; + + if (!style) { + // If no style, return a date object in UTC + const parsed = parseISO(timestamp) as Date; + return isValid(parsed) ? parsed : null; + } + + // Otherwise format it as a calendar date that is timezone agnostic. + const yearMonthDay = timestamp.split('T')[0] ?? ''; + const date = parse(yearMonthDay, 'yyyy-MM-dd', new Date()); // 'yyyy-MM-dd' lets parse() know the format of yearMonthDay + return format(date, style) as string; +}; + +export const buildISOTimestamp = (args: { monthIdx: number; year: number; isEndDate: boolean }) => { + const { monthIdx, year, isEndDate } = args; + // passing `0` for the "day" arg to Date.UTC() returns the last day of the previous month + // which is why the monthIdx is increased by one for end dates. + // Date.UTC() also returns December if -1 is passed (which happens when January is selected) + const utc = isEndDate + ? new Date(Date.UTC(year, monthIdx + 1, 0, 23, 59, 59)) + : new Date(Date.UTC(year, monthIdx, 1)); + + // remove milliseconds to return a UTC timestamp that matches the API + // e.g. "2025-05-01T00:00:00Z" or "2025-09-30T23:59:59Z" + return utc.toISOString().replace('.000', ''); +}; + +export const isSameMonthUTC = (timestampA: string, timestampB: string): boolean => { + const dateA = parseAPITimestamp(timestampA) as Date; + const dateB = parseAPITimestamp(timestampB) as Date; + if (isValid(dateA) && isValid(dateB)) { + // Compare in UTC as any date-fns comparisons will be in localized timezones! + return dateA.getUTCFullYear() === dateB.getUTCFullYear() && dateA.getUTCMonth() === dateB.getUTCMonth(); + } + return false; +}; diff --git a/ui/mirage/handlers/clients.js b/ui/mirage/handlers/clients.js index d0ec77b1e8..4e8a73be94 100644 --- a/ui/mirage/handlers/clients.js +++ b/ui/mirage/handlers/clients.js @@ -5,7 +5,6 @@ import { addMonths, - differenceInCalendarMonths, endOfMonth, formatRFC3339, fromUnixTime, @@ -13,7 +12,6 @@ import { isBefore, isSameMonth, isWithinInterval, - startOfMonth, subMonths, } from 'date-fns'; import { parseAPITimestamp } from 'core/utils/date-formatters'; @@ -30,7 +28,7 @@ export const STATIC_NOW = new Date('2024-01-25T23:59:59Z'); export const STATIC_PREVIOUS_MONTH = new Date('2023-12-25T23:59:59Z'); const COUNTS_START = subMonths(STATIC_NOW, 12); // user started Vault cluster on 2023-01-25 // upgrade happened 2 month after license start -export const UPGRADE_DATE = addMonths(LICENSE_START, 2); // monthly attribution added +export const UPGRADE_DATE = new Date('2023-09-01T00:00:00Z'); // monthly attribution added // exported so that tests not using this scenario can use the same response export const CONFIG_RESPONSE = { @@ -67,7 +65,8 @@ function generateMountBlock(path, counts) { return obj; }, {}); // this logic is random nonsense just to have some mounts be "deleted" - const setMountType = () => (counts.clients % 5 <= 1 ? 'deleted mount' : path.split('/')[1]); + // "mount_type" from /sys/internal/counters/activity ends in a trailing slash (but in the export activity data it does not) + const setMountType = () => (counts.clients % 5 <= 1 ? 'deleted mount' : `${path.split('/')[1]}/`); return { mount_path: path, mount_type: setMountType(), @@ -100,13 +99,13 @@ function generateNamespaceBlock(idx = 0, isLowerCounts = false, ns, skipCounts = // each mount type generates a different type of client return [ - generateMountBlock(`auth/token/${idx}`, { + generateMountBlock(`auth/token/${idx}/`, { clients: non_entity_clients + entity_clients, non_entity_clients, entity_clients, }), - generateMountBlock(`secrets/kv/${idx}`, { clients: secret_syncs, secret_syncs }), - generateMountBlock(`acme/pki/${idx}`, { clients: acme_clients, acme_clients }), + generateMountBlock(`secrets/kv/${idx}/`, { clients: secret_syncs, secret_syncs }), + generateMountBlock(`acme/pki/${idx}/`, { clients: acme_clients, acme_clients }), ]; }; @@ -122,19 +121,23 @@ function generateNamespaceBlock(idx = 0, isLowerCounts = false, ns, skipCounts = function generateMonths(startDate, endDate, namespaces) { const startDateObject = parseAPITimestamp(startDate); const endDateObject = parseAPITimestamp(endDate); - const numberOfMonths = differenceInCalendarMonths(endDateObject, startDateObject) + 1; + const startMonth = startDateObject.getUTCMonth() + startDateObject.getUTCFullYear() * 12; + const endMonth = endDateObject.getUTCMonth() + endDateObject.getUTCFullYear() * 12; + const numberOfMonths = endMonth - startMonth + 1; const months = []; // only generate monthly block if queried dates span or follow upgrade to 1.10 const upgradeWithin = isWithinInterval(UPGRADE_DATE, { start: startDateObject, end: endDateObject }); const upgradeAfter = isAfter(startDateObject, UPGRADE_DATE); + if (upgradeWithin || upgradeAfter) { for (let i = 0; i < numberOfMonths; i++) { - const month = addMonths(startOfMonth(startDateObject), i); + const month = new Date(Date.UTC(startDateObject.getUTCFullYear(), startDateObject.getUTCMonth() + i)); + const hasNoData = isBefore(month, UPGRADE_DATE) && !isSameMonth(month, UPGRADE_DATE); if (hasNoData) { months.push({ - timestamp: formatRFC3339(month), + timestamp: month.toISOString(), counts: null, namespaces: null, new_clients: null, @@ -145,7 +148,7 @@ function generateMonths(startDate, endDate, namespaces) { const monthNs = namespaces.map((ns, idx) => generateNamespaceBlock(idx, false, ns)); const newClients = namespaces.map((ns, idx) => generateNamespaceBlock(idx, true, ns)); months.push({ - timestamp: formatRFC3339(month), + timestamp: month.toISOString(), counts: getTotalCounts(monthNs), namespaces: monthNs.sort((a, b) => b.counts.clients - a.counts.clients), new_clients: { @@ -155,7 +158,6 @@ function generateMonths(startDate, endDate, namespaces) { }); } } - return months; } @@ -295,6 +297,7 @@ export default function (server) { const activities = schema['clients/activities']; const namespace = req.requestHeaders['X-Vault-Namespace']; let { start_time, end_time } = req.queryParams; + if (!start_time) { // if there are no date query params, the activity log default behavior // queries from the builtin license start timestamp to the current month diff --git a/ui/tests/acceptance/clients/counts/overview-test.js b/ui/tests/acceptance/clients/counts/overview-test.js index 6c3b7bab3f..a6a0727d7d 100644 --- a/ui/tests/acceptance/clients/counts/overview-test.js +++ b/ui/tests/acceptance/clients/counts/overview-test.js @@ -19,9 +19,12 @@ import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { CHARTS, CLIENT_COUNT, FILTERS } from 'vault/tests/helpers/clients/client-count-selectors'; import timestamp from 'core/utils/timestamp'; -import { format } from 'date-fns'; -import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers'; +import { + ACTIVITY_EXPORT_STUB, + ACTIVITY_RESPONSE_STUB, +} from 'vault/tests/helpers/clients/client-count-helpers'; import { ClientFilters, flattenMounts } from 'core/utils/client-count-utils'; +import { parseAPITimestamp } from 'core/utils/date-formatters'; module('Acceptance | clients | overview', function (hooks) { setupApplicationTest(hooks); @@ -29,8 +32,10 @@ module('Acceptance | clients | overview', function (hooks) { hooks.beforeEach(async function () { sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); + // These tests use the clientsHandler which dynamically generates activity data, used for asserting date querying, etc clientsHandler(this.server); this.store = this.owner.lookup('service:store'); + this.version = this.owner.lookup('service:version'); }); test('it should hide secrets sync stats when feature is NOT on license', async function (assert) { @@ -45,59 +50,45 @@ module('Acceptance | clients | overview', function (hooks) { assert.dom(CHARTS.legend).hasText('Entity clients Non-entity clients ACME clients'); }); - // These tests use the clientsHandler which dynamically generates activity data, used for asserting date querying, etc - module('dynamic data', function (hooks) { + test('it should render charts', async function (assert) { + await login(); + await visit('/vault/clients/counts/overview'); + assert + .dom(`${GENERAL.flashMessage}.is-info`) + .includesText( + 'counts returned in this usage period are an estimate', + 'Shows warning from API about client count estimations' + ); + assert + .dom(CLIENT_COUNT.dateRange.dateDisplay('start')) + .hasText('July 2023', 'start month is correctly parsed from license'); + assert + .dom(CLIENT_COUNT.dateRange.dateDisplay('end')) + .hasText('January 2024', 'end month is correctly parsed from STATIC_NOW'); + assert + .dom(CLIENT_COUNT.card('Client usage trends for selected billing period')) + .exists('Shows running totals with monthly breakdown charts'); + assert + .dom(`${CLIENT_COUNT.card('Client usage trends for selected billing period')} ${CHARTS.xAxisLabel}`) + .hasText('7/23', 'x-axis labels start with billing start date'); + assert.dom(CHARTS.xAxisLabel).exists({ count: 7 }, 'chart months matches query'); + }); + + module('community', function (hooks) { hooks.beforeEach(async function () { - // stub secrets sync being activated - this.server.get('/sys/activation-flags', function () { - return { - data: { - activated: ['secrets-sync'], - unactivated: [], - }, - }; - }); - - this.activity = await this.store.findRecord('clients/activity', 'some-activity-id'); - this.mostRecentMonth = this.activity.byMonth[this.activity.byMonth.length - 1]; + this.version.type = 'community'; await login(); - return visit('/vault/clients/counts/overview'); - }); - - test('it should render charts', async function (assert) { - assert - .dom(`${GENERAL.flashMessage}.is-info`) - .includesText( - 'counts returned in this usage period are an estimate', - 'Shows warning from API about client count estimations' - ); - assert - .dom(CLIENT_COUNT.dateRange.dateDisplay('start')) - .hasText('July 2023', 'billing start month is correctly parsed from license'); - assert - .dom(CLIENT_COUNT.dateRange.dateDisplay('end')) - .hasText('January 2024', 'billing start month is correctly parsed from license'); - assert - .dom(CLIENT_COUNT.card('Client usage trends for selected billing period')) - .exists('Shows running totals with monthly breakdown charts'); - assert - .dom(`${CLIENT_COUNT.card('Client usage trends for selected billing period')} ${CHARTS.xAxisLabel}`) - .hasText('7/23', 'x-axis labels start with billing start date'); - assert.dom(CHARTS.xAxisLabel).exists({ count: 7 }, 'chart months matches query'); + return await visit('/vault/clients/counts/overview'); }); test('it should update charts when querying date ranges', async function (assert) { - // query for single, historical month with no new counts (July 2023), which means there is no monthly breakdown - const service = this.owner.lookup('service:version'); - service.type = 'community'; - - const licenseStartMonth = format(LICENSE_START, 'yyyy-MM'); - const upgradeMonth = format(UPGRADE_DATE, 'yyyy-MM'); - const endMonth = format(STATIC_PREVIOUS_MONTH, 'yyyy-MM'); + // Use parseAPITimestamp because we want a date string that is timezone agnostic (so it stays in UTC) + const clientCountingStartDate = parseAPITimestamp(LICENSE_START.toISOString(), 'yyyy-MM'); + const upgradeMonth = parseAPITimestamp(UPGRADE_DATE.toISOString(), 'yyyy-MM'); + const endMonth = parseAPITimestamp(STATIC_PREVIOUS_MONTH.toISOString(), 'yyyy-MM'); await click(CLIENT_COUNT.dateRange.edit); - await fillIn(CLIENT_COUNT.dateRange.editDate('start'), licenseStartMonth); - await fillIn(CLIENT_COUNT.dateRange.editDate('end'), licenseStartMonth); - + await fillIn(CLIENT_COUNT.dateRange.editDate('start'), clientCountingStartDate); + await fillIn(CLIENT_COUNT.dateRange.editDate('end'), clientCountingStartDate); await click(GENERAL.submitButton); assert .dom(CLIENT_COUNT.usageStats('Client usage')) @@ -111,9 +102,10 @@ module('Acceptance | clients | overview', function (hooks) { await fillIn(CLIENT_COUNT.dateRange.editDate('start'), upgradeMonth); await fillIn(CLIENT_COUNT.dateRange.editDate('end'), endMonth); await click(GENERAL.submitButton); + assert .dom(CLIENT_COUNT.dateRange.dateDisplay('start')) - .hasText('September 2023', 'billing start month is correctly parsed from license'); + .hasText('September 2023', 'client count start month is correctly parsed from start query'); assert .dom(CLIENT_COUNT.card('Client usage trends for selected billing period')) .exists('Shows running totals with monthly breakdown charts'); @@ -139,10 +131,10 @@ module('Acceptance | clients | overview', function (hooks) { assert .dom(CLIENT_COUNT.dateRange.dateDisplay('start')) - .hasText('September 2023', 'billing start month is correctly parsed from license'); + .hasText('September 2023', 'it displays correct start time'); assert .dom(CLIENT_COUNT.dateRange.dateDisplay('end')) - .hasText('December 2023', 'billing start month is correctly parsed from license'); + .hasText('December 2023', 'it displays correct end time'); assert .dom(CLIENT_COUNT.card('Client usage trends for selected billing period')) .exists('Shows running totals with monthly breakdown charts'); @@ -153,6 +145,12 @@ module('Acceptance | clients | overview', function (hooks) { .dom(xAxisLabels[xAxisLabels.length - 1]) .hasText('12/23', 'x-axis labels end with queried end month'); }); + + test('it does not render client list links for community versions', async function (assert) { + assert + .dom(`${GENERAL.tableData(0, 'clients')} a`) + .doesNotExist('client counts do not render as hyperlinks'); + }); }); // * FILTERING ASSERTIONS @@ -176,9 +174,11 @@ module('Acceptance | clients | overview', function (hooks) { test('it filters attribution table when filters are applied', async function (assert) { const url = '/vault/clients/counts/overview'; const topMount = flattenMounts(this.staticMostRecentMonth.new_clients.namespaces)[0]; + const timestamp = this.staticMostRecentMonth.timestamp; const { namespace_path, mount_type, mount_path } = topMount; assert.strictEqual(currentURL(), url, 'URL does not contain query params'); - await fillIn(GENERAL.selectByAttr('attribution-month'), this.staticMostRecentMonth.timestamp); + await click(FILTERS.dropdownToggle(ClientFilters.MONTH)); + await click(FILTERS.dropdownItem(timestamp)); await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE)); await click(FILTERS.dropdownItem(namespace_path)); await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH)); @@ -187,12 +187,12 @@ module('Acceptance | clients | overview', function (hooks) { await click(FILTERS.dropdownItem(mount_type)); assert.strictEqual( currentURL(), - `${url}?mount_path=${encodeURIComponent( + `${url}?month=${encodeURIComponent(timestamp)}&mount_path=${encodeURIComponent( mount_path )}&mount_type=${mount_type}&namespace_path=${namespace_path}`, 'url query params match filters' ); - assert.dom(FILTERS.tag()).exists({ count: 3 }, '3 filter tags render'); + assert.dom(FILTERS.tag()).exists({ count: 4 }, '4 filter tags render'); assert.dom(GENERAL.tableRow()).exists({ count: 1 }, 'it only renders the filtered table row'); assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText(namespace_path); assert.dom(GENERAL.tableData(0, 'mount_type')).hasText(mount_type); @@ -202,8 +202,10 @@ module('Acceptance | clients | overview', function (hooks) { test('it updates table when filters are cleared', async function (assert) { const url = '/vault/clients/counts/overview'; const mounts = flattenMounts(this.staticMostRecentMonth.new_clients.namespaces); + const timestamp = this.staticMostRecentMonth.timestamp; const { namespace_path, mount_type, mount_path } = mounts[0]; - await fillIn(GENERAL.selectByAttr('attribution-month'), this.staticMostRecentMonth.timestamp); + await click(FILTERS.dropdownToggle(ClientFilters.MONTH)); + await click(FILTERS.dropdownItem(timestamp)); await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE)); await click(FILTERS.dropdownItem(namespace_path)); await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH)); @@ -214,13 +216,15 @@ module('Acceptance | clients | overview', function (hooks) { await click(FILTERS.clearTag(namespace_path)); assert.strictEqual( currentURL(), - `${url}?mount_path=${encodeURIComponent(mount_path)}&mount_type=${mount_type}`, + `${url}?month=${encodeURIComponent(timestamp)}&mount_path=${encodeURIComponent( + mount_path + )}&mount_type=${mount_type}`, 'url does not have namespace_path query param' ); assert.dom(GENERAL.tableRow()).exists({ count: 2 }, 'it renders 2 data rows that match filters'); assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText('root'); assert.dom(GENERAL.tableData(0, 'mount_type')).hasText(mount_type); - assert.dom(GENERAL.tableData(1, 'namespace_path')).hasText('ns1'); + assert.dom(GENERAL.tableData(1, 'namespace_path')).hasText('ns1/'); assert.dom(GENERAL.tableData(1, 'mount_type')).hasText(mount_type); assert.dom(GENERAL.tableData(1, 'mount_path')).hasText(mount_path); await click(GENERAL.button('Clear filters')); @@ -230,11 +234,13 @@ module('Acceptance | clients | overview', function (hooks) { .exists({ count: mounts.length }, 'it renders all data when filters are cleared'); }); - test('it clears query params when month is unselected', async function (assert) { + test('it renders client counts for full billing period when month is unselected', async function (assert) { const url = '/vault/clients/counts/overview'; const mounts = flattenMounts(this.staticMostRecentMonth.new_clients.namespaces); + const timestamp = this.staticMostRecentMonth.timestamp; const { namespace_path, mount_type, mount_path } = mounts[0]; - await fillIn(GENERAL.selectByAttr('attribution-month'), this.staticMostRecentMonth.timestamp); + await click(FILTERS.dropdownToggle(ClientFilters.MONTH)); + await click(FILTERS.dropdownItem(timestamp)); await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE)); await click(FILTERS.dropdownItem(namespace_path)); await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH)); @@ -243,13 +249,48 @@ module('Acceptance | clients | overview', function (hooks) { await click(FILTERS.dropdownItem(mount_type)); assert.strictEqual( currentURL(), - `${url}?mount_path=${encodeURIComponent( + `${url}?month=${encodeURIComponent(timestamp)}&mount_path=${encodeURIComponent( mount_path )}&mount_type=${mount_type}&namespace_path=${namespace_path}`, 'url query params match filters' ); - await fillIn(GENERAL.selectByAttr('attribution-month'), ''); - assert.strictEqual(currentURL(), url, 'url query params clear when month is not selected'); + await click(FILTERS.clearTag('September 2023')); + assert + .dom(GENERAL.tableData(0, 'clients')) + .hasText('4003', 'the table renders clients for the full billing period (not September)'); + assert.strictEqual( + currentURL(), + `${url}?mount_path=${encodeURIComponent( + mount_path + )}&mount_type=${mount_type}&namespace_path=${namespace_path}`, + 'url does not include month' + ); + }); + + test('enterprise: it navigates to the client list page when clicking the client count hyperlink', async function (assert) { + const mockResponse = { + status: 200, + ok: true, + text: () => Promise.resolve(ACTIVITY_EXPORT_STUB.trim()), + }; + const adapter = this.store.adapterFor('clients/activity'); + const exportDataStub = sinon.stub(adapter, 'exportData'); + exportDataStub.resolves(mockResponse); + const timestamp = this.staticMostRecentMonth.timestamp; + await click(FILTERS.dropdownToggle(ClientFilters.MONTH)); + await click(FILTERS.dropdownItem(timestamp)); + await click(`${GENERAL.tableData(0, 'clients')} a`); + const url = '/vault/clients/counts/client-list'; + const monthQp = encodeURIComponent(timestamp); + const ns = encodeURIComponent('ns1/'); + const mPath = encodeURIComponent('auth/userpass/0/'); + const mType = 'userpass'; + assert.strictEqual( + currentURL(), + `${url}?month=${monthQp}&mount_path=${mPath}&mount_type=${mType}&namespace_path=${ns}`, + 'url query params match filters' + ); + exportDataStub.restore(); }); }); diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js index 9bb2941957..f85b0e47c6 100644 --- a/ui/tests/helpers/clients/client-count-helpers.js +++ b/ui/tests/helpers/clients/client-count-helpers.js @@ -35,7 +35,7 @@ export const ACTIVITY_RESPONSE_STUB = { by_namespace: [ { namespace_id: 'e67m31', - namespace_path: 'ns1', + namespace_path: 'ns1/', counts: { acme_clients: 5699, clients: 18903, @@ -45,8 +45,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, mounts: [ { - mount_path: 'auth/userpass/0', - mount_type: 'userpass', + mount_path: 'auth/userpass/0/', + mount_type: 'userpass/', counts: { acme_clients: 0, clients: 8394, @@ -56,8 +56,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'acme/pki/0', - mount_type: 'pki', + mount_path: 'acme/pki/0/', + mount_type: 'pki/', counts: { acme_clients: 5699, clients: 5699, @@ -67,8 +67,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'secrets/kv/0', - mount_type: 'kv', + mount_path: 'secrets/kv/0/', + mount_type: 'kv/', counts: { acme_clients: 0, clients: 4810, @@ -91,8 +91,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, mounts: [ { - mount_path: 'auth/userpass/0', - mount_type: 'userpass', + mount_path: 'auth/userpass/0/', + mount_type: 'userpass/', counts: { acme_clients: 0, clients: 8091, @@ -102,8 +102,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'secrets/kv/0', - mount_type: 'kv', + mount_path: 'secrets/kv/0/', + mount_type: 'kv/', counts: { acme_clients: 0, clients: 4290, @@ -113,8 +113,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'acme/pki/0', - mount_type: 'pki', + mount_path: 'acme/pki/0/', + mount_type: 'pki/', counts: { acme_clients: 4003, clients: 4003, @@ -155,8 +155,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, mounts: [ { - mount_path: 'auth/userpass/0', - mount_type: 'userpass', + mount_path: 'auth/userpass/0/', + mount_type: 'userpass/', counts: { acme_clients: 0, clients: 200, @@ -166,8 +166,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'acme/pki/0', - mount_type: 'pki', + mount_path: 'acme/pki/0/', + mount_type: 'pki/', counts: { acme_clients: 100, clients: 100, @@ -177,8 +177,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'secrets/kv/0', - mount_type: 'kv', + mount_path: 'secrets/kv/0/', + mount_type: 'kv/', counts: { acme_clients: 0, clients: 100, @@ -211,8 +211,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, mounts: [ { - mount_path: 'auth/userpass/0', - mount_type: 'userpass', + mount_path: 'auth/userpass/0/', + mount_type: 'userpass/', counts: { acme_clients: 0, clients: 200, @@ -222,8 +222,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'acme/pki/0', - mount_type: 'pki', + mount_path: 'acme/pki/0/', + mount_type: 'pki/', counts: { acme_clients: 100, clients: 100, @@ -233,8 +233,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'secrets/kv/0', - mount_type: 'kv', + mount_path: 'secrets/kv/0/', + mount_type: 'kv/', counts: { acme_clients: 0, clients: 100, @@ -270,8 +270,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, mounts: [ { - mount_path: 'auth/userpass/0', - mount_type: 'userpass', + mount_path: 'auth/userpass/0/', + mount_type: 'userpass/', counts: { acme_clients: 0, clients: 200, @@ -281,8 +281,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'acme/pki/0', - mount_type: 'pki', + mount_path: 'acme/pki/0/', + mount_type: 'pki/', counts: { acme_clients: 100, clients: 100, @@ -292,8 +292,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'secrets/kv/0', - mount_type: 'kv', + mount_path: 'secrets/kv/0/', + mount_type: 'kv/', counts: { acme_clients: 0, clients: 100, @@ -322,7 +322,7 @@ export const ACTIVITY_RESPONSE_STUB = { namespaces: [ { namespace_id: 'e67m31', - namespace_path: 'ns1', + namespace_path: 'ns1/', counts: { acme_clients: 934, clients: 1981, @@ -332,7 +332,7 @@ export const ACTIVITY_RESPONSE_STUB = { }, mounts: [ { - mount_path: 'acme/pki/0', + mount_path: 'acme/pki/0/', counts: { acme_clients: 934, clients: 934, @@ -342,7 +342,7 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'auth/userpass/0', + mount_path: 'auth/userpass/0/', counts: { acme_clients: 0, clients: 890, @@ -352,7 +352,7 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'secrets/kv/0', + mount_path: 'secrets/kv/0/', counts: { acme_clients: 0, clients: 157, @@ -375,7 +375,7 @@ export const ACTIVITY_RESPONSE_STUB = { }, mounts: [ { - mount_path: 'acme/pki/0', + mount_path: 'acme/pki/0/', counts: { acme_clients: 994, clients: 994, @@ -385,7 +385,7 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'auth/userpass/0', + mount_path: 'auth/userpass/0/', counts: { acme_clients: 0, clients: 872, @@ -395,7 +395,7 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'secrets/kv/0', + mount_path: 'secrets/kv/0/', counts: { acme_clients: 0, clients: 81, @@ -428,8 +428,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, mounts: [ { - mount_path: 'acme/pki/0', - mount_type: 'pki', + mount_path: 'acme/pki/0/', + mount_type: 'pki/', counts: { acme_clients: 91, clients: 91, @@ -439,8 +439,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'auth/userpass/0', - mount_type: 'userpass', + mount_path: 'auth/userpass/0/', + mount_type: 'userpass/', counts: { acme_clients: 0, clients: 75, @@ -450,8 +450,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'secrets/kv/0', - mount_type: 'kv', + mount_path: 'secrets/kv/0/', + mount_type: 'kv/', counts: { acme_clients: 0, clients: 25, @@ -464,7 +464,7 @@ export const ACTIVITY_RESPONSE_STUB = { }, { namespace_id: 'e67m31', - namespace_path: 'ns1', + namespace_path: 'ns1/', counts: { acme_clients: 53, clients: 173, @@ -474,8 +474,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, mounts: [ { - mount_path: 'auth/userpass/0', - mount_type: 'userpass', + mount_path: 'auth/userpass/0/', + mount_type: 'userpass/', counts: { acme_clients: 0, clients: 96, @@ -485,8 +485,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'acme/pki/0', - mount_type: 'pki', + mount_path: 'acme/pki/0/', + mount_type: 'pki/', counts: { acme_clients: 53, clients: 53, @@ -496,8 +496,8 @@ export const ACTIVITY_RESPONSE_STUB = { }, }, { - mount_path: 'secrets/kv/0', - mount_type: 'kv', + mount_path: 'secrets/kv/0/', + mount_type: 'kv/', counts: { acme_clients: 0, clients: 24, @@ -561,7 +561,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = { non_entity_clients: 0, secret_syncs: 0, }, - mount_path: 'auth/userpass/0', + mount_path: 'auth/userpass/0/', mount_type: 'userpass', }, ], @@ -613,7 +613,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = { non_entity_clients: 0, secret_syncs: 0, }, - mount_path: 'auth/userpass/0', + mount_path: 'auth/userpass/0/', mount_type: 'userpass', }, ], @@ -658,7 +658,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = { non_entity_clients: 0, secret_syncs: 0, }, - mount_path: 'auth/userpass/0', + mount_path: 'auth/userpass/0/', mount_type: 'userpass', }, ], @@ -683,7 +683,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { }, by_namespace: [ { - label: 'ns1', + label: 'ns1/', acme_clients: 5699, clients: 18903, entity_clients: 4256, @@ -691,10 +691,10 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 4810, mounts: [ { - label: 'auth/userpass/0', - mount_path: 'auth/userpass/0', + label: 'auth/userpass/0/', + mount_path: 'auth/userpass/0/', mount_type: 'userpass', - namespace_path: 'ns1', + namespace_path: 'ns1/', acme_clients: 0, clients: 8394, entity_clients: 4256, @@ -702,10 +702,10 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'acme/pki/0', - mount_path: 'acme/pki/0', + label: 'acme/pki/0/', + mount_path: 'acme/pki/0/', mount_type: 'pki', - namespace_path: 'ns1', + namespace_path: 'ns1/', acme_clients: 5699, clients: 5699, entity_clients: 0, @@ -713,10 +713,10 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'secrets/kv/0', - mount_path: 'secrets/kv/0', + label: 'secrets/kv/0/', + mount_path: 'secrets/kv/0/', mount_type: 'kv', - namespace_path: 'ns1', + namespace_path: 'ns1/', acme_clients: 0, clients: 4810, entity_clients: 0, @@ -734,8 +734,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 4290, mounts: [ { - label: 'auth/userpass/0', - mount_path: 'auth/userpass/0', + label: 'auth/userpass/0/', + mount_path: 'auth/userpass/0/', mount_type: 'userpass', namespace_path: 'root', acme_clients: 0, @@ -745,8 +745,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'secrets/kv/0', - mount_path: 'secrets/kv/0', + label: 'secrets/kv/0/', + mount_path: 'secrets/kv/0/', mount_type: 'kv', namespace_path: 'root', acme_clients: 0, @@ -756,8 +756,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 4290, }, { - label: 'acme/pki/0', - mount_path: 'acme/pki/0', + label: 'acme/pki/0/', + mount_path: 'acme/pki/0/', mount_type: 'pki', namespace_path: 'root', acme_clients: 4003, @@ -795,9 +795,9 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 100, mounts: [ { - label: 'auth/userpass/0', + label: 'auth/userpass/0/', namespace_path: 'root', - mount_path: 'auth/userpass/0', + mount_path: 'auth/userpass/0/', mount_type: 'userpass', acme_clients: 0, clients: 200, @@ -806,9 +806,9 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'acme/pki/0', + label: 'acme/pki/0/', namespace_path: 'root', - mount_path: 'acme/pki/0', + mount_path: 'acme/pki/0/', mount_type: 'pki', acme_clients: 100, clients: 100, @@ -817,9 +817,9 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'secrets/kv/0', + label: 'secrets/kv/0/', namespace_path: 'root', - mount_path: 'secrets/kv/0', + mount_path: 'secrets/kv/0/', mount_type: 'kv', acme_clients: 0, clients: 100, @@ -847,8 +847,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 100, mounts: [ { - label: 'auth/userpass/0', - mount_path: 'auth/userpass/0', + label: 'auth/userpass/0/', + mount_path: 'auth/userpass/0/', mount_type: 'userpass', namespace_path: 'root', acme_clients: 0, @@ -858,8 +858,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'acme/pki/0', - mount_path: 'acme/pki/0', + label: 'acme/pki/0/', + mount_path: 'acme/pki/0/', namespace_path: 'root', mount_type: 'pki', acme_clients: 100, @@ -869,8 +869,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'secrets/kv/0', - mount_path: 'secrets/kv/0', + label: 'secrets/kv/0/', + mount_path: 'secrets/kv/0/', mount_type: 'kv', namespace_path: 'root', acme_clients: 0, @@ -901,8 +901,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 100, mounts: [ { - label: 'auth/userpass/0', - mount_path: 'auth/userpass/0', + label: 'auth/userpass/0/', + mount_path: 'auth/userpass/0/', namespace_path: 'root', mount_type: 'userpass', acme_clients: 0, @@ -912,8 +912,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'acme/pki/0', - mount_path: 'acme/pki/0', + label: 'acme/pki/0/', + mount_path: 'acme/pki/0/', namespace_path: 'root', mount_type: 'pki', acme_clients: 100, @@ -924,8 +924,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { }, { - label: 'secrets/kv/0', - mount_path: 'secrets/kv/0', + label: 'secrets/kv/0/', + mount_path: 'secrets/kv/0/', namespace_path: 'root', mount_type: 'kv', acme_clients: 0, @@ -951,7 +951,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 238, namespaces: [ { - label: 'ns1', + label: 'ns1/', acme_clients: 934, clients: 1981, entity_clients: 708, @@ -959,8 +959,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 157, mounts: [ { - label: 'acme/pki/0', - mount_path: 'acme/pki/0', + label: 'acme/pki/0/', + mount_path: 'acme/pki/0/', acme_clients: 934, clients: 934, entity_clients: 0, @@ -968,8 +968,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'auth/userpass/0', - mount_path: 'auth/userpass/0', + label: 'auth/userpass/0/', + mount_path: 'auth/userpass/0/', acme_clients: 0, clients: 890, entity_clients: 708, @@ -977,8 +977,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'secrets/kv/0', - mount_path: 'secrets/kv/0', + label: 'secrets/kv/0/', + mount_path: 'secrets/kv/0/', acme_clients: 0, clients: 157, entity_clients: 0, @@ -996,8 +996,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 81, mounts: [ { - label: 'acme/pki/0', - mount_path: 'acme/pki/0', + label: 'acme/pki/0/', + mount_path: 'acme/pki/0/', acme_clients: 994, clients: 994, entity_clients: 0, @@ -1005,8 +1005,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'auth/userpass/0', - mount_path: 'auth/userpass/0', + label: 'auth/userpass/0/', + mount_path: 'auth/userpass/0/', acme_clients: 0, clients: 872, entity_clients: 124, @@ -1014,8 +1014,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'secrets/kv/0', - mount_path: 'secrets/kv/0', + label: 'secrets/kv/0/', + mount_path: 'secrets/kv/0/', acme_clients: 0, clients: 81, entity_clients: 0, @@ -1042,8 +1042,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 25, mounts: [ { - label: 'acme/pki/0', - mount_path: 'acme/pki/0', + label: 'acme/pki/0/', + mount_path: 'acme/pki/0/', mount_type: 'pki', acme_clients: 91, clients: 91, @@ -1052,8 +1052,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'auth/userpass/0', - mount_path: 'auth/userpass/0', + label: 'auth/userpass/0/', + mount_path: 'auth/userpass/0/', mount_type: 'userpass', acme_clients: 0, clients: 75, @@ -1062,8 +1062,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'secrets/kv/0', - mount_path: 'secrets/kv/0', + label: 'secrets/kv/0/', + mount_path: 'secrets/kv/0/', mount_type: 'kv', acme_clients: 0, clients: 25, @@ -1074,7 +1074,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { ], }, { - label: 'ns1', + label: 'ns1/', acme_clients: 53, clients: 173, entity_clients: 34, @@ -1082,8 +1082,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 24, mounts: [ { - label: 'auth/userpass/0', - mount_path: 'auth/userpass/0', + label: 'auth/userpass/0/', + mount_path: 'auth/userpass/0/', mount_type: 'userpass', acme_clients: 0, clients: 96, @@ -1092,8 +1092,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'acme/pki/0', - mount_path: 'acme/pki/0', + label: 'acme/pki/0/', + mount_path: 'acme/pki/0/', mount_type: 'pki', acme_clients: 53, clients: 53, @@ -1102,8 +1102,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 0, }, { - label: 'secrets/kv/0', - mount_path: 'secrets/kv/0', + label: 'secrets/kv/0/', + mount_path: 'secrets/kv/0/', mount_type: 'kv', acme_clients: 0, clients: 24, @@ -1119,61 +1119,62 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { ], }; -export const ENTITY_EXPORT = `{"entity_name":"entity_b3e2a7ff","entity_alias_name":"bob","local_entity_alias":false,"client_id":"5692c6ef-c871-128e-fb06-df2be7bfc0db","client_type":"entity","namespace_id":"root","namespace_path":"","mount_accessor":"auth_userpass_f47ad0b4","mount_type":"userpass","mount_path":"auth/userpass/","token_creation_time":"2025-08-15T23:48:09Z","client_first_used_time":"2025-08-15T23:48:09Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":["7537e6b7-3b06-65c2-1fb2-c83116eb5e6f"]} -{"entity_name":"bob-smith","entity_alias_name":"bob","local_entity_alias":false,"client_id":"23a04911-5d72-ba98-11d3-527f2fcf3a81","client_type":"entity","namespace_id":"root","namespace_path":"","mount_accessor":"auth_userpass_de28062c","mount_type":"userpass","mount_path":"auth/userpass-test/","token_creation_time":"2025-08-15T23:52:38Z","client_first_used_time":"2025-08-15T23:53:19Z","policies":["base"],"entity_metadata":{"organization":"ACME Inc.","team":"QA"},"entity_alias_metadata":{},"entity_alias_custom_metadata":{"account":"Tester Account"},"entity_group_ids":["7537e6b7-3b06-65c2-1fb2-c83116eb5e6f"]} -{"entity_name":"alice-johnson","entity_alias_name":"alice","local_entity_alias":false,"client_id":"a7c8d912-4f61-23b5-88e4-627a3dcf2b92","client_type":"entity","namespace_id":"root","namespace_path":"","mount_accessor":"auth_userpass_f47ad0b4","mount_type":"userpass","mount_path":"auth/userpass/","token_creation_time":"2025-08-16T09:15:42Z","client_first_used_time":"2025-08-16T09:16:03Z","policies":["admin","audit"],"entity_metadata":{"organization":"TechCorp","team":"DevOps","location":"San Francisco"},"entity_alias_metadata":{"department":"Engineering"},"entity_alias_custom_metadata":{"role":"Senior Engineer"},"entity_group_ids":["7537e6b7-3b06-65c2-1fb2-c83116eb5e6f","a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6"]} -{"entity_name":"charlie-brown","entity_alias_name":"charlie","local_entity_alias":true,"client_id":"b9e5f824-7c92-34d6-a1f8-738b4ecf5d73","client_type":"entity","namespace_id":"whUNi","namespace_path":"ns2/","mount_accessor":"auth_ldap_8a3b9c2d","mount_type":"ldap","mount_path":"auth/ldap/","token_creation_time":"2025-08-16T14:22:17Z","client_first_used_time":"2025-08-16T14:22:45Z","policies":["developer","read-only"],"entity_metadata":{"organization":"StartupXYZ","team":"Backend"},"entity_alias_metadata":{"cn":"charlie.brown","ou":"development"},"entity_alias_custom_metadata":{"project":"microservices"},"entity_group_ids":["c7d8e9f0-1a2b-3c4d-5e6f-789012345678"]} -{"entity_name":"diana-prince","entity_alias_name":"diana","local_entity_alias":false,"client_id":"e4f7a935-2b68-47c9-b3e6-849c5dfb7a84","client_type":"entity","namespace_id":"aT9S5","namespace_path":"ns1/","mount_accessor":"auth_oidc_1f2e3d4c","mount_type":"oidc","mount_path":"auth/oidc/","token_creation_time":"2025-08-17T11:08:33Z","client_first_used_time":"2025-08-17T11:09:01Z","policies":["security","compliance"],"entity_metadata":{"organization":"SecureTech","team":"Security","clearance":"high"},"entity_alias_metadata":{"email":"diana.prince@securetech.com"},"entity_alias_custom_metadata":{"access_level":"L4"},"entity_group_ids":["f8e7d6c5-4b3a-2918-7654-321098765432"]} -{"entity_name":"frank-castle","entity_alias_name":"frank","local_entity_alias":false,"client_id":"c6b9d248-5a71-39e4-c7f2-951d8eaf6b95","client_type":"entity","namespace_id":"root","namespace_path":"","mount_accessor":"auth_jwt_9d8c7b6a","mount_type":"jwt","mount_path":"auth/jwt/","token_creation_time":"2025-08-17T16:43:28Z","client_first_used_time":"2025-08-17T16:44:12Z","policies":["operations","monitoring"],"entity_metadata":{"organization":"CloudOps","team":"SRE","region":"us-east-1"},"entity_alias_metadata":{"sub":"frank.castle@cloudops.io","iss":"https://auth.cloudops.io"},"entity_alias_custom_metadata":{"on_call":"true","expertise":"kubernetes"},"entity_group_ids":["9a8b7c6d-5e4f-3210-9876-543210fedcba"]} -{"entity_name":"grace-hopper","entity_alias_name":"grace","local_entity_alias":true,"client_id":"d8a3e517-6f94-42b7-d5c8-062f9bce4a73","client_type":"entity","namespace_id":"YMjS8","namespace_path":"ns5/","mount_accessor":"auth_userpass_3e2d1c0b","mount_type":"userpass","mount_path":"auth/userpass-legacy/","token_creation_time":"2025-08-18T08:17:55Z","client_first_used_time":"2025-08-18T08:18:23Z","policies":["legacy-admin","data-access"],"entity_metadata":{"organization":"LegacySystems","team":"Platform","tenure":"senior"},"entity_alias_metadata":{"legacy_id":"grace.hopper.001"},"entity_alias_custom_metadata":{"system_access":"mainframe","certification":"vault-admin"},"entity_group_ids":["1f2e3d4c-5b6a-7980-1234-567890abcdef"]} +export const ENTITY_EXPORT = `{"entity_name":"entity_b3e2a7ff","entity_alias_name":"bob","local_entity_alias":false,"client_id":"5692c6ef-c871-128e-fb06-df2be7bfc0db","client_type":"entity","namespace_id":"vK5Bt","namespace_path":"ns1/","mount_accessor":"auth_userpass_f47ad0b4","mount_type":"userpass","mount_path":"auth/userpass/0/","token_creation_time":"2022-09-15T23:48:09Z","client_first_used_time":"2023-09-15T23:48:09Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":["7537e6b7-3b06-65c2-1fb2-c83116eb5e6f"]} +{"entity_name":"entity_b3e2a7ff","entity_alias_name":"bob","local_entity_alias":false,"client_id":"daf8420c-0b6b-34e6-ff38-ee1ed093bea9","client_type":"entity","namespace_id":"root","namespace_path":"","mount_accessor":"auth_userpass_f47ad0b4","mount_type":"userpass","mount_path":"auth/userpass/","token_creation_time":"2020-08-15T23:48:09Z","client_first_used_time":"2025-07-15T23:48:09Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":["7537e6b7-3b06-65c2-1fb2-c83116eb5e6f"]} +{"entity_name":"bob-smith","entity_alias_name":"bob","local_entity_alias":false,"client_id":"23a04911-5d72-ba98-11d3-527f2fcf3a81","client_type":"entity","namespace_id":"root","namespace_path":"","mount_accessor":"auth_userpass_de28062c","mount_type":"userpass","mount_path":"auth/userpass-test/","token_creation_time":"2020-08-15T23:52:38Z","client_first_used_time":"2025-08-15T23:53:19Z","policies":["base"],"entity_metadata":{"organization":"ACME Inc.","team":"QA"},"entity_alias_metadata":{},"entity_alias_custom_metadata":{"account":"Tester Account"},"entity_group_ids":["7537e6b7-3b06-65c2-1fb2-c83116eb5e6f"]} +{"entity_name":"alice-johnson","entity_alias_name":"alice","local_entity_alias":false,"client_id":"a7c8d912-4f61-23b5-88e4-627a3dcf2b92","client_type":"entity","namespace_id":"root","namespace_path":"","mount_accessor":"auth_userpass_f47ad0b4","mount_type":"userpass","mount_path":"auth/userpass/","token_creation_time":"2020-08-16T09:15:42Z","client_first_used_time":"2025-09-16T09:16:03Z","policies":["admin","audit"],"entity_metadata":{"organization":"TechCorp","team":"DevOps","location":"San Francisco"},"entity_alias_metadata":{"department":"Engineering"},"entity_alias_custom_metadata":{"role":"Senior Engineer"},"entity_group_ids":["7537e6b7-3b06-65c2-1fb2-c83116eb5e6f","a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6"]} +{"entity_name":"charlie-brown","entity_alias_name":"charlie","local_entity_alias":true,"client_id":"b9e5f824-7c92-34d6-a1f8-738b4ecf5d73","client_type":"entity","namespace_id":"whUNi","namespace_path":"ns2/","mount_accessor":"auth_ldap_8a3b9c2d","mount_type":"ldap","mount_path":"auth/ldap/","token_creation_time":"2020-08-16T14:22:17Z","client_first_used_time":"2025-10-16T14:22:45Z","policies":["developer","read-only"],"entity_metadata":{"organization":"StartupXYZ","team":"Backend"},"entity_alias_metadata":{"cn":"charlie.brown","ou":"development"},"entity_alias_custom_metadata":{"project":"microservices"},"entity_group_ids":["c7d8e9f0-1a2b-3c4d-5e6f-789012345678"]} +{"entity_name":"diana-prince","entity_alias_name":"diana","local_entity_alias":false,"client_id":"e4f7a935-2b68-47c9-b3e6-849c5dfb7a84","client_type":"entity","namespace_id":"aT9S5","namespace_path":"ns1/","mount_accessor":"auth_oidc_1f2e3d4c","mount_type":"oidc","mount_path":"auth/oidc/","token_creation_time":"2020-08-17T11:08:33Z","client_first_used_time":"2025-11-17T11:09:01Z","policies":["security","compliance"],"entity_metadata":{"organization":"SecureTech","team":"Security","clearance":"high"},"entity_alias_metadata":{"email":"diana.prince@securetech.com"},"entity_alias_custom_metadata":{"access_level":"L4"},"entity_group_ids":["f8e7d6c5-4b3a-2918-7654-321098765432"]} +{"entity_name":"frank-castle","entity_alias_name":"frank","local_entity_alias":false,"client_id":"c6b9d248-5a71-39e4-c7f2-951d8eaf6b95","client_type":"entity","namespace_id":"root","namespace_path":"","mount_accessor":"auth_jwt_9d8c7b6a","mount_type":"jwt","mount_path":"auth/jwt/","token_creation_time":"2020-08-17T16:43:28Z","client_first_used_time":"2025-12-17T16:44:12Z","policies":["operations","monitoring"],"entity_metadata":{"organization":"CloudOps","team":"SRE","region":"us-east-1"},"entity_alias_metadata":{"sub":"frank.castle@cloudops.io","iss":"https://auth.cloudops.io"},"entity_alias_custom_metadata":{"on_call":"true","expertise":"kubernetes"},"entity_group_ids":["9a8b7c6d-5e4f-3210-9876-543210fedcba"]} +{"entity_name":"grace-hopper","entity_alias_name":"grace","local_entity_alias":true,"client_id":"d8a3e517-6f94-42b7-d5c8-062f9bce4a73","client_type":"entity","namespace_id":"YMjS8","namespace_path":"ns5/","mount_accessor":"auth_userpass_3e2d1c0b","mount_type":"userpass","mount_path":"auth/userpass-legacy/","token_creation_time":"2020-08-18T08:17:55Z","client_first_used_time":"2025-06-18T08:18:23Z","policies":["legacy-admin","data-access"],"entity_metadata":{"organization":"LegacySystems","team":"Platform","tenure":"senior"},"entity_alias_metadata":{"legacy_id":"grace.hopper.001"},"entity_alias_custom_metadata":{"system_access":"mainframe","certification":"vault-admin"},"entity_group_ids":["1f2e3d4c-5b6a-7980-1234-567890abcdef"]} `; -const NON_ENTITY_EXPORT = `{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"46dcOXXH+P1VEQiKTQjtWXEtBlbHdMOWwz+svXf3xuU=","client_type":"non-entity-token","namespace_id":"whUNi","namespace_path":"ns2/","mount_accessor":"auth_ns_token_3b2bf405","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:21Z","client_first_used_time":"2025-08-15T16:19:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"VKAJVITyTwyqF1GUzwYHwkaK6bbnL1zN8ZJ7viKR8no=","client_type":"non-entity-token","namespace_id":"omjn8","namespace_path":"ns8/","mount_accessor":"auth_ns_token_07b90be7","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:22Z","client_first_used_time":"2025-08-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"ww4L5n9WE32lPNh3UBgT3JxTDZb1a+m/3jqUffp04tQ=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:23Z","client_first_used_time":"2025-08-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"cBLb9erIROCw7cczXpfkXTOdnZoVwfWF4EAPD9k61lU=","client_type":"non-entity-token","namespace_id":"aT9S5","namespace_path":"ns1/","mount_accessor":"auth_ns_token_62a4e52a","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:21Z","client_first_used_time":"2025-08-15T16:19:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"KMHoH3Kvr6nnW2ZIs+i37pYvyVtnuaL3DmyVxUL6boI=","client_type":"non-entity-token","namespace_id":"YMjS8","namespace_path":"ns5/","mount_accessor":"auth_ns_token_45cbc810","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:22Z","client_first_used_time":"2025-08-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"hcMH4P4IGAN13cJqkwIJLXYoPLTodtOj/wPTZKS0x4U=","client_type":"non-entity-token","namespace_id":"ZNdL5","namespace_path":"ns7/","mount_accessor":"auth_ns_token_8bbd9440","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:22Z","client_first_used_time":"2025-08-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Oby0ABLmfhqYdfqGfljGHHhAA5zX+BwsGmFu4QGJZd0=","client_type":"non-entity-token","namespace_id":"bJIgY","namespace_path":"ns9/","mount_accessor":"auth_ns_token_8d188479","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:22Z","client_first_used_time":"2025-08-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Z6MjZuH/VD7HU11efiKoM/hfoxssSbeu4c6DhC7zUZ4=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:23Z","client_first_used_time":"2025-08-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"1UxaPHJUOPWrf0ivMgBURK6WHzbfXGkcn/C/xI3AeHQ=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:24Z","client_first_used_time":"2025-08-15T16:19:24Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"hfFbwhMucs/f84p2QTOiBLT72i0WLVkIgCGV7RIuWlo=","client_type":"non-entity-token","namespace_id":"x6sKN","namespace_path":"ns4/","mount_accessor":"auth_ns_token_2aaebdc2","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:21Z","client_first_used_time":"2025-08-15T16:19:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"sOdIr+zoNqOUa4hq6Jv4LCGVr0sTLGbvcRPVGAtUA7g=","client_type":"non-entity-token","namespace_id":"Rsvk5","namespace_path":"ns6/","mount_accessor":"auth_ns_token_f603fd8d","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:22Z","client_first_used_time":"2025-08-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"vOIAwNhe6P6HFdJQgUIU/8K6Z5e+oxyVP5x3KtTKS6U=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:23Z","client_first_used_time":"2025-08-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"ZOkJY3P7IzOqulsnEI0JAQQXwTPnXmpGUh9otqNUclc=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:23Z","client_first_used_time":"2025-08-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Lsha/HH+xLZq92XG4GYZVlwVQCiqPCUIuoego4aCybU=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:23Z","client_first_used_time":"2025-08-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Tsl/u7CDTYSXA9HRwlNTW7K/yyEe5PDkLOVTvTWy3q0=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:23Z","client_first_used_time":"2025-08-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"vnq6JntpiGV4FN6GDICLECe2in31aanLA6Q1UWqBmL0=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:24Z","client_first_used_time":"2025-08-15T16:19:24Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"MRMrywfPPL3QnKFMBGfRjjmaefBRH1VKpQVIfrd0Xb4=","client_type":"non-entity-token","namespace_id":"6aDiU","namespace_path":"ns3/","mount_accessor":"auth_ns_token_ef771c23","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:21Z","client_first_used_time":"2025-08-15T16:19:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Rce6fjHs15+hDl5XdXbWmzGNYrTcQsJuaoqfs9Vrhvw=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2025-08-15T16:19:24Z","client_first_used_time":"2025-08-15T16:19:24Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +const NON_ENTITY_EXPORT = `{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"46dcOXXH+P1VEQiKTQjtWXEtBlbHdMOWwz+svXf3xuU=","client_type":"non-entity-token","namespace_id":"whUNi","namespace_path":"ns2/","mount_accessor":"auth_ns_token_3b2bf405","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:21Z","client_first_used_time":"2025-05-15T16:19:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"VKAJVITyTwyqF1GUzwYHwkaK6bbnL1zN8ZJ7viKR8no=","client_type":"non-entity-token","namespace_id":"omjn8","namespace_path":"ns8/","mount_accessor":"auth_ns_token_07b90be7","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:22Z","client_first_used_time":"2025-05-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"ww4L5n9WE32lPNh3UBgT3JxTDZb1a+m/3jqUffp04tQ=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:23Z","client_first_used_time":"2025-05-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"cBLb9erIROCw7cczXpfkXTOdnZoVwfWF4EAPD9k61lU=","client_type":"non-entity-token","namespace_id":"aT9S5","namespace_path":"ns1/","mount_accessor":"auth_ns_token_62a4e52a","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:21Z","client_first_used_time":"2025-06-15T16:19:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"KMHoH3Kvr6nnW2ZIs+i37pYvyVtnuaL3DmyVxUL6boI=","client_type":"non-entity-token","namespace_id":"YMjS8","namespace_path":"ns5/","mount_accessor":"auth_ns_token_45cbc810","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:22Z","client_first_used_time":"2025-06-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"hcMH4P4IGAN13cJqkwIJLXYoPLTodtOj/wPTZKS0x4U=","client_type":"non-entity-token","namespace_id":"ZNdL5","namespace_path":"ns7/","mount_accessor":"auth_ns_token_8bbd9440","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:22Z","client_first_used_time":"2025-06-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Oby0ABLmfhqYdfqGfljGHHhAA5zX+BwsGmFu4QGJZd0=","client_type":"non-entity-token","namespace_id":"bJIgY","namespace_path":"ns9/","mount_accessor":"auth_ns_token_8d188479","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:22Z","client_first_used_time":"2025-06-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Z6MjZuH/VD7HU11efiKoM/hfoxssSbeu4c6DhC7zUZ4=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:23Z","client_first_used_time":"2025-07-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"1UxaPHJUOPWrf0ivMgBURK6WHzbfXGkcn/C/xI3AeHQ=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:24Z","client_first_used_time":"2025-07-15T16:19:24Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"hfFbwhMucs/f84p2QTOiBLT72i0WLVkIgCGV7RIuWlo=","client_type":"non-entity-token","namespace_id":"x6sKN","namespace_path":"ns4/","mount_accessor":"auth_ns_token_2aaebdc2","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:21Z","client_first_used_time":"2025-07-15T16:19:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"sOdIr+zoNqOUa4hq6Jv4LCGVr0sTLGbvcRPVGAtUA7g=","client_type":"non-entity-token","namespace_id":"Rsvk5","namespace_path":"ns6/","mount_accessor":"auth_ns_token_f603fd8d","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:22Z","client_first_used_time":"2025-07-15T16:19:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"vOIAwNhe6P6HFdJQgUIU/8K6Z5e+oxyVP5x3KtTKS6U=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:23Z","client_first_used_time":"2025-08-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"ZOkJY3P7IzOqulsnEI0JAQQXwTPnXmpGUh9otqNUclc=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:23Z","client_first_used_time":"2025-08-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Lsha/HH+xLZq92XG4GYZVlwVQCiqPCUIuoego4aCybU=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:23Z","client_first_used_time":"2025-08-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Tsl/u7CDTYSXA9HRwlNTW7K/yyEe5PDkLOVTvTWy3q0=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:23Z","client_first_used_time":"2025-09-15T16:19:23Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"vnq6JntpiGV4FN6GDICLECe2in31aanLA6Q1UWqBmL0=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:24Z","client_first_used_time":"2025-09-15T16:19:24Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"MRMrywfPPL3QnKFMBGfRjjmaefBRH1VKpQVIfrd0Xb4=","client_type":"non-entity-token","namespace_id":"6aDiU","namespace_path":"ns3/","mount_accessor":"auth_ns_token_ef771c23","mount_type":"ns_token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:21Z","client_first_used_time":"2025-09-15T16:19:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"Rce6fjHs15+hDl5XdXbWmzGNYrTcQsJuaoqfs9Vrhvw=","client_type":"non-entity-token","namespace_id":"root","namespace_path":"","mount_accessor":"auth_token_360f591b","mount_type":"token","mount_path":"auth/token/","token_creation_time":"2020-08-15T16:19:24Z","client_first_used_time":"2025-09-15T16:19:24Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} `; -const ACME_EXPORT = `{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.3U8nSB_yMBvrdu7PvAVykKurDiaH_vQGaEdAUsp-Cew","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:47:54Z","client_first_used_time":"2025-08-21T18:47:54Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.77tKDzxw0i81Nr4XLliTP9xRsztXLTuS16nN32B9jHA","client_type":"pki-acme","namespace_id":"whUNi","namespace_path":"ns2/","mount_accessor":"pki_06dad7b8","mount_type":"ns_pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:48:17Z","client_first_used_time":"2025-08-21T18:48:17Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.RoN77EahLU0wfem4z--ZqJSaqOZ7RvBWR3OkPHM_xaw","client_type":"pki-acme","namespace_id":"omjn8","namespace_path":"ns8/","mount_accessor":"pki_06dad7b8","mount_type":"ns_pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:49:26Z","client_first_used_time":"2025-08-21T18:49:26Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.5S7vaaJIrXormQSLHv4YkBhVfu6Ug0GERhTVTCrq-Fk","client_type":"pki-acme","namespace_id":"aT9S5","namespace_path":"ns1/","mount_accessor":"pki_06dad7b8","mount_type":"ns_pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:45:12Z","client_first_used_time":"2025-08-21T18:45:12Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.IeJQMwtkReJHVNL6fZLmqiu8-Re4JdKCQixXkfcaSRE","client_type":"pki-acme","namespace_id":"YMjS8","namespace_path":"ns5/","mount_accessor":"pki_06dad7b8","mount_type":"ns_pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:45:41Z","client_first_used_time":"2025-08-21T18:45:41Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.vTm3SCZom90qy3SuyIacpVsQgGLx7ASf3SeGpqn5XBA","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:47:19Z","client_first_used_time":"2025-08-21T18:47:19Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.64jWs15k6roUH6MiQ2u80K08Bmqw8IQOpqTpDZgZ1f4","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:47:25Z","client_first_used_time":"2025-08-21T18:47:25Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.RkjnwyIIn6bnc4LDdKQ9HNfnhuVXT7vQONXgGHJl4CE","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:49:21Z","client_first_used_time":"2025-08-21T18:49:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.uozIMLVXDMU7Fc2TFFwq0-uE1GFSui5rbTI1XyNAYBY","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:44:44Z","client_first_used_time":"2025-08-21T18:44:44Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.WiLdlzq93WtVmObB__CC2SPX6sI7EVLTTzxOIRHHN3o","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:44:49Z","client_first_used_time":"2025-08-21T18:44:49Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.P65jgamzwLYbKyxTlJFD5DL3sIUbusbXcQhYaysgzlU","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:45:59Z","client_first_used_time":"2025-08-21T18:45:59Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.2REWUkDLXAG2UB0ZJQcjPnHc4H39aq8fG3LMaHSHKow","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:46:05Z","client_first_used_time":"2025-08-21T18:46:05Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.Eeyq9-EfWv-iE9Aj3DzCU4r9P8V1Maewx51vcxMN-jA","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:46:10Z","client_first_used_time":"2025-08-21T18:46:10Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.vaeb2KR58sRuMUdUlv2TsbaOkSICTAxmJxhkuOs8ZiM","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:46:22Z","client_first_used_time":"2025-08-21T18:46:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.xEPG0eNfrAfRgXg6AKjsCrFPMs0IbLTCfUsCie_rfzY","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:46:51Z","client_first_used_time":"2025-08-21T18:46:51Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.Bkg4862LEoFXJUDWlfFtJHU9a69KRJPiEdw5XCbkkAI","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2025-08-21T18:47:42Z","client_first_used_time":"2025-08-21T18:47:42Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +const ACME_EXPORT = `{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.3U8nSB_yMBvrdu7PvAVykKurDiaH_vQGaEdAUsp-Cew","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:47:54Z","client_first_used_time":"2025-06-21T18:47:54Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.77tKDzxw0i81Nr4XLliTP9xRsztXLTuS16nN32B9jHA","client_type":"pki-acme","namespace_id":"whUNi","namespace_path":"ns2/","mount_accessor":"pki_06dad7b8","mount_type":"ns_pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:48:17Z","client_first_used_time":"2025-07-21T18:48:17Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.RoN77EahLU0wfem4z--ZqJSaqOZ7RvBWR3OkPHM_xaw","client_type":"pki-acme","namespace_id":"omjn8","namespace_path":"ns8/","mount_accessor":"pki_06dad7b8","mount_type":"ns_pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:49:26Z","client_first_used_time":"2025-08-21T18:49:26Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.5S7vaaJIrXormQSLHv4YkBhVfu6Ug0GERhTVTCrq-Fk","client_type":"pki-acme","namespace_id":"aT9S5","namespace_path":"ns1/","mount_accessor":"pki_06dad7b8","mount_type":"ns_pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:45:12Z","client_first_used_time":"2025-08-21T18:45:12Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.IeJQMwtkReJHVNL6fZLmqiu8-Re4JdKCQixXkfcaSRE","client_type":"pki-acme","namespace_id":"YMjS8","namespace_path":"ns5/","mount_accessor":"pki_06dad7b8","mount_type":"ns_pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:45:41Z","client_first_used_time":"2025-08-21T18:45:41Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.vTm3SCZom90qy3SuyIacpVsQgGLx7ASf3SeGpqn5XBA","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:47:19Z","client_first_used_time":"2025-08-21T18:47:19Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.64jWs15k6roUH6MiQ2u80K08Bmqw8IQOpqTpDZgZ1f4","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:47:25Z","client_first_used_time":"2025-08-21T18:47:25Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.RkjnwyIIn6bnc4LDdKQ9HNfnhuVXT7vQONXgGHJl4CE","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:49:21Z","client_first_used_time":"2025-08-21T18:49:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.uozIMLVXDMU7Fc2TFFwq0-uE1GFSui5rbTI1XyNAYBY","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:44:44Z","client_first_used_time":"2025-08-21T18:44:44Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.WiLdlzq93WtVmObB__CC2SPX6sI7EVLTTzxOIRHHN3o","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:44:49Z","client_first_used_time":"2025-08-21T18:44:49Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.P65jgamzwLYbKyxTlJFD5DL3sIUbusbXcQhYaysgzlU","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:45:59Z","client_first_used_time":"2025-08-21T18:45:59Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.2REWUkDLXAG2UB0ZJQcjPnHc4H39aq8fG3LMaHSHKow","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:46:05Z","client_first_used_time":"2025-08-21T18:46:05Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.Eeyq9-EfWv-iE9Aj3DzCU4r9P8V1Maewx51vcxMN-jA","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:46:10Z","client_first_used_time":"2025-08-21T18:46:10Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.vaeb2KR58sRuMUdUlv2TsbaOkSICTAxmJxhkuOs8ZiM","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:46:22Z","client_first_used_time":"2025-08-21T18:46:22Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.xEPG0eNfrAfRgXg6AKjsCrFPMs0IbLTCfUsCie_rfzY","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:46:51Z","client_first_used_time":"2025-08-21T18:46:51Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"pki-acme.Bkg4862LEoFXJUDWlfFtJHU9a69KRJPiEdw5XCbkkAI","client_type":"pki-acme","namespace_id":"root","namespace_path":"","mount_accessor":"pki_06dad7b8","mount_type":"pki","mount_path":"pki_int/","token_creation_time":"2020-08-21T18:47:42Z","client_first_used_time":"2025-08-21T18:47:42Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} `; -const SECRET_SYNC_EXPORT = `{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.3U8nSB_yMBvrdu7PvAVykKurDiaH_vQGaEdAUsp-Cew","client_type":"secret-sync","namespace_id":"root","namespace_path":"","mount_accessor":"kv_06dad7b8","mount_type":"kv","mount_path":"secrets/kv/0","token_creation_time":"2025-08-21T18:47:54Z","client_first_used_time":"2025-08-21T18:47:54Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.77tKDzxw0i81Nr4XLliTP9xRsztXLTuS16nN32B9jHA","client_type":"secret-sync","namespace_id":"ZNdL5","namespace_path":"ns7/","mount_accessor":"kv_06dad7b8","mount_type":"ns_kv","mount_path":"secrets/kv/0","token_creation_time":"2025-08-21T18:48:17Z","client_first_used_time":"2025-08-21T18:48:17Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.RoN77EahLU0wfem4z--ZqJSaqOZ7RvBWR3OkPHM_xaw","client_type":"secret-sync","namespace_id":"bJIgY","namespace_path":"ns9/","mount_accessor":"kv_12abc3d4","mount_type":"ns_kv","mount_path":"secrets/kv/1","token_creation_time":"2025-08-21T18:49:26Z","client_first_used_time":"2025-08-21T18:49:26Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.5S7vaaJIrXormQSLHv4YkBhVfu6Ug0GERhTVTCrq-Fk","client_type":"secret-sync","namespace_id":"x6sKN","namespace_path":"ns4/","mount_accessor":"kv_06dad7b8","mount_type":"ns_kv","mount_path":"secrets/kv/0","token_creation_time":"2025-08-21T18:45:12Z","client_first_used_time":"2025-08-21T18:45:12Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.IeJQMwtkReJHVNL6fZLmqiu8-Re4JdKCQixXkfcaSRE","client_type":"secret-sync","namespace_id":"Rsvk5","namespace_path":"ns6/","mount_accessor":"kv_12abc3d4","mount_type":"ns_kv","mount_path":"secrets/kv/1","token_creation_time":"2025-08-21T18:45:41Z","client_first_used_time":"2025-08-21T18:45:41Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.vTm3SCZom90qy3SuyIacpVsQgGLx7ASf3SeGpqn5XBA","client_type":"secret-sync","namespace_id":"root","namespace_path":"","mount_accessor":"kv_06dad7b8","mount_type":"kv","mount_path":"secrets/kv/0","token_creation_time":"2025-08-21T18:47:19Z","client_first_used_time":"2025-08-21T18:47:19Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.64jWs15k6roUH6MiQ2u80K08Bmqw8IQOpqTpDZgZ1f4","client_type":"secret-sync","namespace_id":"6aDiU","namespace_path":"ns3/","mount_accessor":"kv_12abc3d4","mount_type":"kv","mount_path":"secrets/kv/1","token_creation_time":"2025-08-21T18:47:25Z","client_first_used_time":"2025-08-21T18:47:25Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} -{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.RkjnwyIIn6bnc4LDdKQ9HNfnhuVXT7vQONXgGHJl4CE","client_type":"secret-sync","namespace_id":"root","namespace_path":"","mount_accessor":"kv_06dad7b8","mount_type":"kv","mount_path":"secrets/kv/0","token_creation_time":"2025-08-21T18:49:21Z","client_first_used_time":"2025-08-21T18:49:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +const SECRET_SYNC_EXPORT = `{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.3U8nSB_yMBvrdu7PvAVykKurDiaH_vQGaEdAUsp-Cew","client_type":"secret-sync","namespace_id":"root","namespace_path":"","mount_accessor":"kv_06dad7b8","mount_type":"kv","mount_path":"secrets/kv/0/","token_creation_time":"2020-08-21T18:47:54Z","client_first_used_time":"2025-05-21T18:47:54Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.77tKDzxw0i81Nr4XLliTP9xRsztXLTuS16nN32B9jHA","client_type":"secret-sync","namespace_id":"ZNdL5","namespace_path":"ns7/","mount_accessor":"kv_06dad7b8","mount_type":"ns_kv","mount_path":"secrets/kv/0/","token_creation_time":"2020-08-21T18:48:17Z","client_first_used_time":"2025-05-21T18:48:17Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.RoN77EahLU0wfem4z--ZqJSaqOZ7RvBWR3OkPHM_xaw","client_type":"secret-sync","namespace_id":"bJIgY","namespace_path":"ns9/","mount_accessor":"kv_12abc3d4","mount_type":"ns_kv","mount_path":"secrets/kv/1","token_creation_time":"2020-08-21T18:49:26Z","client_first_used_time":"2025-06-21T18:49:26Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.5S7vaaJIrXormQSLHv4YkBhVfu6Ug0GERhTVTCrq-Fk","client_type":"secret-sync","namespace_id":"x6sKN","namespace_path":"ns4/","mount_accessor":"kv_06dad7b8","mount_type":"ns_kv","mount_path":"secrets/kv/0/","token_creation_time":"2020-08-21T18:45:12Z","client_first_used_time":"2025-06-21T18:45:12Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.IeJQMwtkReJHVNL6fZLmqiu8-Re4JdKCQixXkfcaSRE","client_type":"secret-sync","namespace_id":"Rsvk5","namespace_path":"ns6/","mount_accessor":"kv_12abc3d4","mount_type":"ns_kv","mount_path":"secrets/kv/1","token_creation_time":"2020-08-21T18:45:41Z","client_first_used_time":"2025-07-21T18:45:41Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.vTm3SCZom90qy3SuyIacpVsQgGLx7ASf3SeGpqn5XBA","client_type":"secret-sync","namespace_id":"root","namespace_path":"","mount_accessor":"kv_06dad7b8","mount_type":"kv","mount_path":"secrets/kv/0/","token_creation_time":"2020-08-21T18:47:19Z","client_first_used_time":"2025-08-21T18:47:19Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.64jWs15k6roUH6MiQ2u80K08Bmqw8IQOpqTpDZgZ1f4","client_type":"secret-sync","namespace_id":"6aDiU","namespace_path":"ns3/","mount_accessor":"kv_12abc3d4","mount_type":"kv","mount_path":"secrets/kv/1","token_creation_time":"2020-08-21T18:47:25Z","client_first_used_time":"2025-08-21T18:47:25Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} +{"entity_name":"","entity_alias_name":"","local_entity_alias":false,"client_id":"secret-sync.RkjnwyIIn6bnc4LDdKQ9HNfnhuVXT7vQONXgGHJl4CE","client_type":"secret-sync","namespace_id":"root","namespace_path":"","mount_accessor":"kv_06dad7b8","mount_type":"kv","mount_path":"secrets/kv/0/","token_creation_time":"2020-08-21T18:49:21Z","client_first_used_time":"2025-08-21T18:49:21Z","policies":[],"entity_metadata":{},"entity_alias_metadata":{},"entity_alias_custom_metadata":{},"entity_group_ids":[]} `; export const ACTIVITY_EXPORT_STUB = ENTITY_EXPORT + NON_ENTITY_EXPORT + ACME_EXPORT + SECRET_SYNC_EXPORT; diff --git a/ui/tests/helpers/clients/client-count-selectors.ts b/ui/tests/helpers/clients/client-count-selectors.ts index ad7f257953..364e5b769b 100644 --- a/ui/tests/helpers/clients/client-count-selectors.ts +++ b/ui/tests/helpers/clients/client-count-selectors.ts @@ -13,7 +13,10 @@ export const CLIENT_COUNT = { mountPaths: '[data-test-counts-auth-mounts]', }, dateRange: { - dropdownOption: (idx = 0) => `[data-test-date-range-billing-start="${idx}"]`, + dropdownOption: (idx: number | null) => + typeof idx === 'number' + ? `[data-test-date-range-billing-start="${idx}"]` + : '[data-test-date-range-billing-start]', dateDisplay: (name: string) => (name ? `[data-test-date-range="${name}"]` : '[data-test-date-range]'), edit: '[data-test-date-range-edit]', editModal: '[data-test-date-range-edit-modal]', diff --git a/ui/tests/integration/components/clients/date-range-test.js b/ui/tests/integration/components/clients/date-range-test.js index 1f5e34f1da..5d816e98ba 100644 --- a/ui/tests/integration/components/clients/date-range-test.js +++ b/ui/tests/integration/components/clients/date-range-test.js @@ -5,7 +5,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; -import { click, fillIn, render } from '@ember/test-helpers'; +import { click, fillIn, findAll, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import Sinon from 'sinon'; import timestamp from 'core/utils/timestamp'; @@ -22,7 +22,7 @@ module('Integration | Component | clients/date-range', function (hooks) { this.now = timestamp.now(); this.startTimestamp = '2018-01-01T14:15:30'; this.endTimestamp = '2019-01-31T14:15:30'; - this.billingStartTime = '2018-01-01T14:15:30'; + this.billingStartTime = ''; this.retentionMonths = 48; this.onChange = Sinon.spy(); this.setEditModalVisible = Sinon.stub().callsFake((visible) => { @@ -51,8 +51,8 @@ module('Integration | Component | clients/date-range', function (hooks) { await fillIn(DATE_RANGE.editDate('end'), '2018-03'); await click(GENERAL.submitButton); const { start_time, end_time } = this.onChange.lastCall.args[0]; - assert.strictEqual(start_time, '2018-01-01T00:00:00.000Z', 'it formats start_time param'); - assert.strictEqual(end_time, '2018-03-31T23:59:59.000Z', 'it formats end_time param'); + assert.strictEqual(start_time, '2018-01-01T00:00:00Z', 'it formats start_time param'); + assert.strictEqual(end_time, '2018-03-31T23:59:59Z', 'it formats end_time param'); assert.dom(DATE_RANGE.editModal).doesNotExist('closes modal'); }); @@ -101,4 +101,29 @@ module('Integration | Component | clients/date-range', function (hooks) { await click(GENERAL.submitButton); assert.false(this.onChange.called); }); + + module('enterprise', function (hooks) { + hooks.beforeEach(function () { + this.version = this.owner.lookup('service:version'); + this.version.type = 'enterprise'; + this.billingStartTime = '2018-01-01T14:15:30'; + }); + + test('it billing start date dropdown for enterprise', async function (assert) { + await this.renderComponent(); + await click(DATE_RANGE.edit); + const expectedPeriods = [ + 'January 2018', + 'January 2017', + 'January 2016', + 'January 2015', + 'January 2014', + ]; + const dropdownList = findAll(DATE_RANGE.dropdownOption(null)); + dropdownList.forEach((item, idx) => { + const month = expectedPeriods[idx]; + assert.dom(item).hasText(month, `dropdown index: ${idx} renders ${month}`); + }); + }); + }); }); diff --git a/ui/tests/integration/components/clients/filter-toolbar-test.js b/ui/tests/integration/components/clients/filter-toolbar-test.js index 4699bf76f7..fe559f96cf 100644 --- a/ui/tests/integration/components/clients/filter-toolbar-test.js +++ b/ui/tests/integration/components/clients/filter-toolbar-test.js @@ -25,7 +25,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) { { namespace_path: 'ns1/', mount_type: 'ns_token/', mount_path: 'auth/token/' }, ]; this.onFilter = sinon.spy(); - this.filterQueryParams = { namespace_path: '', mount_path: '', mount_type: '' }; + this.filterQueryParams = { namespace_path: '', mount_path: '', mount_type: '', month: '' }; this.renderComponent = async () => { await render(hbs` @@ -260,7 +260,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) { const [afterUpdate] = this.onFilter.lastCall.args; assert.propEqual( afterUpdate, - { namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/' }, + { namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/', month: '' }, 'callback fires with updated selection' ); assert.dom(FILTERS.tagContainer).hasText('Filters applied: admin/ auth/userpass-root/ token/'); @@ -273,7 +273,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) { const [afterClear] = this.onFilter.lastCall.args; assert.propEqual( afterClear, - { namespace_path: '', mount_path: '', mount_type: '' }, + { namespace_path: '', mount_path: '', mount_type: '', month: '' }, 'onFilter callback has empty values when "Clear filters" is clicked' ); assert.dom(FILTERS.tagContainer).hasText('Filters applied: None'); @@ -286,7 +286,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) { const afterClear = this.onFilter.lastCall.args[0]; assert.propEqual( afterClear, - { namespace_path: '', mount_path: 'auth/userpass-root/', mount_type: 'token/' }, + { namespace_path: '', mount_path: 'auth/userpass-root/', mount_type: 'token/', month: '' }, 'onFilter callback fires with empty namespace_path' ); }); diff --git a/ui/tests/integration/components/clients/page/client-list-test.js b/ui/tests/integration/components/clients/page/client-list-test.js index 8d16391b7e..b6f997dc96 100644 --- a/ui/tests/integration/components/clients/page/client-list-test.js +++ b/ui/tests/integration/components/clients/page/client-list-test.js @@ -250,6 +250,8 @@ module('Integration | Component | clients/page/client-list', function (hooks) { }); test('it renders empty state message when filter selections yield no results', async function (assert) { + const flags = this.owner.lookup('service:flags'); + flags.activatedFlags = ['secrets-sync']; this.filterQueryParams = { namespace_path: 'dev/', mount_path: 'pluto/', mount_type: 'banana' }; await this.renderComponent(); @@ -257,7 +259,18 @@ module('Integration | Component | clients/page/client-list', function (hooks) { await click(GENERAL.hdsTab(tabName)); assert .dom(CLIENT_COUNT.card('table empty state')) - .hasText('No data found Clear or change filters to view client count data.'); + .hasText('No data found Select another client type or update filters to view client count data.'); } }); + + test('it renders empty state message when secret sync is not activated', async function (assert) { + this.filterQueryParams = { namespace_path: 'dev/', mount_path: 'pluto/', mount_type: 'banana' }; + await this.renderComponent(); + await click(GENERAL.hdsTab('Secret sync')); + assert + .dom(CLIENT_COUNT.card('table empty state')) + .hasText( + 'No secret sync clients No data is available because Secrets Sync has not been activated. Activate Secrets Sync' + ); + }); }); diff --git a/ui/tests/integration/components/clients/page/counts-test.js b/ui/tests/integration/components/clients/page/counts-test.js index 01f127d3b5..463c7382de 100644 --- a/ui/tests/integration/components/clients/page/counts-test.js +++ b/ui/tests/integration/components/clients/page/counts-test.js @@ -94,10 +94,10 @@ module('Integration | Component | clients | Page::Counts', function (hooks) { .hasText('Tracking is disabled', 'Config disabled alert renders'); }); - const jan23start = '2023-01-01T00:00:00.000Z'; + const jan23start = '2023-01-01T00:00:00Z'; // license start is July 2, 2024 on date change it recalculates start to beginning of the month - const july23start = '2023-07-01T00:00:00.000Z'; - const dec23end = '2023-12-31T23:59:59.000Z'; + const july23start = '2023-07-01T00:00:00Z'; + const dec23end = '2023-12-31T23:59:59Z'; const testCases = [ { scenario: 'changing start only', @@ -123,7 +123,7 @@ module('Integration | Component | clients | Page::Counts', function (hooks) { }, ]; testCases.forEach((testCase) => { - test(`it should send correct millis value on filter change when ${testCase.scenario}`, async function (assert) { + test(`it should send correct timestamp on filter change when ${testCase.scenario}`, async function (assert) { assert.expect(5); this.owner.lookup('service:version').type = 'community'; this.onFilterChange = (params) => { diff --git a/ui/tests/integration/components/clients/page/overview-test.js b/ui/tests/integration/components/clients/page/overview-test.js index bf2c752062..e572b52659 100644 --- a/ui/tests/integration/components/clients/page/overview-test.js +++ b/ui/tests/integration/components/clients/page/overview-test.js @@ -5,7 +5,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; -import { click, fillIn, find, findAll, render, triggerEvent } from '@ember/test-helpers'; +import { click, find, findAll, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers'; @@ -13,6 +13,7 @@ import { CLIENT_COUNT, FILTERS } from 'vault/tests/helpers/clients/client-count- import { GENERAL } from 'vault/tests/helpers/general-selectors'; import sinon from 'sinon'; import { ClientFilters, flattenMounts } from 'core/utils/client-count-utils'; +import { parseAPITimestamp } from 'core/utils/date-formatters'; module('Integration | Component | clients/page/overview', function (hooks) { setupRenderingTest(hooks); @@ -30,7 +31,7 @@ module('Integration | Component | clients/page/overview', function (hooks) { this.activity = await this.store.queryRecord('clients/activity', {}); this.mostRecentMonth = this.activity.byMonth[this.activity.byMonth.length - 1]; this.onFilterChange = sinon.spy(); - this.filterQueryParams = { namespace_path: '', mount_path: '', mount_type: '' }; + this.filterQueryParams = { namespace_path: '', mount_path: '', mount_type: '', month: '' }; this.renderComponent = () => render(hbs` `); this.assertTableData = async (assert, filterKey, filterValue) => { - const expectedData = flattenMounts(this.mostRecentMonth.new_clients.namespaces).filter( + const expectedData = flattenMounts(this.activity.byNamespace).filter( (d) => d[filterKey] === filterValue ); - await fillIn(GENERAL.selectByAttr('attribution-month'), this.mostRecentMonth.timestamp); - assert.dom(GENERAL.tableRow()).exists({ count: expectedData.length }); // Find all rendered rows and assert they satisfy the filter value and table data matches expected values const rows = findAll(GENERAL.tableRow()); rows.forEach((_, idx) => { @@ -85,45 +84,6 @@ module('Integration | Component | clients/page/overview', function (hooks) { this.activity = await this.store.queryRecord('clients/activity', {}); await this.renderComponent(); assert.dom(CLIENT_COUNT.card('Client attribution')).doesNotExist('it does not render attribution card'); - assert.dom(GENERAL.selectByAttr('attribution-month')).doesNotExist('it hides months dropdown'); - }); - - test('it shows correct state message when selected month has no data', async function (assert) { - await this.renderComponent(); - assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown'); - await fillIn(GENERAL.selectByAttr('attribution-month'), '2023-06-01T00:00:00Z'); - - assert - .dom(CLIENT_COUNT.card('table empty state')) - .hasText('No data found Clear or change filters to view client count data. Client count documentation'); - }); - - test('it shows table when month selection has data', async function (assert) { - await this.renderComponent(); - - assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown'); - await fillIn(GENERAL.selectByAttr('attribution-month'), '9/23'); - - assert.dom(CLIENT_COUNT.card('table empty state')).doesNotExist('does not show card when table has data'); - assert.dom(GENERAL.table('attribution')).exists('shows table'); - assert.dom(GENERAL.paginationInfo).hasText('1–6 of 6', 'shows correct pagination info'); - assert.dom(GENERAL.paginationSizeSelector).hasValue('10', 'page size selector defaults to "10"'); - }); - - test('it shows correct month options for billing period', async function (assert) { - await this.renderComponent(); - - assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown'); - await fillIn(GENERAL.selectByAttr('attribution-month'), ''); - await triggerEvent(GENERAL.selectByAttr('attribution-month'), 'change'); - - // assert that months options in select are those of selected billing period - // '' represents default state of 'Select month' - const expectedOptions = ['', ...this.activity.byMonth.reverse().map((m) => m.timestamp)]; - const actualOptions = findAll(`${GENERAL.selectByAttr('attribution-month')} option`).map( - (option) => option.value - ); - assert.deepEqual(actualOptions, expectedOptions, 'All