mirror of
https://github.com/hashicorp/vault.git
synced 2025-09-19 12:51:08 +02:00
* render export activity in table by client type * refactor filter toolbar to apply filters when selected * finish filter toolbar refactor * finish building client-list page * remaing test updates from the filter-toolbar refactor * WIP tests * finish tests for export tab! * add test for bar chart colors * reveal client list tab * add changelog * filter root namespace on empty string or "root" Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
parent
6a50eeed9f
commit
c2823e96eb
3
changelog/_8880.txt
Normal file
3
changelog/_8880.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
```release-note:feature
|
||||||
|
**UI Client List Explorer (Enterprise)**: Adds ability to view and filter client IDs and metadata by namespace, mount path, or mount type for a billing period.
|
||||||
|
```
|
@ -10,13 +10,13 @@ import Component from '@glimmer/component';
|
|||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
|
|
||||||
import type ClientsActivityModel from 'vault/models/clients/activity';
|
import type ClientsActivityModel from 'vault/models/clients/activity';
|
||||||
import type { ActivityExportData, ClientFilterTypes, EntityClients } from 'core/utils/client-count-utils';
|
import type { ActivityExportData, ClientFilterTypes } from 'core/utils/client-count-utils';
|
||||||
|
|
||||||
/* This component does not actually render and is the base class to house
|
/* 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 */
|
shared computations between the Clients::Page::Overview and Clients::Page::List components */
|
||||||
interface Args {
|
export interface Args {
|
||||||
activity: ClientsActivityModel;
|
activity: ClientsActivityModel;
|
||||||
exportData: ActivityExportData[] | EntityClients[];
|
exportData: ActivityExportData[];
|
||||||
onFilterChange: CallableFunction;
|
onFilterChange: CallableFunction;
|
||||||
filterQueryParams: Record<ClientFilterTypes, string>;
|
filterQueryParams: Record<ClientFilterTypes, string>;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,9 @@ Data visualizations render in in a flex row with a 1/3-width left element and a
|
|||||||
|
|
||||||
<Hds::Card::Container class="counts-card-container" data-test-card={{@title}} ...attributes>
|
<Hds::Card::Container class="counts-card-container" data-test-card={{@title}} ...attributes>
|
||||||
<div class="has-bottom-margin-xl">
|
<div class="has-bottom-margin-xl">
|
||||||
<Hds::Text::Display @tag="h2" @size="300" class="has-bottom-margin-xs">{{@title}}</Hds::Text::Display>
|
{{#if @title}}
|
||||||
|
<Hds::Text::Display @tag="h2" @size="300" class="has-bottom-margin-xs">{{@title}}</Hds::Text::Display>
|
||||||
|
{{/if}}
|
||||||
{{#if @description}}
|
{{#if @description}}
|
||||||
<Hds::Text::Body @tag="p" @size="100" @color="faint">{{@description}}</Hds::Text::Body>
|
<Hds::Text::Body @tag="p" @size="100" @color="faint">{{@description}}</Hds::Text::Body>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -11,11 +11,9 @@
|
|||||||
</LinkTo>
|
</LinkTo>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{{#if this.isNotProduction}}
|
<LinkTo @route="vault.cluster.clients.counts.client-list" data-test-tab="client list">
|
||||||
<LinkTo @route="vault.cluster.clients.counts.client-list" data-test-tab="client list">
|
Client list
|
||||||
Client list
|
</LinkTo>
|
||||||
</LinkTo>
|
|
||||||
{{/if}}
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) HashiCorp, Inc.
|
|
||||||
* SPDX-License-Identifier: BUSL-1.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO this component just exists while the client-list tab is in development for the 1.21 release
|
|
||||||
// Unless more is added to it, it can be removed when the `client list` tab+route is unhidden
|
|
||||||
import Component from '@glimmer/component';
|
|
||||||
import config from 'vault/config/environment';
|
|
||||||
|
|
||||||
export default class NavBar extends Component {
|
|
||||||
get isNotProduction() {
|
|
||||||
return config.environment !== 'production';
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,60 +3,66 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
}}
|
}}
|
||||||
|
|
||||||
<Hds::SegmentedGroup as |SG|>
|
<Hds::ButtonSet>
|
||||||
{{#each-in this.dropdownConfig as |filterProperty d|}}
|
<Hds::SegmentedGroup as |SG|>
|
||||||
{{#let d.label d.dropdownItems d.searchProperty as |label dropdownItems searchProperty|}}
|
{{! "filterProperty" is the tracked variable in the component class that corresponds to the filter type }}
|
||||||
<SG.Dropdown data-test-dropdown={{filterProperty}} @onClose={{this.updateSearch searchProperty ""}} as |D|>
|
{{#each-in this.dropdownConfig as |filterProperty d|}}
|
||||||
<D.ToggleButton @color="secondary" @text={{capitalize label}} />
|
{{! "searchProperty" is the tracked variable in the component class that corresponds to the search input }}
|
||||||
<D.Header>
|
{{#let d.label d.dropdownItems d.searchProperty as |label dropdownItems searchProperty|}}
|
||||||
<Hds::Form::TextInput::Base
|
<SG.Dropdown data-test-dropdown={{filterProperty}} @onClose={{fn this.handleDropdownClose searchProperty}} as |D|>
|
||||||
type="search"
|
<D.ToggleButton @color="secondary" @text={{capitalize label}} />
|
||||||
placeholder="Search"
|
<D.Header>
|
||||||
id={{searchProperty}}
|
<Hds::Form::TextInput::Base
|
||||||
autocomplete="off"
|
type="search"
|
||||||
@value={{get this searchProperty}}
|
placeholder="Search"
|
||||||
{{on "input" this.handleSearch}}
|
id={{searchProperty}}
|
||||||
/>
|
autocomplete="off"
|
||||||
</D.Header>
|
@value={{get this searchProperty}}
|
||||||
<D.Separator />
|
{{on "input" this.handleSearch}}
|
||||||
{{#let (this.searchDropdown dropdownItems searchProperty) as |matchingItems|}}
|
|
||||||
{{#each matchingItems as |item|}}
|
|
||||||
<D.Checkmark
|
|
||||||
{{! Select dropdown item and set as tracked property to pass back on "Apply filters" }}
|
|
||||||
{{on "click" (fn this.updateFilter filterProperty item D.close)}}
|
|
||||||
@selected={{eq item (get this filterProperty)}}
|
|
||||||
data-test-dropdown-item={{item}}
|
|
||||||
>
|
|
||||||
{{item}}
|
|
||||||
</D.Checkmark>
|
|
||||||
{{else}}
|
|
||||||
<D.Description
|
|
||||||
class="has-top-padding-xs"
|
|
||||||
@text={{this.noItemsMessage (get this searchProperty) (pluralize label)}}
|
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
</D.Header>
|
||||||
{{/let}}
|
<D.Separator />
|
||||||
</SG.Dropdown>
|
{{#let (this.searchDropdown dropdownItems searchProperty) as |matchingItems|}}
|
||||||
{{/let}}
|
{{#each matchingItems as |item|}}
|
||||||
{{/each-in}}
|
<D.Checkmark
|
||||||
|
{{! Select dropdown item and set as tracked property to update URL query params }}
|
||||||
|
{{on "click" (fn this.handleFilterSelect filterProperty item D.close)}}
|
||||||
|
{{! (get this filterProperty) returns the tracked filter value }}
|
||||||
|
@selected={{eq item (get this filterProperty)}}
|
||||||
|
data-test-dropdown-item={{item}}
|
||||||
|
>
|
||||||
|
{{item}}
|
||||||
|
</D.Checkmark>
|
||||||
|
{{else}}
|
||||||
|
<D.Description
|
||||||
|
class="has-top-padding-xs"
|
||||||
|
{{! (get this searchProperty) returns the tracked search value }}
|
||||||
|
@text={{this.noItemsMessage (get this searchProperty) (pluralize label)}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
{{/let}}
|
||||||
|
</SG.Dropdown>
|
||||||
|
{{/let}}
|
||||||
|
{{/each-in}}
|
||||||
|
</Hds::SegmentedGroup>
|
||||||
<Hds::Button
|
<Hds::Button
|
||||||
@icon="filter"
|
@icon="x-circle"
|
||||||
@text="Apply filters"
|
@text="Clear filters"
|
||||||
@color="primary"
|
@color="tertiary"
|
||||||
{{on "click" this.applyFilters}}
|
{{on "click" (fn this.clearFilters "")}}
|
||||||
data-test-button="Apply filters"
|
data-test-button="Clear filters"
|
||||||
/>
|
/>
|
||||||
</Hds::SegmentedGroup>
|
</Hds::ButtonSet>
|
||||||
|
|
||||||
<Hds::Layout::Flex class="has-top-margin-s" @gap="8" @align="center">
|
<Hds::Layout::Flex class="has-top-margin-s" @gap="8" @align="center" data-test-filter-tag-container>
|
||||||
|
<Hds::Text::Body @color="faint">Filters applied:</Hds::Text::Body>
|
||||||
{{#if this.anyFilters}}
|
{{#if this.anyFilters}}
|
||||||
<Hds::Text::Body @color="faint">Filters applied:</Hds::Text::Body>
|
{{#each-in this.filterProps as |filter value|}}
|
||||||
{{! render tags based on applied @filters and not the internally tracked properties }}
|
|
||||||
{{#each-in @appliedFilters as |filter value|}}
|
|
||||||
{{#if value}}
|
{{#if value}}
|
||||||
<div>
|
<div>
|
||||||
<Hds::Tag
|
<Hds::Tag
|
||||||
@text={{value}}
|
@text={{value}}
|
||||||
|
{{! Filter renders in a tooltip if exceeds 20 characters }}
|
||||||
@tooltipPlacement="bottom"
|
@tooltipPlacement="bottom"
|
||||||
@onDismiss={{fn this.clearFilters filter}}
|
@onDismiss={{fn this.clearFilters filter}}
|
||||||
data-test-filter-tag="{{filter}} {{value}}"
|
data-test-filter-tag="{{filter}} {{value}}"
|
||||||
@ -64,12 +70,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/each-in}}
|
{{/each-in}}
|
||||||
<Hds::Button
|
{{else}}
|
||||||
@icon="x-circle"
|
<Hds::Text::Body @color="faint">None</Hds::Text::Body>
|
||||||
@text="Clear filters"
|
|
||||||
@color="tertiary"
|
|
||||||
{{on "click" (fn this.clearFilters "")}}
|
|
||||||
data-test-button="Clear filters"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</Hds::Layout::Flex>
|
</Hds::Layout::Flex>
|
||||||
|
|
||||||
|
{{#if this.filterAlert}}
|
||||||
|
<Hds::Alert @type="compact" class="has-top-padding-xs" data-test-inline-alert as |A|>
|
||||||
|
<A.Description>{{this.filterAlert}}</A.Description>
|
||||||
|
</Hds::Alert>
|
||||||
|
{{/if}}
|
@ -7,42 +7,44 @@ import Component from '@glimmer/component';
|
|||||||
import { cached, tracked } from '@glimmer/tracking';
|
import { cached, tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { debounce } from '@ember/runloop';
|
import { debounce } from '@ember/runloop';
|
||||||
import { ClientFilters, type ClientFilterTypes, filterIsSupported } from 'core/utils/client-count-utils';
|
import { capitalize } from '@ember/string';
|
||||||
|
|
||||||
|
import { ClientFilters, type ClientFilterTypes } from 'core/utils/client-count-utils';
|
||||||
import type { HTMLElementEvent } from 'vault/forms';
|
import type { HTMLElementEvent } from 'vault/forms';
|
||||||
|
|
||||||
interface Args {
|
interface Args {
|
||||||
appliedFilters: Record<ClientFilterTypes, string>;
|
filterQueryParams: Record<ClientFilterTypes, string>;
|
||||||
// the dataset objects have more keys than the client filter types, but at minimum they have ClientFilterTypes
|
// Dataset objects technically have more keys than the client filter types, but at minimum they contain ClientFilterTypes
|
||||||
dataset: Record<ClientFilterTypes, string>[];
|
dataset: Record<ClientFilterTypes, string>[];
|
||||||
onFilter: CallableFunction;
|
onFilter: CallableFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Correspond to each search input's tracked variable in the component class
|
||||||
type SearchProperty = 'namespacePathSearch' | 'mountPathSearch' | 'mountTypeSearch';
|
type SearchProperty = 'namespacePathSearch' | 'mountPathSearch' | 'mountTypeSearch';
|
||||||
|
|
||||||
export default class ClientsFilterToolbar extends Component<Args> {
|
export default class ClientsFilterToolbar extends Component<Args> {
|
||||||
filterTypes = ClientFilters;
|
filterTypes = Object.values(ClientFilters);
|
||||||
|
|
||||||
|
// Tracked filter values
|
||||||
@tracked namespace_path: string;
|
@tracked namespace_path: string;
|
||||||
@tracked mount_path: string;
|
@tracked mount_path: string;
|
||||||
@tracked mount_type: string;
|
@tracked mount_type: string;
|
||||||
|
|
||||||
|
// Tracked search inputs
|
||||||
@tracked namespacePathSearch = '';
|
@tracked namespacePathSearch = '';
|
||||||
@tracked mountPathSearch = '';
|
@tracked mountPathSearch = '';
|
||||||
@tracked mountTypeSearch = '';
|
@tracked mountTypeSearch = '';
|
||||||
|
|
||||||
constructor(owner: unknown, args: Args) {
|
constructor(owner: unknown, args: Args) {
|
||||||
super(owner, args);
|
super(owner, args);
|
||||||
const { namespace_path, mount_path, mount_type } = this.args.appliedFilters;
|
const { namespace_path, mount_path, mount_type } = this.args.filterQueryParams;
|
||||||
this.namespace_path = namespace_path || '';
|
this.namespace_path = namespace_path || '';
|
||||||
this.mount_path = mount_path || '';
|
this.mount_path = mount_path || '';
|
||||||
this.mount_type = mount_type || '';
|
this.mount_type = mount_type || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
get anyFilters() {
|
get anyFilters() {
|
||||||
return (
|
return Object.values(this.filterProps).some((v) => !!v);
|
||||||
Object.keys(this.args.appliedFilters).every((f) => filterIsSupported(f)) &&
|
|
||||||
Object.values(this.args.appliedFilters).some((v) => !!v)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
@ -61,39 +63,72 @@ export default class ClientsFilterToolbar extends Component<Args> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[this.filterTypes.NAMESPACE]: [...namespacePaths],
|
[ClientFilters.NAMESPACE]: [...namespacePaths],
|
||||||
[this.filterTypes.MOUNT_PATH]: [...mountPaths],
|
[ClientFilters.MOUNT_PATH]: [...mountPaths],
|
||||||
[this.filterTypes.MOUNT_TYPE]: [...mountTypes],
|
[ClientFilters.MOUNT_TYPE]: [...mountTypes],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
get dropdownConfig() {
|
get dropdownConfig() {
|
||||||
return {
|
return {
|
||||||
[this.filterTypes.NAMESPACE]: {
|
[ClientFilters.NAMESPACE]: {
|
||||||
label: 'namespace',
|
label: 'namespace',
|
||||||
dropdownItems: this.dropdownItems[this.filterTypes.NAMESPACE],
|
dropdownItems: this.dropdownItems[ClientFilters.NAMESPACE],
|
||||||
searchProperty: 'namespacePathSearch',
|
searchProperty: 'namespacePathSearch',
|
||||||
},
|
},
|
||||||
[this.filterTypes.MOUNT_PATH]: {
|
[ClientFilters.MOUNT_PATH]: {
|
||||||
label: 'mount path',
|
label: 'mount path',
|
||||||
dropdownItems: this.dropdownItems[this.filterTypes.MOUNT_PATH],
|
dropdownItems: this.dropdownItems[ClientFilters.MOUNT_PATH],
|
||||||
searchProperty: 'mountPathSearch',
|
searchProperty: 'mountPathSearch',
|
||||||
},
|
},
|
||||||
[this.filterTypes.MOUNT_TYPE]: {
|
[ClientFilters.MOUNT_TYPE]: {
|
||||||
label: 'mount type',
|
label: 'mount type',
|
||||||
dropdownItems: this.dropdownItems[this.filterTypes.MOUNT_TYPE],
|
dropdownItems: this.dropdownItems[ClientFilters.MOUNT_TYPE],
|
||||||
searchProperty: 'mountTypeSearch',
|
searchProperty: 'mountTypeSearch',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It's possible that a query param may not exist in the dropdown, in which case show an alert
|
||||||
|
get filterAlert() {
|
||||||
|
const alert = (label: string, filter: string) =>
|
||||||
|
`${capitalize(label)} "${filter}" not found in the current data.`;
|
||||||
|
return this.filterTypes
|
||||||
|
.flatMap((f: ClientFilters) => {
|
||||||
|
const filterValue = this.filterProps[f];
|
||||||
|
const inDropdown = this.dropdownItems[f].includes(filterValue);
|
||||||
|
return !inDropdown && filterValue ? [alert(this.dropdownConfig[f].label, filterValue)] : [];
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// the cached decorator recomputes this getter every time the tracked properties
|
||||||
|
// update instead of every time it is accessed
|
||||||
|
@cached
|
||||||
|
get filterProps() {
|
||||||
|
return this.filterTypes.reduce(
|
||||||
|
(obj, filterType) => {
|
||||||
|
obj[filterType] = this[filterType];
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{} as Record<ClientFilterTypes, string>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateFilter(filterProperty: ClientFilterTypes, value: string, close: CallableFunction) {
|
handleFilterSelect(filterProperty: ClientFilterTypes, value: string, close: CallableFunction) {
|
||||||
this[filterProperty] = value;
|
this[filterProperty] = value;
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleDropdownClose(searchProperty: SearchProperty) {
|
||||||
|
// reset search input for that dropdown
|
||||||
|
this.updateSearch(searchProperty, '');
|
||||||
|
this.applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
clearFilters(filterProperty: ClientFilterTypes | '') {
|
clearFilters(filterProperty: ClientFilterTypes | '') {
|
||||||
if (filterProperty) {
|
if (filterProperty) {
|
||||||
@ -103,17 +138,13 @@ export default class ClientsFilterToolbar extends Component<Args> {
|
|||||||
this.mount_path = '';
|
this.mount_path = '';
|
||||||
this.mount_type = '';
|
this.mount_type = '';
|
||||||
}
|
}
|
||||||
// Fire callback so URL query params update when filters are cleared
|
|
||||||
this.applyFilters();
|
this.applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
applyFilters() {
|
applyFilters() {
|
||||||
this.args.onFilter({
|
// Fire callback so URL query params match selected filters
|
||||||
namespace_path: this.namespace_path,
|
this.args.onFilter(this.filterProps);
|
||||||
mount_path: this.mount_path,
|
|
||||||
mount_type: this.mount_type,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -3,16 +3,53 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
}}
|
}}
|
||||||
|
|
||||||
<Clients::CountsCard @title="Client counts by path">
|
<Clients::CountsCard data-test-card="Export activity data">
|
||||||
<:subheader>
|
<:subheader>
|
||||||
<Clients::FilterToolbar
|
<Clients::FilterToolbar
|
||||||
@dataset={{@exportData}}
|
@dataset={{@exportData}}
|
||||||
@onFilter={{this.handleFilter}}
|
@onFilter={{this.handleFilter}}
|
||||||
@appliedFilters={{@filterQueryParams}}
|
@filterQueryParams={{@filterQueryParams}}
|
||||||
/>
|
/>
|
||||||
</:subheader>
|
</:subheader>
|
||||||
|
|
||||||
<:table>
|
<:table>
|
||||||
{{! table }}
|
<Hds::Tabs @onClickTab={{this.onClickTab}} @selectedTabIndex={{this.selectedTabIndex}} as |T|>
|
||||||
|
{{#each-in this.exportDataByTab as |tabName exportData|}}
|
||||||
|
{{#let (this.filterData exportData) as |tableData|}}
|
||||||
|
<T.Tab @count={{or tableData.length "0"}} data-test-tab={{tabName}}>{{tabName}}</T.Tab>
|
||||||
|
<T.Panel>
|
||||||
|
<div class="has-top-margin-xs">
|
||||||
|
{{#if this.anyFilters}}
|
||||||
|
<Hds::Text::Body @tag="p" @color="faint" class="has-bottom-margin-xs" data-test-table-summary={{tabName}}>
|
||||||
|
Summary:
|
||||||
|
{{pluralize tableData.length "client"}}
|
||||||
|
{{if (eq tableData.length 1) "matches" "match"}}
|
||||||
|
the filter criteria.
|
||||||
|
</Hds::Text::Body>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{! Elements "behind" tabs always render on the DOM and are just superficially hidden/shown. }}
|
||||||
|
{{! The export data can be many rows so for performance only render the currently selected tab }}
|
||||||
|
{{#if (eq tabName this.selectedTab)}}
|
||||||
|
<Clients::Table
|
||||||
|
data-test-table="attribution"
|
||||||
|
@data={{tableData}}
|
||||||
|
@columns={{this.tableColumns tabName}}
|
||||||
|
@setPageSize={{50}}
|
||||||
|
@showPaginationSizeSelector={{true}}
|
||||||
|
>
|
||||||
|
<:emptyState>
|
||||||
|
<Hds::ApplicationState as |A|>
|
||||||
|
<A.Header @title="No data found" />
|
||||||
|
<A.Body @text="Clear or change filters to view client count data." />
|
||||||
|
</Hds::ApplicationState>
|
||||||
|
</:emptyState>
|
||||||
|
</Clients::Table>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</T.Panel>
|
||||||
|
{{/let}}
|
||||||
|
{{/each-in}}
|
||||||
|
</Hds::Tabs>
|
||||||
</:table>
|
</:table>
|
||||||
</Clients::CountsCard>
|
</Clients::CountsCard>
|
@ -3,6 +3,103 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ActivityComponent from '../activity';
|
import ActivityComponent, { Args } from '../activity';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { HTMLElementEvent } from 'vault/forms';
|
||||||
|
|
||||||
export default class ClientsClientListPageComponent extends ActivityComponent {}
|
import { filterIsSupported, filterTableData, type ActivityExportData } from 'core/utils/client-count-utils';
|
||||||
|
|
||||||
|
// Define the base mapping to derive types from
|
||||||
|
const CLIENT_TYPE_MAP = {
|
||||||
|
entity: 'Entity',
|
||||||
|
'non-entity-token': 'Non-entity',
|
||||||
|
'pki-acme': 'ACME',
|
||||||
|
'secret-sync': 'Secret sync',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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<ClientListTabs, ActivityExportData[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstTab = Object.keys(this.exportDataByTab)[0] as ClientListTabs;
|
||||||
|
this.selectedTab = firstTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedTabIndex() {
|
||||||
|
return Object.keys(this.exportDataByTab).indexOf(this.selectedTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only render tabs for whatever the export data returns
|
||||||
|
get tabs(): ClientListTabs[] {
|
||||||
|
return Object.keys(this.exportDataByTab) as ClientListTabs[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onClickTab(_event: HTMLElementEvent<HTMLInputElement>, idx: number) {
|
||||||
|
const tab = this.tabs[idx];
|
||||||
|
this.selectedTab = tab ?? this.tabs[0]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
get anyFilters() {
|
||||||
|
return (
|
||||||
|
Object.keys(this.args.filterQueryParams).every((f) => filterIsSupported(f)) &&
|
||||||
|
Object.values(this.args.filterQueryParams).some((v) => !!v)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TEMPLATE HELPERS
|
||||||
|
filterData = (dataset: ActivityExportData[]) => filterTableData(dataset, this.args.filterQueryParams);
|
||||||
|
|
||||||
|
tableColumns(tab: ClientListTabs) {
|
||||||
|
// all client types have values for these columns
|
||||||
|
const defaultColumns = [
|
||||||
|
{ key: 'client_id', label: 'Client ID' },
|
||||||
|
{ key: 'client_type', label: 'Client type' },
|
||||||
|
{ key: 'namespace_path', label: 'Namespace path' },
|
||||||
|
{ key: 'namespace_id', label: 'Namespace ID' },
|
||||||
|
{
|
||||||
|
key: 'client_first_used_time',
|
||||||
|
label: 'Initial usage',
|
||||||
|
tooltip: 'When the client ID was first used in the selected billing period.',
|
||||||
|
},
|
||||||
|
{ key: 'mount_path', label: 'Mount path' },
|
||||||
|
{ key: 'mount_type', label: 'Mount type' },
|
||||||
|
{ key: 'mount_accessor', label: 'Mount accessor' },
|
||||||
|
];
|
||||||
|
// these params only have value for "entity" client types
|
||||||
|
const entityOnly = [
|
||||||
|
{
|
||||||
|
key: 'entity_name',
|
||||||
|
label: 'Entity name',
|
||||||
|
tooltip: 'Entity name will be empty in the case of a deleted entity.',
|
||||||
|
},
|
||||||
|
{ key: 'entity_alias_name', label: 'Entity alias name' },
|
||||||
|
{ key: 'local_entity_alias', label: 'Local entity alias' },
|
||||||
|
{ key: 'policies', label: 'Policies' },
|
||||||
|
{ key: 'entity_metadata', label: 'Entity metadata' },
|
||||||
|
{ key: 'entity_alias_metadata', label: 'Entity alias metadata' },
|
||||||
|
{ key: 'entity_alias_custom_metadata', label: 'Entity alias custom metadata' },
|
||||||
|
{ key: 'entity_group_ids', label: 'Entity group IDs' },
|
||||||
|
];
|
||||||
|
return tab === 'Entity' ? [...defaultColumns, ...entityOnly] : defaultColumns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless is-shadowless">
|
<div class="has-top-bottom-margin">
|
||||||
|
|
||||||
{{#if (eq @activity.id "no-data")}}
|
{{#if (eq @activity.id "no-data")}}
|
||||||
<Clients::NoData @config={{@config}} @dateRangeMessage={{this.dateRangeMessage}} />
|
<Clients::NoData @config={{@config}} @dateRangeMessage={{this.dateRangeMessage}} />
|
||||||
@ -71,7 +71,10 @@
|
|||||||
</Hds::Alert>
|
</Hds::Alert>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<Clients::Counts::NavBar />
|
{{#if this.version.isEnterprise}}
|
||||||
|
{{! The "Client list" tab only renders for enterprise versions so there is no need for the nav bar }}
|
||||||
|
<Clients::Counts::NavBar />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{! CLIENT COUNT PAGE COMPONENTS RENDER HERE }}
|
{{! CLIENT COUNT PAGE COMPONENTS RENDER HERE }}
|
||||||
{{yield}}
|
{{yield}}
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
<Clients::FilterToolbar
|
<Clients::FilterToolbar
|
||||||
@dataset={{this.activityData}}
|
@dataset={{this.activityData}}
|
||||||
@onFilter={{this.handleFilter}}
|
@onFilter={{this.handleFilter}}
|
||||||
@appliedFilters={{@filterQueryParams}}
|
@filterQueryParams={{@filterQueryParams}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -21,8 +21,15 @@
|
|||||||
{{#let (get B.data key) as |value|}}
|
{{#let (get B.data key) as |value|}}
|
||||||
{{#if (and (eq key "mount_type") (eq value "deleted mount"))}}
|
{{#if (and (eq key "mount_type") (eq value "deleted mount"))}}
|
||||||
<B.Td data-test-table-data={{key}}><Hds::Badge @text="Deleted" @type="outlined" /></B.Td>
|
<B.Td data-test-table-data={{key}}><Hds::Badge @text="Deleted" @type="outlined" /></B.Td>
|
||||||
|
{{else if (eq key "client_id")}}
|
||||||
|
<B.Td data-test-table-data={{key}} class="white-space-nowrap">
|
||||||
|
<Hds::Copy::Snippet @textToCopy={{value}} @color="secondary" />
|
||||||
|
</B.Td>
|
||||||
{{else}}
|
{{else}}
|
||||||
<B.Td data-test-table-data={{key}}>{{value}}</B.Td>
|
<B.Td class="white-space-nowrap" data-test-table-data={{key}}>
|
||||||
|
{{! stringify value if it is an array or object, otherwise render directly }}
|
||||||
|
{{if (this.isObject value) (stringify value) value}}
|
||||||
|
</B.Td>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/let}}
|
{{/let}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
@ -129,4 +129,7 @@ export default class ClientsTable extends Component<Args> {
|
|||||||
this.sortColumn = column;
|
this.sortColumn = column;
|
||||||
this.sortDirection = direction;
|
this.sortDirection = direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TEMPLATE HELPERS
|
||||||
|
isObject = (value: any) => typeof value === 'object';
|
||||||
}
|
}
|
||||||
|
@ -41,10 +41,7 @@ Router.map(function () {
|
|||||||
this.route('clients', function () {
|
this.route('clients', function () {
|
||||||
this.route('counts', function () {
|
this.route('counts', function () {
|
||||||
this.route('overview');
|
this.route('overview');
|
||||||
// TODO remove this conditional when client count feature work for 1.21 is complete
|
this.route('client-list');
|
||||||
if (config.environment !== 'production') {
|
|
||||||
this.route('client-list');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
this.route('config');
|
this.route('config');
|
||||||
this.route('edit');
|
this.route('edit');
|
||||||
|
@ -88,7 +88,7 @@ $fourth: #6cc5b0;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lineal-chart-bar {
|
.lineal-chart-bar {
|
||||||
fill: var(--token-color-palette-single);
|
fill: $single;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lineal-axis {
|
.lineal-axis {
|
||||||
|
@ -117,6 +117,10 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.white-space-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
// screen reader only
|
// screen reader only
|
||||||
.sr-only {
|
.sr-only {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -35,6 +35,12 @@ export enum ClientFilters {
|
|||||||
|
|
||||||
export type ClientFilterTypes = (typeof ClientFilters)[keyof typeof ClientFilters];
|
export type ClientFilterTypes = (typeof ClientFilters)[keyof typeof ClientFilters];
|
||||||
|
|
||||||
|
// client_type in the exported activity data differs slightly from the types of client keys
|
||||||
|
// returned by sys/internal/counters/activity endpoint (:
|
||||||
|
export const EXPORT_CLIENT_TYPES = ['non-entity-token', 'pki-acme', 'secret-sync', 'entity'] as const;
|
||||||
|
|
||||||
|
export type ActivityExportClientTypes = (typeof EXPORT_CLIENT_TYPES)[number];
|
||||||
|
|
||||||
// returns array of VersionHistoryModels for noteworthy upgrades: 1.9, 1.10
|
// returns array of VersionHistoryModels for noteworthy upgrades: 1.9, 1.10
|
||||||
// that occurred between timestamps (i.e. queried activity data)
|
// that occurred between timestamps (i.e. queried activity data)
|
||||||
export const filterVersionHistory = (
|
export const filterVersionHistory = (
|
||||||
@ -159,10 +165,16 @@ export const sortMonthsByTimestamp = (monthsArray: ActivityMonthBlock[]) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterTableData = (
|
// *Performance note*
|
||||||
data: MountClients[],
|
// The client dashboard renders dropdown lists that specify filters. When the user selects a dropdown item (filter)
|
||||||
|
// it updates the query param and this method is called to filter the data passed to the displayed table.
|
||||||
|
// This method is not doing anything computationally expensive so it should be fine for filtering up to 50K rows of data.
|
||||||
|
// If activity data (either the by_namespace list or rows of data in the activity export API) grow past that, then we
|
||||||
|
// will want to look at converting this to a restartable task or do something else :)
|
||||||
|
export function filterTableData(
|
||||||
|
data: MountClients[] | ActivityExportData[],
|
||||||
filters: Record<ClientFilterTypes, string>
|
filters: Record<ClientFilterTypes, string>
|
||||||
): MountClients[] => {
|
): MountClients[] | ActivityExportData[] {
|
||||||
// Return original data if no filters are specified
|
// Return original data if no filters are specified
|
||||||
if (!filters || Object.values(filters).every((v) => !v)) {
|
if (!filters || Object.values(filters).every((v) => !v)) {
|
||||||
return data;
|
return data;
|
||||||
@ -174,9 +186,25 @@ export const filterTableData = (
|
|||||||
// If no filter is specified for that key, return true
|
// If no filter is specified for that key, return true
|
||||||
if (!filterValue) return true;
|
if (!filterValue) return true;
|
||||||
// Otherwise only return true if the datum matches the filter
|
// Otherwise only return true if the datum matches the filter
|
||||||
return datum[filterKey as ClientFilterTypes] === filterValue;
|
return matchesFilter(datum, filterKey as ClientFilterTypes, filterValue);
|
||||||
});
|
});
|
||||||
});
|
}) as typeof data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesFilter = (
|
||||||
|
datum: MountClients | ActivityExportData,
|
||||||
|
filterKey: ClientFilterTypes,
|
||||||
|
filterValue: string
|
||||||
|
) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return datumValue === filterValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const flattenMounts = (namespaceArray: ByNamespaceClients[]) =>
|
export const flattenMounts = (namespaceArray: ByNamespaceClients[]) =>
|
||||||
@ -273,7 +301,7 @@ export interface MountNewClients extends TotalClientsSometimes {
|
|||||||
// Serialized data from activity/export API
|
// Serialized data from activity/export API
|
||||||
export interface ActivityExportData {
|
export interface ActivityExportData {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
client_type: string;
|
client_type: ActivityExportClientTypes;
|
||||||
namespace_id: string;
|
namespace_id: string;
|
||||||
namespace_path: string;
|
namespace_path: string;
|
||||||
mount_accessor: string;
|
mount_accessor: string;
|
||||||
|
@ -84,7 +84,7 @@ function generateNamespaceBlock(idx = 0, isLowerCounts = false, ns, skipCounts =
|
|||||||
const max = isLowerCounts ? 100 : 1000;
|
const max = isLowerCounts ? 100 : 1000;
|
||||||
const nsBlock = {
|
const nsBlock = {
|
||||||
namespace_id: ns?.namespace_id || (idx === 0 ? 'root' : Math.random().toString(36).slice(2, 7) + idx),
|
namespace_id: ns?.namespace_id || (idx === 0 ? 'root' : Math.random().toString(36).slice(2, 7) + idx),
|
||||||
namespace_path: ns?.namespace_path || (idx === 0 ? '' : `ns${idx}`),
|
namespace_path: ns?.namespace_path || (idx === 0 ? '' : `ns${idx}/`),
|
||||||
counts: {},
|
counts: {},
|
||||||
mounts: {},
|
mounts: {},
|
||||||
};
|
};
|
||||||
@ -295,15 +295,17 @@ export default function (server) {
|
|||||||
const activities = schema['clients/activities'];
|
const activities = schema['clients/activities'];
|
||||||
const namespace = req.requestHeaders['X-Vault-Namespace'];
|
const namespace = req.requestHeaders['X-Vault-Namespace'];
|
||||||
let { start_time, end_time } = req.queryParams;
|
let { start_time, end_time } = req.queryParams;
|
||||||
if (!start_time && !end_time) {
|
if (!start_time) {
|
||||||
// if there are no date query params, the activity log default behavior
|
// if there are no date query params, the activity log default behavior
|
||||||
// queries from the builtin license start timestamp to the current month
|
// queries from the builtin license start timestamp to the current month
|
||||||
start_time = LICENSE_START.toISOString();
|
start_time = LICENSE_START.toISOString();
|
||||||
|
}
|
||||||
|
if (!end_time) {
|
||||||
end_time = STATIC_NOW.toISOString();
|
end_time = STATIC_NOW.toISOString();
|
||||||
}
|
}
|
||||||
// backend returns a timestamp if given unix time, so first convert to timestamp string here
|
// backend returns a timestamp if given unix time, so first convert to timestamp string here
|
||||||
if (!start_time.includes('T')) start_time = fromUnixTime(start_time).toISOString();
|
if (!start_time?.includes('T')) start_time = fromUnixTime(start_time).toISOString();
|
||||||
if (!end_time.includes('T')) end_time = fromUnixTime(end_time).toISOString();
|
if (!end_time?.includes('T')) end_time = fromUnixTime(end_time).toISOString();
|
||||||
|
|
||||||
const record = activities.findBy({ start_time, end_time });
|
const record = activities.findBy({ start_time, end_time });
|
||||||
let data;
|
let data;
|
||||||
|
@ -8,7 +8,7 @@ import { setupApplicationTest } from 'ember-qunit';
|
|||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import clientsHandler, { STATIC_NOW } from 'vault/mirage/handlers/clients';
|
import clientsHandler, { STATIC_NOW } from 'vault/mirage/handlers/clients';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { visit, click, currentURL, fillIn } from '@ember/test-helpers';
|
import { visit, currentURL } from '@ember/test-helpers';
|
||||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||||
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
|
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||||
@ -43,35 +43,6 @@ module('Acceptance | clients | counts', function (hooks) {
|
|||||||
assert.strictEqual(currentURL(), '/vault/clients/counts/overview', 'Redirects to counts overview route');
|
assert.strictEqual(currentURL(), '/vault/clients/counts/overview', 'Redirects to counts overview route');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should persist filter query params between child routes', async function (assert) {
|
|
||||||
this.owner.lookup('service:version').type = 'community';
|
|
||||||
await visit('/vault/clients/counts/overview');
|
|
||||||
await click(CLIENT_COUNT.dateRange.edit);
|
|
||||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2023-03');
|
|
||||||
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), '2023-10');
|
|
||||||
await click(GENERAL.submitButton);
|
|
||||||
assert.strictEqual(
|
|
||||||
currentURL(),
|
|
||||||
'/vault/clients/counts/overview?end_time=1698710400&start_time=1677628800',
|
|
||||||
'Start and end times added as query params'
|
|
||||||
);
|
|
||||||
|
|
||||||
await click(GENERAL.tab('client list'));
|
|
||||||
assert.strictEqual(
|
|
||||||
currentURL(),
|
|
||||||
'/vault/clients/counts/client-list?end_time=1698710400&start_time=1677628800',
|
|
||||||
'Start and end times persist through child route change'
|
|
||||||
);
|
|
||||||
|
|
||||||
await click(GENERAL.navLink('Dashboard'));
|
|
||||||
await click(GENERAL.navLink('Client Count'));
|
|
||||||
assert.strictEqual(
|
|
||||||
currentURL(),
|
|
||||||
'/vault/clients/counts/overview',
|
|
||||||
'Query params are reset when exiting route'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should render empty state if no permission to query activity data', async function (assert) {
|
test('it should render empty state if no permission to query activity data', async function (assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
server.get('/sys/internal/counters/activity', () => {
|
server.get('/sys/internal/counters/activity', () => {
|
||||||
|
@ -20,6 +20,10 @@ module('Acceptance | clients | counts | client list', function (hooks) {
|
|||||||
setupMirage(hooks);
|
setupMirage(hooks);
|
||||||
|
|
||||||
hooks.beforeEach(async function () {
|
hooks.beforeEach(async function () {
|
||||||
|
// This tab is hidden on community so the version is stubbed for consistent test running on either version
|
||||||
|
this.version = this.owner.lookup('service:version');
|
||||||
|
this.version.type === 'enterprise';
|
||||||
|
|
||||||
// The activity export endpoint returns a ReadableStream of json lines, this is not easily mocked using mirage.
|
// The activity export endpoint returns a ReadableStream of json lines, this is not easily mocked using mirage.
|
||||||
// Stubbing the adapter method return instead.
|
// Stubbing the adapter method return instead.
|
||||||
const mockResponse = {
|
const mockResponse = {
|
||||||
@ -39,6 +43,11 @@ module('Acceptance | clients | counts | client list', function (hooks) {
|
|||||||
this.exportDataStub.restore();
|
this.exportDataStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it hides client list tab on community', async function (assert) {
|
||||||
|
this.version.type === 'community';
|
||||||
|
assert.dom(GENERAL.tab('client list')).doesNotExist();
|
||||||
|
});
|
||||||
|
|
||||||
test('it navigates to client list tab', async function (assert) {
|
test('it navigates to client list tab', async function (assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
await click(GENERAL.navLink('Client Count'));
|
await click(GENERAL.navLink('Client Count'));
|
||||||
@ -51,7 +60,7 @@ module('Acceptance | clients | counts | client list', function (hooks) {
|
|||||||
|
|
||||||
test('filters are preset if URL includes query params', async function (assert) {
|
test('filters are preset if URL includes query params', async function (assert) {
|
||||||
assert.expect(4);
|
assert.expect(4);
|
||||||
const ns = 'test-ns-2/';
|
const ns = 'ns2/';
|
||||||
const mPath = 'auth/userpass/';
|
const mPath = 'auth/userpass/';
|
||||||
const mType = 'userpass';
|
const mType = 'userpass';
|
||||||
await visit(
|
await visit(
|
||||||
@ -65,7 +74,7 @@ module('Acceptance | clients | counts | client list', function (hooks) {
|
|||||||
|
|
||||||
test('selecting filters update URL query params', async function (assert) {
|
test('selecting filters update URL query params', async function (assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
const ns = 'test-ns-2/';
|
const ns = 'ns2/';
|
||||||
const mPath = 'auth/userpass/';
|
const mPath = 'auth/userpass/';
|
||||||
const mType = 'userpass';
|
const mType = 'userpass';
|
||||||
const url = '/vault/clients/counts/client-list';
|
const url = '/vault/clients/counts/client-list';
|
||||||
@ -80,7 +89,6 @@ module('Acceptance | clients | counts | client list', function (hooks) {
|
|||||||
// select mount type
|
// select mount type
|
||||||
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||||
await click(FILTERS.dropdownItem(mType));
|
await click(FILTERS.dropdownItem(mType));
|
||||||
await click(GENERAL.button('Apply filters'));
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`${url}?mount_path=${encodeURIComponent(mPath)}&mount_type=${mType}&namespace_path=${encodeURIComponent(
|
`${url}?mount_path=${encodeURIComponent(mPath)}&mount_type=${mType}&namespace_path=${encodeURIComponent(
|
||||||
|
@ -196,7 +196,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||||||
await click(FILTERS.dropdownItem(mount_path));
|
await click(FILTERS.dropdownItem(mount_path));
|
||||||
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||||
await click(FILTERS.dropdownItem(mount_type));
|
await click(FILTERS.dropdownItem(mount_type));
|
||||||
await click(GENERAL.button('Apply filters'));
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`${url}?mount_path=${encodeURIComponent(
|
`${url}?mount_path=${encodeURIComponent(
|
||||||
@ -222,7 +221,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||||||
await click(FILTERS.dropdownItem(mount_path));
|
await click(FILTERS.dropdownItem(mount_path));
|
||||||
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||||
await click(FILTERS.dropdownItem(mount_type));
|
await click(FILTERS.dropdownItem(mount_type));
|
||||||
await click(GENERAL.button('Apply filters'));
|
|
||||||
assert.dom(GENERAL.tableRow()).exists({ count: 1 }, 'it only renders the filtered table row');
|
assert.dom(GENERAL.tableRow()).exists({ count: 1 }, 'it only renders the filtered table row');
|
||||||
await click(FILTERS.clearTag(namespace_path));
|
await click(FILTERS.clearTag(namespace_path));
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
@ -254,7 +252,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||||||
await click(FILTERS.dropdownItem(mount_path));
|
await click(FILTERS.dropdownItem(mount_path));
|
||||||
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||||
await click(FILTERS.dropdownItem(mount_type));
|
await click(FILTERS.dropdownItem(mount_type));
|
||||||
await click(GENERAL.button('Apply filters'));
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`${url}?mount_path=${encodeURIComponent(
|
`${url}?mount_path=${encodeURIComponent(
|
||||||
|
@ -1119,25 +1119,61 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ACTIVITY_EXPORT_STUB = `
|
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":"","entity_alias_name":"","local_entity_alias":false,"client_id":"46dcOXXH+P1VEQiKTQjtWXEtBlbHdMOWwz+svXf3xuU=","client_type":"non-entity-token","namespace_id":"whUNi","namespace_path":"test-ns-2/","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":"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":"","entity_alias_name":"","local_entity_alias":false,"client_id":"VKAJVITyTwyqF1GUzwYHwkaK6bbnL1zN8ZJ7viKR8no=","client_type":"non-entity-token","namespace_id":"omjn8","namespace_path":"test-ns-8/","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":"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"]}
|
||||||
|
`;
|
||||||
|
|
||||||
|
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":"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":"test-ns-1/","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":"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":"test-ns-5/","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":"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":"test-ns-7/","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":"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":"test-ns-9/","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":"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":"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":"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":"test-ns-4/","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":"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":"test-ns-6/","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":"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":"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":"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":"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":"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":"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":"test-ns-3/","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":"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":[]}
|
{"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":[]}
|
||||||
{"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"]}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
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 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":[]}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ACTIVITY_EXPORT_STUB = ENTITY_EXPORT + NON_ENTITY_EXPORT + ACME_EXPORT + SECRET_SYNC_EXPORT;
|
||||||
|
@ -27,11 +27,7 @@ export const CLIENT_COUNT = {
|
|||||||
statTextValue: (label: string) =>
|
statTextValue: (label: string) =>
|
||||||
label ? `[data-test-stat-text="${label}"] .stat-value` : '[data-test-stat-text]',
|
label ? `[data-test-stat-text="${label}"] .stat-value` : '[data-test-stat-text]',
|
||||||
usageStats: (title: string) => `[data-test-usage-stats="${title}"]`,
|
usageStats: (title: string) => `[data-test-usage-stats="${title}"]`,
|
||||||
filterBar: '[data-test-clients-filter-bar]',
|
tableSummary: (tabName: string) => `[data-test-table-summary="${tabName}"]`,
|
||||||
nsFilter: '#namespace-search-select',
|
|
||||||
mountFilter: '#mounts-search-select',
|
|
||||||
selectedAuthMount: 'div#mounts-search-select [data-test-selected-option] div',
|
|
||||||
selectedNs: 'div#namespace-search-select [data-test-selected-option] div',
|
|
||||||
upgradeWarning: '[data-test-clients-upgrade-warning]',
|
upgradeWarning: '[data-test-clients-upgrade-warning]',
|
||||||
exportButton: '[data-test-export-button]',
|
exportButton: '[data-test-export-button]',
|
||||||
};
|
};
|
||||||
@ -60,5 +56,6 @@ export const FILTERS = {
|
|||||||
dropdownSearch: (name: string) => `[data-test-dropdown="${name}"] input`,
|
dropdownSearch: (name: string) => `[data-test-dropdown="${name}"] input`,
|
||||||
tag: (filter?: string, value?: string) =>
|
tag: (filter?: string, value?: string) =>
|
||||||
filter && value ? `[data-test-filter-tag="${filter} ${value}"]` : '[data-test-filter-tag]',
|
filter && value ? `[data-test-filter-tag="${filter} ${value}"]` : '[data-test-filter-tag]',
|
||||||
|
tagContainer: '[data-test-filter-tag-container]',
|
||||||
clearTag: (value: string) => `[aria-label="Dismiss ${value}"]`,
|
clearTag: (value: string) => `[aria-label="Dismiss ${value}"]`,
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||||
import { render, triggerEvent } from '@ember/test-helpers';
|
import { findAll, render, triggerEvent } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
|
||||||
const EXAMPLE = [
|
const EXAMPLE = [
|
||||||
@ -40,12 +40,29 @@ module('Integration | Component | clients/charts/vertical-bar-basic', function (
|
|||||||
|
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
this.data = EXAMPLE;
|
this.data = EXAMPLE;
|
||||||
|
this.showTable = false;
|
||||||
|
this.renderComponent = async () => {
|
||||||
|
await render(
|
||||||
|
hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" @chartTitle="My chart" @showTable={{this.showTable}} />`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders bars the expected color', async function (assert) {
|
||||||
|
await this.renderComponent();
|
||||||
|
// the first bar has no data and doesn't render so get the second one
|
||||||
|
const bars = findAll('.lineal-chart-bar');
|
||||||
|
const actualColor = getComputedStyle(bars[1]).fill;
|
||||||
|
const expectedColor = 'rgb(28, 52, 95)';
|
||||||
|
assert.strictEqual(
|
||||||
|
actualColor,
|
||||||
|
expectedColor,
|
||||||
|
`actual color: ${actualColor}, expected color: ${expectedColor}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders when some months have no data', async function (assert) {
|
test('it renders when some months have no data', async function (assert) {
|
||||||
await render(
|
await this.renderComponent();
|
||||||
hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" @chartTitle="My chart"/>`
|
|
||||||
);
|
|
||||||
assert.dom('[data-test-chart="My chart"]').exists('renders chart container');
|
assert.dom('[data-test-chart="My chart"]').exists('renders chart container');
|
||||||
assert.dom('[data-test-vertical-bar]').exists({ count: 3 }, 'renders 3 vertical bars');
|
assert.dom('[data-test-vertical-bar]').exists({ count: 3 }, 'renders 3 vertical bars');
|
||||||
|
|
||||||
@ -88,9 +105,7 @@ module('Integration | Component | clients/charts/vertical-bar-basic', function (
|
|||||||
secret_syncs: 0,
|
secret_syncs: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await render(
|
await this.renderComponent();
|
||||||
hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" @chartTitle="My chart"/>`
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.dom('[data-test-chart="My chart"]').exists('renders chart container');
|
assert.dom('[data-test-chart="My chart"]').exists('renders chart container');
|
||||||
assert.dom('[data-test-vertical-bar]').exists({ count: 2 }, 'renders 2 vertical bars');
|
assert.dom('[data-test-vertical-bar]').exists({ count: 2 }, 'renders 2 vertical bars');
|
||||||
@ -108,9 +123,8 @@ module('Integration | Component | clients/charts/vertical-bar-basic', function (
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it renders underlying data', async function (assert) {
|
test('it renders underlying data', async function (assert) {
|
||||||
await render(
|
this.showTable = true;
|
||||||
hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" @showTable={{true}} @chartTitle="My chart"/>`
|
await this.renderComponent();
|
||||||
);
|
|
||||||
assert.dom('[data-test-chart="My chart"]').exists('renders chart container');
|
assert.dom('[data-test-chart="My chart"]').exists('renders chart container');
|
||||||
assert.dom('[data-test-underlying-data]').exists('renders underlying data when showTable=true');
|
assert.dom('[data-test-underlying-data]').exists('renders underlying data when showTable=true');
|
||||||
assert
|
assert
|
||||||
|
@ -16,6 +16,7 @@ const EXAMPLE = [
|
|||||||
fuji_apples: null,
|
fuji_apples: null,
|
||||||
gala_apples: null,
|
gala_apples: null,
|
||||||
red_delicious: null,
|
red_delicious: null,
|
||||||
|
honey_crisp: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamp: '2022-10-01T00:00:00',
|
timestamp: '2022-10-01T00:00:00',
|
||||||
@ -23,6 +24,7 @@ const EXAMPLE = [
|
|||||||
fuji_apples: 1471,
|
fuji_apples: 1471,
|
||||||
gala_apples: 4389,
|
gala_apples: 4389,
|
||||||
red_delicious: 4207,
|
red_delicious: 4207,
|
||||||
|
honey_crisp: 1234,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamp: '2022-11-01T00:00:00',
|
timestamp: '2022-11-01T00:00:00',
|
||||||
@ -30,6 +32,7 @@ const EXAMPLE = [
|
|||||||
fuji_apples: 149,
|
fuji_apples: 149,
|
||||||
gala_apples: 20,
|
gala_apples: 20,
|
||||||
red_delicious: 5802,
|
red_delicious: 5802,
|
||||||
|
honey_crisp: 134,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -42,13 +45,42 @@ module('Integration | Component | clients/charts/vertical-bar-stacked', function
|
|||||||
{ key: 'fuji_apples', label: 'Fuji counts' },
|
{ key: 'fuji_apples', label: 'Fuji counts' },
|
||||||
{ key: 'gala_apples', label: 'Gala counts' },
|
{ key: 'gala_apples', label: 'Gala counts' },
|
||||||
];
|
];
|
||||||
|
this.showTable = false;
|
||||||
|
this.renderComponent = async () => {
|
||||||
|
await render(
|
||||||
|
hbs`<Clients::Charts::VerticalBarStacked @data={{this.data}} @chartLegend={{this.legend}} @chartTitle="My chart" @showTable={{this.showTable}} />`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders bars the expected color', async function (assert) {
|
||||||
|
this.legend = [
|
||||||
|
{ key: 'fuji_apples', label: 'Fuji counts' },
|
||||||
|
{ key: 'gala_apples', label: 'Gala counts' },
|
||||||
|
{ key: 'red_delicious', label: 'Red Delicious counts' },
|
||||||
|
{ key: 'honey_crisp', label: 'Honey Crisp counts' },
|
||||||
|
];
|
||||||
|
await this.renderComponent();
|
||||||
|
const barClasses = ['.stacked-bar-1', '.stacked-bar-2', '.stacked-bar-3', '.stacked-bar-4'];
|
||||||
|
const expectedFills = [
|
||||||
|
'rgb(66, 105, 208)',
|
||||||
|
'rgb(239, 177, 23)',
|
||||||
|
'rgb(255, 114, 92)',
|
||||||
|
'rgb(108, 197, 176)',
|
||||||
|
];
|
||||||
|
barClasses.forEach((className, idx) => {
|
||||||
|
const bars = findAll(className);
|
||||||
|
// Skip the first set of bars because they have no data
|
||||||
|
const bar = bars[1];
|
||||||
|
const actual = getComputedStyle(bar).fill;
|
||||||
|
const expected = expectedFills[idx];
|
||||||
|
assert.strictEqual(actual, expected, `${className} has expected fill color: ${expected}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders when some months have no data', async function (assert) {
|
test('it renders when some months have no data', async function (assert) {
|
||||||
assert.expect(10);
|
assert.expect(10);
|
||||||
await render(
|
await this.renderComponent();
|
||||||
hbs`<Clients::Charts::VerticalBarStacked @data={{this.data}} @chartLegend={{this.legend}} @chartTitle="My chart"/>`
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.dom(CHARTS.chart('My chart')).exists('renders chart container');
|
assert.dom(CHARTS.chart('My chart')).exists('renders chart container');
|
||||||
|
|
||||||
@ -106,10 +138,7 @@ module('Integration | Component | clients/charts/vertical-bar-stacked', function
|
|||||||
red_delicious: 180,
|
red_delicious: 180,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await render(
|
await this.renderComponent();
|
||||||
hbs`<Clients::Charts::VerticalBarStacked @data={{this.data}} @chartLegend={{this.legend}} @chartTitle="My chart"/>`
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.dom(CHARTS.chart('My chart')).exists('renders chart container');
|
assert.dom(CHARTS.chart('My chart')).exists('renders chart container');
|
||||||
findAll(CHARTS.verticalBar).forEach((b, idx) =>
|
findAll(CHARTS.verticalBar).forEach((b, idx) =>
|
||||||
assert.dom(b).isNotVisible(`bar: ${idx} does not render`)
|
assert.dom(b).isNotVisible(`bar: ${idx} does not render`)
|
||||||
@ -132,9 +161,8 @@ module('Integration | Component | clients/charts/vertical-bar-stacked', function
|
|||||||
|
|
||||||
test('it renders underlying data', async function (assert) {
|
test('it renders underlying data', async function (assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
await render(
|
this.showTable = true;
|
||||||
hbs`<Clients::Charts::VerticalBarStacked @data={{this.data}} @chartLegend={{this.legend}} @showTable={{true}} @chartTitle="My chart"/>`
|
await this.renderComponent();
|
||||||
);
|
|
||||||
assert.dom(CHARTS.chart('My chart')).exists('renders chart container');
|
assert.dom(CHARTS.chart('My chart')).exists('renders chart container');
|
||||||
assert.dom(CHARTS.table).exists('renders underlying data when showTable=true');
|
assert.dom(CHARTS.table).exists('renders underlying data when showTable=true');
|
||||||
assert
|
assert
|
||||||
|
@ -25,19 +25,19 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
|||||||
{ namespace_path: 'ns1/', mount_type: 'ns_token/', mount_path: 'auth/token/' },
|
{ namespace_path: 'ns1/', mount_type: 'ns_token/', mount_path: 'auth/token/' },
|
||||||
];
|
];
|
||||||
this.onFilter = sinon.spy();
|
this.onFilter = sinon.spy();
|
||||||
this.appliedFilters = { namespace_path: '', mount_path: '', mount_type: '' };
|
this.filterQueryParams = { namespace_path: '', mount_path: '', mount_type: '' };
|
||||||
|
|
||||||
this.renderComponent = async () => {
|
this.renderComponent = async () => {
|
||||||
await render(hbs`
|
await render(hbs`
|
||||||
<Clients::FilterToolbar
|
<Clients::FilterToolbar
|
||||||
@dataset={{this.dataset}}
|
@dataset={{this.dataset}}
|
||||||
@onFilter={{this.onFilter}}
|
@onFilter={{this.onFilter}}
|
||||||
@appliedFilters={{this.appliedFilters}}
|
@filterQueryParams={{this.filterQueryParams}}
|
||||||
/>`);
|
/>`);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.presetFilters = () => {
|
this.presetFilters = () => {
|
||||||
this.appliedFilters = {
|
this.filterQueryParams = {
|
||||||
namespace_path: 'admin/',
|
namespace_path: 'admin/',
|
||||||
mount_path: 'auth/userpass-root/',
|
mount_path: 'auth/userpass-root/',
|
||||||
mount_type: 'token/',
|
mount_type: 'token/',
|
||||||
@ -63,10 +63,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
|||||||
assert.dom(FILTERS.dropdownToggle(ClientFilters.NAMESPACE)).hasText('Namespace');
|
assert.dom(FILTERS.dropdownToggle(ClientFilters.NAMESPACE)).hasText('Namespace');
|
||||||
assert.dom(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH)).hasText('Mount path');
|
assert.dom(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH)).hasText('Mount path');
|
||||||
assert.dom(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE)).hasText('Mount type');
|
assert.dom(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE)).hasText('Mount type');
|
||||||
assert.dom(GENERAL.button('Apply filters')).exists();
|
assert.dom(FILTERS.tagContainer).hasText('Filters applied: None');
|
||||||
assert
|
|
||||||
.dom(GENERAL.button('Clear filters'))
|
|
||||||
.doesNotExist('"Clear filters" button does not render when filters are unset');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders dropdown items and does not include duplicates', async function (assert) {
|
test('it renders dropdown items and does not include duplicates', async function (assert) {
|
||||||
@ -138,7 +135,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
|||||||
assert.dom('ul').hasText('userpass/ token/ ns_token/', 'it resets filter and renders all mount types');
|
assert.dom('ul').hasText('userpass/ token/ ns_token/', 'it resets filter and renders all mount types');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it searches renders no matches found message', async function (assert) {
|
test('it searches and renders no matches found message', async function (assert) {
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
|
|
||||||
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||||
@ -171,7 +168,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
|||||||
assert.dom('ul').hasText('No mount types to filter');
|
assert.dom('ul').hasText('No mount types to filter');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders no items to filter if dataset is missing expected keys', async function (assert) {
|
test('it renders no items to filter if dataset does not contain expected keys', async function (assert) {
|
||||||
this.dataset = [{ foo: null, bar: null, baz: null }];
|
this.dataset = [{ foo: null, bar: null, baz: null }];
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||||
@ -182,9 +179,26 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
|||||||
assert.dom('ul').hasText('No mount types to filter');
|
assert.dom('ul').hasText('No mount types to filter');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it selects dropdown items', async function (assert) {
|
test('it selects dropdown items and renders a filter tag', async function (assert) {
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
await this.selectFilters();
|
|
||||||
|
// select namespace
|
||||||
|
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||||
|
await click(FILTERS.dropdownItem('admin/'));
|
||||||
|
assert.dom(FILTERS.tag(ClientFilters.NAMESPACE, 'admin/')).exists();
|
||||||
|
assert.dom(FILTERS.tag()).exists({ count: 1 }, '1 filter tag renders');
|
||||||
|
|
||||||
|
// select mount path
|
||||||
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
|
||||||
|
await click(FILTERS.dropdownItem('auth/userpass-root/'));
|
||||||
|
assert.dom(FILTERS.tag(ClientFilters.MOUNT_PATH, 'auth/userpass-root/')).exists();
|
||||||
|
assert.dom(FILTERS.tag()).exists({ count: 2 }, '2 filter tags render');
|
||||||
|
|
||||||
|
// select mount type
|
||||||
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||||
|
await click(FILTERS.dropdownItem('token/'));
|
||||||
|
assert.dom(FILTERS.tag(ClientFilters.MOUNT_TYPE, 'token/')).exists();
|
||||||
|
assert.dom(FILTERS.tag()).exists({ count: 3 }, '3 filter tags render');
|
||||||
|
|
||||||
// dropdown closes when an item is selected, reopen each one to assert the correct item is selected
|
// dropdown closes when an item is selected, reopen each one to assert the correct item is selected
|
||||||
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||||
@ -200,48 +214,33 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
|||||||
assert.dom(`${FILTERS.dropdownItem('token/')} ${GENERAL.icon('check')}`).exists();
|
assert.dom(`${FILTERS.dropdownItem('token/')} ${GENERAL.icon('check')}`).exists();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it applies filters when no filters are set', async function (assert) {
|
test('it fires callback when a filter is selected', async function (assert) {
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
await this.selectFilters();
|
|
||||||
|
|
||||||
await click(GENERAL.button('Apply filters'));
|
// select namespace
|
||||||
const [obj] = this.onFilter.lastCall.args;
|
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||||
assert.strictEqual(
|
await click(FILTERS.dropdownItem('admin/'));
|
||||||
obj[ClientFilters.NAMESPACE],
|
let lastCall = this.onFilter.lastCall.args[0];
|
||||||
'admin/',
|
// this.filterQueryParams has empty values for each filter type
|
||||||
`onFilter callback has expected "${ClientFilters.NAMESPACE}"`
|
let expectedObject = { ...this.filterQueryParams, [ClientFilters.NAMESPACE]: 'admin/' };
|
||||||
);
|
assert.propEqual(lastCall, expectedObject, `callback includes value for ${ClientFilters.NAMESPACE}`);
|
||||||
assert.strictEqual(
|
|
||||||
obj[ClientFilters.MOUNT_PATH],
|
// select mount path
|
||||||
'auth/userpass-root/',
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
|
||||||
`onFilter callback has expected "${ClientFilters.MOUNT_PATH}"`
|
await click(FILTERS.dropdownItem('auth/userpass-root/'));
|
||||||
);
|
lastCall = this.onFilter.lastCall.args[0];
|
||||||
assert.strictEqual(
|
expectedObject = { ...expectedObject, [ClientFilters.MOUNT_PATH]: 'auth/userpass-root/' };
|
||||||
obj[ClientFilters.MOUNT_TYPE],
|
assert.propEqual(lastCall, expectedObject, `callback includes value for ${ClientFilters.MOUNT_PATH}`);
|
||||||
'token/',
|
|
||||||
`onFilter callback has expected "${ClientFilters.MOUNT_TYPE}"`
|
// select mount type
|
||||||
);
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||||
|
await click(FILTERS.dropdownItem('token/'));
|
||||||
|
lastCall = this.onFilter.lastCall.args[0];
|
||||||
|
expectedObject = { ...expectedObject, [ClientFilters.MOUNT_TYPE]: 'token/' };
|
||||||
|
assert.propEqual(lastCall, expectedObject, `callback includes value for ${ClientFilters.MOUNT_TYPE}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it applies updated filters when filters are preset', async function (assert) {
|
test('it renders filter tags when initialized with @filterQueryParams', async function (assert) {
|
||||||
this.appliedFilters = { namespace_path: 'ns1', mount_path: 'auth/token/', mount_type: 'ns_token/' };
|
|
||||||
await this.renderComponent();
|
|
||||||
// Check initial filters
|
|
||||||
await click(GENERAL.button('Apply filters'));
|
|
||||||
const [beforeUpdate] = this.onFilter.lastCall.args;
|
|
||||||
assert.propEqual(beforeUpdate, this.appliedFilters, 'callback fires with preset filters');
|
|
||||||
// Change filters and confirm callback has updated values
|
|
||||||
await this.selectFilters();
|
|
||||||
await click(GENERAL.button('Apply filters'));
|
|
||||||
const [afterUpdate] = this.onFilter.lastCall.args;
|
|
||||||
assert.propEqual(
|
|
||||||
afterUpdate,
|
|
||||||
{ namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/' },
|
|
||||||
'callback fires with updated selection'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it renders a tag for each filter', async function (assert) {
|
|
||||||
this.presetFilters();
|
this.presetFilters();
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
|
|
||||||
@ -249,23 +248,27 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
|||||||
assert.dom(FILTERS.tag(ClientFilters.NAMESPACE, 'admin/')).exists();
|
assert.dom(FILTERS.tag(ClientFilters.NAMESPACE, 'admin/')).exists();
|
||||||
assert.dom(FILTERS.tag(ClientFilters.MOUNT_PATH, 'auth/userpass-root/')).exists();
|
assert.dom(FILTERS.tag(ClientFilters.MOUNT_PATH, 'auth/userpass-root/')).exists();
|
||||||
assert.dom(FILTERS.tag(ClientFilters.MOUNT_TYPE, 'token/')).exists();
|
assert.dom(FILTERS.tag(ClientFilters.MOUNT_TYPE, 'token/')).exists();
|
||||||
assert
|
|
||||||
.dom(GENERAL.button('Clear filters'))
|
|
||||||
.exists('"Clear filters" button renders when filters are present');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it resets all filters', async function (assert) {
|
test('it updates filters tags when initialized with @filterQueryParams', async function (assert) {
|
||||||
|
this.filterQueryParams = { namespace_path: 'ns1/', mount_path: 'auth/token/', mount_type: 'ns_token/' };
|
||||||
|
await this.renderComponent();
|
||||||
|
// Check initial filters
|
||||||
|
assert.dom(FILTERS.tagContainer).hasText('Filters applied: ns1/ auth/token/ ns_token/');
|
||||||
|
// Change filters and confirm callback has updated values
|
||||||
|
await this.selectFilters();
|
||||||
|
const [afterUpdate] = this.onFilter.lastCall.args;
|
||||||
|
assert.propEqual(
|
||||||
|
afterUpdate,
|
||||||
|
{ namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/' },
|
||||||
|
'callback fires with updated selection'
|
||||||
|
);
|
||||||
|
assert.dom(FILTERS.tagContainer).hasText('Filters applied: admin/ auth/userpass-root/ token/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it clears all filters', async function (assert) {
|
||||||
this.presetFilters();
|
this.presetFilters();
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
// first check that filters have preset values
|
|
||||||
await click(GENERAL.button('Apply filters'));
|
|
||||||
const [beforeClear] = this.onFilter.lastCall.args;
|
|
||||||
assert.propEqual(
|
|
||||||
beforeClear,
|
|
||||||
{ namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/' },
|
|
||||||
'callback fires with preset filters'
|
|
||||||
);
|
|
||||||
// now clear filters and confirm values are cleared
|
|
||||||
await click(GENERAL.button('Clear filters'));
|
await click(GENERAL.button('Clear filters'));
|
||||||
const [afterClear] = this.onFilter.lastCall.args;
|
const [afterClear] = this.onFilter.lastCall.args;
|
||||||
assert.propEqual(
|
assert.propEqual(
|
||||||
@ -273,19 +276,12 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
|||||||
{ namespace_path: '', mount_path: '', mount_type: '' },
|
{ namespace_path: '', mount_path: '', mount_type: '' },
|
||||||
'onFilter callback has empty values when "Clear filters" is clicked'
|
'onFilter callback has empty values when "Clear filters" is clicked'
|
||||||
);
|
);
|
||||||
|
assert.dom(FILTERS.tagContainer).hasText('Filters applied: None');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it clears individual filters', async function (assert) {
|
test('it clears individual filters', async function (assert) {
|
||||||
this.presetFilters();
|
this.presetFilters();
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
// first check that filters have preset values
|
|
||||||
await click(GENERAL.button('Apply filters'));
|
|
||||||
const [beforeClear] = this.onFilter.lastCall.args;
|
|
||||||
assert.propEqual(
|
|
||||||
beforeClear,
|
|
||||||
{ namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/' },
|
|
||||||
'callback fires with preset filters'
|
|
||||||
);
|
|
||||||
await click(FILTERS.clearTag('admin/'));
|
await click(FILTERS.clearTag('admin/'));
|
||||||
const afterClear = this.onFilter.lastCall.args[0];
|
const afterClear = this.onFilter.lastCall.args[0];
|
||||||
assert.propEqual(
|
assert.propEqual(
|
||||||
@ -295,12 +291,11 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it only renders tags for supported filters', async function (assert) {
|
test('it renders an alert when initialized with @filterQueryParams that are not present in the dropdown', async function (assert) {
|
||||||
this.appliedFilters = { start_time: '2025-08-31T23:59:59Z' };
|
this.filterQueryParams = { namespace_path: 'admin/', mount_path: '', mount_type: 'banana' };
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
assert
|
assert.dom(FILTERS.tag()).exists({ count: 2 }, '2 filter tags render');
|
||||||
.dom(GENERAL.button('Clear filters'))
|
assert.dom(FILTERS.tagContainer).hasText('Filters applied: admin/ banana');
|
||||||
.doesNotExist('"Clear filters" button does not render when filters are unset');
|
assert.dom(GENERAL.inlineAlert).hasText(`Mount type "banana" not found in the current data.`);
|
||||||
assert.dom(FILTERS.tag()).doesNotExist();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
263
ui/tests/integration/components/clients/page/client-list-test.js
Normal file
263
ui/tests/integration/components/clients/page/client-list-test.js
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||||
|
import { click, find, findAll, render } from '@ember/test-helpers';
|
||||||
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
import { ACTIVITY_EXPORT_STUB } from 'vault/tests/helpers/clients/client-count-helpers';
|
||||||
|
import { CLIENT_COUNT, FILTERS } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||||
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { ClientFilters } from 'core/utils/client-count-utils';
|
||||||
|
|
||||||
|
const EXPORT_TAB_TO_TYPE = {
|
||||||
|
Entity: 'entity',
|
||||||
|
'Non-entity': 'non-entity-token',
|
||||||
|
ACME: 'pki-acme',
|
||||||
|
'Secret sync': 'secret-sync',
|
||||||
|
};
|
||||||
|
|
||||||
|
module('Integration | Component | clients/page/client-list', function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(async function () {
|
||||||
|
this.exportData = ACTIVITY_EXPORT_STUB.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => JSON.parse(line));
|
||||||
|
this.onFilterChange = sinon.spy();
|
||||||
|
this.filterQueryParams = { namespace_path: '', mount_path: '', mount_type: '' };
|
||||||
|
|
||||||
|
this.expectedData = (type, { key, value } = {}) =>
|
||||||
|
this.exportData.filter((d) => {
|
||||||
|
const isClientType = d.client_type === type;
|
||||||
|
return key && value ? isClientType && d[key] === value : isClientType;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.expectedOptions = (type) => [...new Set(this.exportData.map((m) => m[type]))];
|
||||||
|
this.expectedNamespaces = this.expectedOptions('namespace_path');
|
||||||
|
this.expectedMountPaths = this.expectedOptions('mount_path');
|
||||||
|
this.expectedMountTypes = this.expectedOptions('mount_type');
|
||||||
|
|
||||||
|
this.renderComponent = () =>
|
||||||
|
render(hbs`
|
||||||
|
<Clients::Page::ClientList
|
||||||
|
@exportData={{this.exportData}}
|
||||||
|
@onFilterChange={{this.onFilterChange}}
|
||||||
|
@filterQueryParams={{this.filterQueryParams}}
|
||||||
|
/>`);
|
||||||
|
|
||||||
|
// Filter key is one of ClientFilterTypes
|
||||||
|
this.assertTabData = async (assert, filterKey, filterValue) => {
|
||||||
|
// Iterate over each tab and assert rendered table data
|
||||||
|
for (const [tabName, clientType] of Object.entries(EXPORT_TAB_TO_TYPE)) {
|
||||||
|
const expectedData = this.expectedData(clientType, { key: filterKey, value: filterValue });
|
||||||
|
const length = expectedData.length;
|
||||||
|
await click(GENERAL.hdsTab(tabName));
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.hdsTab(tabName))
|
||||||
|
.hasText(`${tabName} ${length}`, `${tabName} tab counts match dataset length`);
|
||||||
|
const noun = length === 1 ? 'client' : 'clients';
|
||||||
|
const verb = length === 1 ? 'matches' : 'match';
|
||||||
|
assert
|
||||||
|
.dom(CLIENT_COUNT.tableSummary(tabName))
|
||||||
|
.hasText(`Summary: ${length} ${noun} ${verb} the filter criteria.`);
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.hdsTab(tabName))
|
||||||
|
.hasAttribute('aria-selected', 'true', `it selects the tab: ${tabName}`);
|
||||||
|
assert.dom(GENERAL.tableRow()).exists({ count: length });
|
||||||
|
|
||||||
|
// Find all rendered rows and assert they satisfy the filter value and client IDs match
|
||||||
|
const rows = findAll(GENERAL.tableRow());
|
||||||
|
rows.forEach((_, idx) => {
|
||||||
|
assert.dom(GENERAL.tableData(idx, filterKey)).hasText(filterValue);
|
||||||
|
const clientId = find(GENERAL.tableData(idx, 'client_id')).innerText;
|
||||||
|
// Make sure the rendered client id exists in the expected data
|
||||||
|
const isValid = expectedData.find((d) => d.client_id === clientId);
|
||||||
|
assert.true(!!isValid, `client_id: ${clientId} exists in expected dataset`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders export data by client type in tabs organized by client type', async function (assert) {
|
||||||
|
await this.renderComponent();
|
||||||
|
assert.dom(GENERAL.hdsTab('Entity')).hasAttribute('aria-selected', 'true', 'the first tab is selected');
|
||||||
|
|
||||||
|
for (const [tabName, clientType] of Object.entries(EXPORT_TAB_TO_TYPE)) {
|
||||||
|
const expectedData = this.expectedData(clientType);
|
||||||
|
await click(GENERAL.hdsTab(tabName));
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.hdsTab(tabName))
|
||||||
|
.hasText(`${tabName} ${expectedData.length}`, `${tabName} tab counts match dataset length`);
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.hdsTab(tabName))
|
||||||
|
.hasAttribute('aria-selected', 'true', `it selects the tab: ${tabName}`);
|
||||||
|
|
||||||
|
// Find all rendered rows and assert they match the client type tab
|
||||||
|
const rows = findAll(GENERAL.tableRow());
|
||||||
|
rows.forEach((_, idx) => {
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.tableData(idx, 'client_type'))
|
||||||
|
.hasText(clientType, `it renders ${clientType} data when ${tabName} is selected`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders expected columns for each client type', async function (assert) {
|
||||||
|
const expectedColumns = (isEntity = false) => {
|
||||||
|
const base = [
|
||||||
|
{ label: 'Client ID' },
|
||||||
|
{ label: 'Client type' },
|
||||||
|
{ label: 'Namespace path' },
|
||||||
|
{ label: 'Namespace ID' },
|
||||||
|
{ label: 'Initial usage More information for' }, // renders a tooltip which is why "More information for" is included
|
||||||
|
{ label: 'Mount path' },
|
||||||
|
{ label: 'Mount type' },
|
||||||
|
{ label: 'Mount accessor' },
|
||||||
|
];
|
||||||
|
const entityOnly = [
|
||||||
|
{ label: 'Entity name More information for' }, // renders a tooltip which is why "More information for" is included
|
||||||
|
{ label: 'Entity alias name' },
|
||||||
|
{ label: 'Local entity alias' },
|
||||||
|
{ label: 'Policies' },
|
||||||
|
{ label: 'Entity metadata' },
|
||||||
|
{ label: 'Entity alias metadata' },
|
||||||
|
{ label: 'Entity alias custom metadata' },
|
||||||
|
{ label: 'Entity group IDs' },
|
||||||
|
];
|
||||||
|
return isEntity ? [...base, ...entityOnly] : base;
|
||||||
|
};
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
|
for (const tabName of Object.keys(EXPORT_TAB_TO_TYPE)) {
|
||||||
|
await click(GENERAL.hdsTab(tabName));
|
||||||
|
expectedColumns(tabName === 'Entity').forEach((col, idx) => {
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.tableColumnHeader(idx + 1, { isAdvanced: true }))
|
||||||
|
.hasText(col.label, `${tabName} renders ${col.label} column`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders dropdown lists from activity response to filter table data', async function (assert) {
|
||||||
|
await this.renderComponent();
|
||||||
|
// Select each filter
|
||||||
|
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||||
|
findAll(`${FILTERS.dropdown(ClientFilters.NAMESPACE)} li button`).forEach((item, idx) => {
|
||||||
|
const expected = this.expectedNamespaces[idx] === '' ? 'root' : this.expectedNamespaces[idx];
|
||||||
|
assert.dom(item).hasText(expected, `namespace dropdown renders: ${expected}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
|
||||||
|
findAll(`${FILTERS.dropdown(ClientFilters.MOUNT_PATH)} li button`).forEach((item, idx) => {
|
||||||
|
const expected = this.expectedMountPaths[idx];
|
||||||
|
assert.dom(item).hasText(expected, `mount_path dropdown renders: ${expected}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||||
|
findAll(`${FILTERS.dropdown(ClientFilters.MOUNT_TYPE)} li button`).forEach((item, idx) => {
|
||||||
|
const expected = this.expectedMountTypes[idx];
|
||||||
|
assert.dom(item).hasText(expected, `mount_type dropdown renders: ${expected}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it fires @onFilterChange when filters are selected', async function (assert) {
|
||||||
|
const ns = 'root';
|
||||||
|
const { mount_path, mount_type } = this.exportData[0];
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
|
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||||
|
await click(FILTERS.dropdownItem(ns));
|
||||||
|
// select mount path
|
||||||
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
|
||||||
|
await click(FILTERS.dropdownItem(mount_path));
|
||||||
|
// select mount type
|
||||||
|
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||||
|
await click(FILTERS.dropdownItem(mount_type));
|
||||||
|
|
||||||
|
const [actual] = this.onFilterChange.lastCall.args;
|
||||||
|
assert.strictEqual(actual.namespace_path, ns, `@onFilterChange called with: ${ns}`);
|
||||||
|
assert.strictEqual(actual.mount_path, mount_path, `@onFilterChange called with: ${mount_path}`);
|
||||||
|
assert.strictEqual(actual.mount_type, mount_type, `@onFilterChange called with: ${mount_type}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// *FILTERING TESTS
|
||||||
|
test('it filters data if @filterQueryParams specify a namespace_path', async function (assert) {
|
||||||
|
const filterKey = 'namespace_path';
|
||||||
|
const filterValue = 'ns2/';
|
||||||
|
this.filterQueryParams[filterKey] = filterValue;
|
||||||
|
await this.renderComponent();
|
||||||
|
await this.assertTabData(assert, filterKey, filterValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it filters data if @filterQueryParams specify a mount_path', async function (assert) {
|
||||||
|
const filterKey = 'mount_path';
|
||||||
|
const filterValue = 'auth/token/';
|
||||||
|
this.filterQueryParams[filterKey] = filterValue;
|
||||||
|
await this.renderComponent();
|
||||||
|
await this.assertTabData(assert, filterKey, filterValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it filters data if @filterQueryParams specify a mount_type', async function (assert) {
|
||||||
|
const filterKey = 'mount_type';
|
||||||
|
const filterValue = 'auth/ns_token/';
|
||||||
|
this.filterQueryParams[filterKey] = filterValue;
|
||||||
|
await this.renderComponent();
|
||||||
|
await this.assertTabData(assert, filterKey, filterValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it filters data if @filterQueryParams specify a multiple filters', async function (assert) {
|
||||||
|
this.filterQueryParams = { namespace_path: 'ns5/', mount_path: 'auth/token/', mount_type: 'ns_token' };
|
||||||
|
const { namespace_path, mount_path, mount_type } = this.filterQueryParams;
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
|
for (const [tabName, clientType] of Object.entries(EXPORT_TAB_TO_TYPE)) {
|
||||||
|
const expectedData = this.expectedData(clientType).filter(
|
||||||
|
(d) =>
|
||||||
|
d.namespace_path == namespace_path && d.mount_path === mount_path && d.mount_type === mount_type
|
||||||
|
);
|
||||||
|
const length = expectedData.length;
|
||||||
|
await click(GENERAL.hdsTab(tabName));
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.hdsTab(tabName))
|
||||||
|
.hasText(`${tabName} ${length}`, `${tabName} tab counts match dataset length`);
|
||||||
|
const noun = length === 1 ? 'client' : 'clients';
|
||||||
|
const verb = length === 1 ? 'matches' : 'match';
|
||||||
|
assert
|
||||||
|
.dom(CLIENT_COUNT.tableSummary(tabName))
|
||||||
|
.hasText(`Summary: ${length} ${noun} ${verb} the filter criteria.`);
|
||||||
|
assert
|
||||||
|
.dom(GENERAL.hdsTab(tabName))
|
||||||
|
.hasAttribute('aria-selected', 'true', `it selects the tab: ${tabName}`);
|
||||||
|
assert.dom(GENERAL.tableRow()).exists({ count: length });
|
||||||
|
|
||||||
|
// Find all rendered rows and assert they satisfy the filter value and client IDs match
|
||||||
|
const rows = findAll(GENERAL.tableRow());
|
||||||
|
rows.forEach((_, idx) => {
|
||||||
|
assert.dom(GENERAL.tableData(idx, 'namespace_path')).hasText('ns5/');
|
||||||
|
assert.dom(GENERAL.tableData(idx, 'mount_path')).hasText('auth/token/');
|
||||||
|
assert.dom(GENERAL.tableData(idx, 'mount_type')).hasText('ns_token');
|
||||||
|
// client_id is the unique identifier for each row
|
||||||
|
const clientId = find(GENERAL.tableData(idx, 'client_id')).innerText;
|
||||||
|
// Make sure the rendered client id exists in the expected data
|
||||||
|
const isValid = expectedData.find((d) => d.client_id === clientId);
|
||||||
|
assert.true(!!isValid, `client_id: ${clientId} exists in expected dataset`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders empty state message when filter selections yield no results', async function (assert) {
|
||||||
|
this.filterQueryParams = { namespace_path: 'dev/', mount_path: 'pluto/', mount_type: 'banana' };
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
|
for (const tabName of Object.keys(EXPORT_TAB_TO_TYPE)) {
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||||
import { click, fillIn, findAll, render, triggerEvent } from '@ember/test-helpers';
|
import { click, fillIn, find, findAll, render, triggerEvent } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers';
|
import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers';
|
||||||
@ -38,6 +38,27 @@ module('Integration | Component | clients/page/overview', function (hooks) {
|
|||||||
@onFilterChange={{this.onFilterChange}}
|
@onFilterChange={{this.onFilterChange}}
|
||||||
@filterQueryParams={{this.filterQueryParams}}
|
@filterQueryParams={{this.filterQueryParams}}
|
||||||
/>`);
|
/>`);
|
||||||
|
|
||||||
|
this.assertTableData = async (assert, filterKey, filterValue) => {
|
||||||
|
const expectedData = flattenMounts(this.mostRecentMonth.new_clients.namespaces).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) => {
|
||||||
|
assert.dom(GENERAL.tableData(idx, filterKey)).hasText(filterValue);
|
||||||
|
// Get namespace and mount paths to find original data in expectedData
|
||||||
|
const rowMountPath = find(GENERAL.tableData(idx, 'mount_path')).innerText;
|
||||||
|
const rowNsPath = find(GENERAL.tableData(idx, 'namespace_path')).innerText;
|
||||||
|
// find the expected clients from the response and assert the table matches
|
||||||
|
const { clients: expectedClients } = expectedData.find(
|
||||||
|
(d) => d.mount_path === rowMountPath && d.namespace_path === rowNsPath
|
||||||
|
);
|
||||||
|
assert.dom(GENERAL.tableData(idx, 'clients')).hasText(`${expectedClients}`);
|
||||||
|
});
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it hides attribution when there is no data', async function (assert) {
|
test('it hides attribution when there is no data', async function (assert) {
|
||||||
@ -182,4 +203,56 @@ module('Integration | Component | clients/page/overview', function (hooks) {
|
|||||||
assert.dom(GENERAL.tableRow()).exists({ count: 5 }, '5 rows render');
|
assert.dom(GENERAL.tableRow()).exists({ count: 5 }, '5 rows render');
|
||||||
assert.dom(GENERAL.paginationSizeSelector).hasValue('5', 'size selector does not reset to 10');
|
assert.dom(GENERAL.paginationSizeSelector).hasValue('5', 'size selector does not reset to 10');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it filters data if @filterQueryParams specify a namespace_path', async function (assert) {
|
||||||
|
const filterKey = 'namespace_path';
|
||||||
|
const filterValue = 'ns1';
|
||||||
|
this.filterQueryParams[filterKey] = filterValue;
|
||||||
|
await this.renderComponent();
|
||||||
|
await this.assertTableData(assert, filterKey, filterValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it filters data if @filterQueryParams specify a mount_path', async function (assert) {
|
||||||
|
const filterKey = 'mount_path';
|
||||||
|
const filterValue = 'acme/pki/0';
|
||||||
|
this.filterQueryParams[filterKey] = filterValue;
|
||||||
|
await this.renderComponent();
|
||||||
|
await this.assertTableData(assert, filterKey, filterValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it filters data if @filterQueryParams specify a mount_type', async function (assert) {
|
||||||
|
const filterKey = 'mount_type';
|
||||||
|
const filterValue = 'kv';
|
||||||
|
this.filterQueryParams[filterKey] = filterValue;
|
||||||
|
await this.renderComponent();
|
||||||
|
await this.assertTableData(assert, filterKey, filterValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it filters data if @filterQueryParams specify a multiple filters', async function (assert) {
|
||||||
|
this.filterQueryParams = {
|
||||||
|
namespace_path: 'ns1',
|
||||||
|
mount_path: 'auth/userpass/0',
|
||||||
|
mount_type: 'userpass',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { namespace_path, mount_path, mount_type } = this.filterQueryParams;
|
||||||
|
await this.renderComponent();
|
||||||
|
await fillIn(GENERAL.selectByAttr('attribution-month'), this.mostRecentMonth.timestamp);
|
||||||
|
const expectedData = flattenMounts(this.mostRecentMonth.new_clients.namespaces).find(
|
||||||
|
(d) => d.namespace_path === namespace_path && d.mount_path === mount_path && d.mount_type === mount_type
|
||||||
|
);
|
||||||
|
assert.dom(GENERAL.tableRow()).exists({ count: 1 });
|
||||||
|
assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText(expectedData.namespace_path);
|
||||||
|
assert.dom(GENERAL.tableData(0, 'mount_path')).hasText(expectedData.mount_path);
|
||||||
|
assert.dom(GENERAL.tableData(0, 'mount_type')).hasText(expectedData.mount_type);
|
||||||
|
assert.dom(GENERAL.tableData(0, 'clients')).hasText(`${expectedData.clients}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders empty state message when filter selections yield no results', async function (assert) {
|
||||||
|
this.filterQueryParams = { namespace_path: 'dev/', mount_path: 'pluto/', mount_type: 'banana' };
|
||||||
|
await this.renderComponent();
|
||||||
|
assert
|
||||||
|
.dom(CLIENT_COUNT.card('table empty state'))
|
||||||
|
.hasText('No data found Clear or change filters to view client count data. Client count documentation');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,6 @@ import { getUnixTime } from 'date-fns';
|
|||||||
import { findAll } from '@ember/test-helpers';
|
import { findAll } from '@ember/test-helpers';
|
||||||
import { formatNumber } from 'core/helpers/format-number';
|
import { formatNumber } from 'core/helpers/format-number';
|
||||||
import timestamp from 'core/utils/timestamp';
|
import timestamp from 'core/utils/timestamp';
|
||||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
|
||||||
import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors';
|
import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||||
@ -46,12 +45,6 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
|||||||
/>
|
/>
|
||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
// Fails on #ember-testing-container
|
|
||||||
setRunOptions({
|
|
||||||
rules: {
|
|
||||||
'scrollable-region-focusable': { enabled: false },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders with full monthly activity data', async function (assert) {
|
test('it renders with full monthly activity data', async function (assert) {
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
ACTIVITY_RESPONSE_STUB as RESPONSE,
|
ACTIVITY_RESPONSE_STUB as RESPONSE,
|
||||||
MIXED_ACTIVITY_RESPONSE_STUB as MIXED_RESPONSE,
|
MIXED_ACTIVITY_RESPONSE_STUB as MIXED_RESPONSE,
|
||||||
SERIALIZED_ACTIVITY_RESPONSE,
|
SERIALIZED_ACTIVITY_RESPONSE,
|
||||||
|
ENTITY_EXPORT,
|
||||||
} from 'vault/tests/helpers/clients/client-count-helpers';
|
} from 'vault/tests/helpers/clients/client-count-helpers';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -463,5 +464,153 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||||||
assert.propEqual(noMatches, [], 'returns an empty array when no keys match dataset');
|
assert.propEqual(noMatches, [], 'returns an empty array when no keys match dataset');
|
||||||
this.assertOriginal(assert);
|
this.assertOriginal(assert);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it matches on empty strings or "root" for the root namespace', async function (assert) {
|
||||||
|
const mockExportData = ENTITY_EXPORT.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => JSON.parse(line));
|
||||||
|
const combinedData = [...this.mockMountData, ...mockExportData];
|
||||||
|
const filteredData = filterTableData(combinedData, { namespace_path: 'root' });
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
acme_clients: 0,
|
||||||
|
clients: 8091,
|
||||||
|
entity_clients: 4002,
|
||||||
|
label: 'auth/userpass/0',
|
||||||
|
mount_path: 'auth/userpass/0',
|
||||||
|
mount_type: 'userpass',
|
||||||
|
namespace_path: 'root',
|
||||||
|
non_entity_clients: 4089,
|
||||||
|
secret_syncs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acme_clients: 0,
|
||||||
|
clients: 4290,
|
||||||
|
entity_clients: 0,
|
||||||
|
label: 'secrets/kv/0',
|
||||||
|
mount_path: 'secrets/kv/0',
|
||||||
|
mount_type: 'kv',
|
||||||
|
namespace_path: 'root',
|
||||||
|
non_entity_clients: 0,
|
||||||
|
secret_syncs: 4290,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acme_clients: 4003,
|
||||||
|
clients: 4003,
|
||||||
|
entity_clients: 0,
|
||||||
|
label: 'acme/pki/0',
|
||||||
|
mount_path: 'acme/pki/0',
|
||||||
|
mount_type: 'pki',
|
||||||
|
namespace_path: 'root',
|
||||||
|
non_entity_clients: 0,
|
||||||
|
secret_syncs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
client_first_used_time: '2025-08-15T23:48:09Z',
|
||||||
|
client_id: '5692c6ef-c871-128e-fb06-df2be7bfc0db',
|
||||||
|
client_type: 'entity',
|
||||||
|
entity_alias_custom_metadata: {},
|
||||||
|
entity_alias_metadata: {},
|
||||||
|
entity_alias_name: 'bob',
|
||||||
|
entity_group_ids: ['7537e6b7-3b06-65c2-1fb2-c83116eb5e6f'],
|
||||||
|
entity_metadata: {},
|
||||||
|
entity_name: 'entity_b3e2a7ff',
|
||||||
|
local_entity_alias: false,
|
||||||
|
mount_accessor: 'auth_userpass_f47ad0b4',
|
||||||
|
mount_path: 'auth/userpass/',
|
||||||
|
mount_type: 'userpass',
|
||||||
|
namespace_id: 'root',
|
||||||
|
namespace_path: '',
|
||||||
|
policies: [],
|
||||||
|
token_creation_time: '2025-08-15T23:48:09Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
client_first_used_time: '2025-08-15T23:53:19Z',
|
||||||
|
client_id: '23a04911-5d72-ba98-11d3-527f2fcf3a81',
|
||||||
|
client_type: 'entity',
|
||||||
|
entity_alias_custom_metadata: {
|
||||||
|
account: 'Tester Account',
|
||||||
|
},
|
||||||
|
entity_alias_metadata: {},
|
||||||
|
entity_alias_name: 'bob',
|
||||||
|
entity_group_ids: ['7537e6b7-3b06-65c2-1fb2-c83116eb5e6f'],
|
||||||
|
entity_metadata: {
|
||||||
|
organization: 'ACME Inc.',
|
||||||
|
team: 'QA',
|
||||||
|
},
|
||||||
|
entity_name: 'bob-smith',
|
||||||
|
local_entity_alias: false,
|
||||||
|
mount_accessor: 'auth_userpass_de28062c',
|
||||||
|
mount_path: 'auth/userpass-test/',
|
||||||
|
mount_type: 'userpass',
|
||||||
|
namespace_id: 'root',
|
||||||
|
namespace_path: '',
|
||||||
|
policies: ['base'],
|
||||||
|
token_creation_time: '2025-08-15T23:52:38Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
client_first_used_time: '2025-08-16T09:16:03Z',
|
||||||
|
client_id: 'a7c8d912-4f61-23b5-88e4-627a3dcf2b92',
|
||||||
|
client_type: 'entity',
|
||||||
|
entity_alias_custom_metadata: {
|
||||||
|
role: 'Senior Engineer',
|
||||||
|
},
|
||||||
|
entity_alias_metadata: {
|
||||||
|
department: 'Engineering',
|
||||||
|
},
|
||||||
|
entity_alias_name: 'alice',
|
||||||
|
entity_group_ids: ['7537e6b7-3b06-65c2-1fb2-c83116eb5e6f', 'a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6'],
|
||||||
|
entity_metadata: {
|
||||||
|
location: 'San Francisco',
|
||||||
|
organization: 'TechCorp',
|
||||||
|
team: 'DevOps',
|
||||||
|
},
|
||||||
|
entity_name: 'alice-johnson',
|
||||||
|
local_entity_alias: false,
|
||||||
|
mount_accessor: 'auth_userpass_f47ad0b4',
|
||||||
|
mount_path: 'auth/userpass/',
|
||||||
|
mount_type: 'userpass',
|
||||||
|
namespace_id: 'root',
|
||||||
|
namespace_path: '',
|
||||||
|
policies: ['admin', 'audit'],
|
||||||
|
token_creation_time: '2025-08-16T09:15:42Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
client_first_used_time: '2025-08-17T16:44:12Z',
|
||||||
|
client_id: 'c6b9d248-5a71-39e4-c7f2-951d8eaf6b95',
|
||||||
|
client_type: 'entity',
|
||||||
|
entity_alias_custom_metadata: {
|
||||||
|
expertise: 'kubernetes',
|
||||||
|
on_call: 'true',
|
||||||
|
},
|
||||||
|
entity_alias_metadata: {
|
||||||
|
iss: 'https://auth.cloudops.io',
|
||||||
|
sub: 'frank.castle@cloudops.io',
|
||||||
|
},
|
||||||
|
entity_alias_name: 'frank',
|
||||||
|
entity_group_ids: ['9a8b7c6d-5e4f-3210-9876-543210fedcba'],
|
||||||
|
entity_metadata: {
|
||||||
|
organization: 'CloudOps',
|
||||||
|
region: 'us-east-1',
|
||||||
|
team: 'SRE',
|
||||||
|
},
|
||||||
|
entity_name: 'frank-castle',
|
||||||
|
local_entity_alias: false,
|
||||||
|
mount_accessor: 'auth_jwt_9d8c7b6a',
|
||||||
|
mount_path: 'auth/jwt/',
|
||||||
|
mount_type: 'jwt',
|
||||||
|
namespace_id: 'root',
|
||||||
|
namespace_path: '',
|
||||||
|
policies: ['operations', 'monitoring'],
|
||||||
|
token_creation_time: '2025-08-17T16:43:28Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
assert.propEqual(
|
||||||
|
filteredData,
|
||||||
|
expected,
|
||||||
|
"filtered data includes items with namespace_path equal to either 'root' or an empty string"
|
||||||
|
);
|
||||||
|
this.assertOriginal(assert);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user