mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 12:26:34 +02:00
* rename query params to match keys from api response * move type guard check to util * update color scheme * remove passing selected namespace to export activity data request * remove namespace and mount filter toolbar * update response, sort by client count number * remove default page size for testing * implement table and filters in overview tab * remove old query params * cleanup unused args * revert page header changes * update mirage, remove month from utils * update client count utils * one more color! * reset table to page 1; * workaround to force Hds::Pagination::Numbered to update when currentPage changes * add empty state test for no attribution * delete unused methods * add test for new utils * add changelog * rename changelog --------- Co-authored-by: claire bontempo <cbontempo@hashicorp.com>
This commit is contained in:
parent
1fcf55471d
commit
2b469deeca
@ -7,53 +7,35 @@
|
||||
// contains getters that filter and extract data from activity model for use in charts
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { cached } from '@glimmer/tracking';
|
||||
import { isSameMonth } from 'date-fns';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import {
|
||||
filterByMonthDataForMount,
|
||||
filteredTotalForMount,
|
||||
filterVersionHistory,
|
||||
} from 'core/utils/client-count-utils';
|
||||
import { service } from '@ember/service';
|
||||
import { sanitizePath } from 'core/utils/sanitize-path';
|
||||
|
||||
import type ClientsActivityModel from 'vault/models/clients/activity';
|
||||
import type ClientsVersionHistoryModel from 'vault/models/clients/version-history';
|
||||
import type { TotalClients } from 'core/utils/client-count-utils';
|
||||
import type { ClientFilterTypes } from 'core/utils/client-count-utils';
|
||||
import type NamespaceService from 'vault/services/namespace';
|
||||
|
||||
/* 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 */
|
||||
interface Args {
|
||||
activity: ClientsActivityModel;
|
||||
versionHistory: ClientsVersionHistoryModel[];
|
||||
startTimestamp: string;
|
||||
endTimestamp: string;
|
||||
namespace: string;
|
||||
mountPath: string;
|
||||
mountType: string;
|
||||
onFilterChange: CallableFunction;
|
||||
filterQueryParams: Record<ClientFilterTypes, string>;
|
||||
}
|
||||
|
||||
export default class ClientsActivityComponent extends Component<Args> {
|
||||
@service declare readonly namespace: NamespaceService;
|
||||
|
||||
// path of the filtered namespace OR current one, for filtering relevant data
|
||||
get namespacePathForFilter() {
|
||||
const { namespace } = this.args;
|
||||
const currentNs = this.namespace.currentNamespace;
|
||||
return sanitizePath(namespace || currentNs || 'root');
|
||||
}
|
||||
|
||||
@cached
|
||||
get byMonthNewClients() {
|
||||
const { activity, mountPath } = this.args;
|
||||
const nsPath = this.namespacePathForFilter;
|
||||
|
||||
const data = mountPath
|
||||
? filterByMonthDataForMount(activity.byMonth, nsPath, mountPath)
|
||||
: activity.byMonth;
|
||||
|
||||
return data ? data?.map((m) => m?.new_clients) : [];
|
||||
return this.args.activity.byMonth?.map((m) => m?.new_clients) || [];
|
||||
}
|
||||
|
||||
@cached
|
||||
get isCurrentMonth() {
|
||||
const { activity } = this.args;
|
||||
const current = parseAPITimestamp(activity.responseTimestamp) as Date;
|
||||
@ -62,6 +44,7 @@ export default class ClientsActivityComponent extends Component<Args> {
|
||||
return isSameMonth(start, current) && isSameMonth(end, current);
|
||||
}
|
||||
|
||||
@cached
|
||||
get isDateRange() {
|
||||
const { activity } = this.args;
|
||||
return !isSameMonth(
|
||||
@ -70,19 +53,14 @@ export default class ClientsActivityComponent extends Component<Args> {
|
||||
);
|
||||
}
|
||||
|
||||
// (object) top level TOTAL client counts for given date range
|
||||
get totalUsageCounts(): TotalClients {
|
||||
const { namespace, activity, mountPath } = this.args;
|
||||
// only do this if we have a mountPath filter.
|
||||
// namespace is filtered on API layer
|
||||
if (activity?.byNamespace && namespace && mountPath) {
|
||||
return filteredTotalForMount(activity.byNamespace, namespace, mountPath);
|
||||
}
|
||||
return activity?.total;
|
||||
@action
|
||||
handleFilter(filters: Record<ClientFilterTypes, string>) {
|
||||
const { namespace_path, mount_path, mount_type } = filters;
|
||||
this.args.onFilterChange({ namespace_path, mount_path, mount_type });
|
||||
}
|
||||
|
||||
get upgradesDuringActivity() {
|
||||
const { versionHistory, activity } = this.args;
|
||||
return filterVersionHistory(versionHistory, activity.startTime, activity.endTime);
|
||||
@action
|
||||
resetFilters() {
|
||||
this.handleFilter({ namespace_path: '', mount_path: '', mount_type: '' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
{{#each @namespaces as |namespace|}}
|
||||
<D.Checkmark
|
||||
{{on "click" (fn this.updateFilter this.filterTypes.NAMESPACE namespace D.close)}}
|
||||
@selected={{eq namespace this.nsLabel}}
|
||||
@selected={{eq namespace this.namespace_path}}
|
||||
data-test-dropdown-item={{namespace}}
|
||||
>{{namespace}}</D.Checkmark>
|
||||
{{/each}}
|
||||
@ -22,7 +22,7 @@
|
||||
{{#each @mountPaths as |mountPath|}}
|
||||
<D.Checkmark
|
||||
{{on "click" (fn this.updateFilter this.filterTypes.MOUNT_PATH mountPath D.close)}}
|
||||
@selected={{eq mountPath this.mountPath}}
|
||||
@selected={{eq mountPath this.mount_path}}
|
||||
data-test-dropdown-item={{mountPath}}
|
||||
>{{mountPath}}</D.Checkmark>
|
||||
{{/each}}
|
||||
@ -34,7 +34,7 @@
|
||||
{{#each @mountTypes as |mountType|}}
|
||||
<D.Checkmark
|
||||
{{on "click" (fn this.updateFilter this.filterTypes.MOUNT_TYPE mountType D.close)}}
|
||||
@selected={{eq mountType this.mountType}}
|
||||
@selected={{eq mountType this.mount_type}}
|
||||
data-test-dropdown-item={{mountType}}
|
||||
>{{mountType}}</D.Checkmark>
|
||||
{{/each}}
|
||||
@ -54,7 +54,7 @@
|
||||
<Hds::Text::Body @color="faint">Filters applied:</Hds::Text::Body>
|
||||
{{! render tags based on applied @filters and not the internally tracked properties }}
|
||||
{{#each-in @appliedFilters as |filter value|}}
|
||||
{{#if (and (this.supportedFilter filter) value)}}
|
||||
{{#if value}}
|
||||
<div>
|
||||
<Hds::Tag
|
||||
@text={{value}}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { ClientFilters, ClientFilterTypes } from 'core/utils/client-count-utils';
|
||||
import { ClientFilters, ClientFilterTypes, filterIsSupported } from 'core/utils/client-count-utils';
|
||||
|
||||
interface Args {
|
||||
onFilter: CallableFunction;
|
||||
@ -16,21 +16,21 @@ interface Args {
|
||||
export default class ClientsFilterToolbar extends Component<Args> {
|
||||
filterTypes = ClientFilters;
|
||||
|
||||
@tracked nsLabel = '';
|
||||
@tracked mountPath = '';
|
||||
@tracked mountType = '';
|
||||
@tracked namespace_path = '';
|
||||
@tracked mount_path = '';
|
||||
@tracked mount_type = '';
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
const { nsLabel, mountPath, mountType } = this.args.appliedFilters;
|
||||
this.nsLabel = nsLabel;
|
||||
this.mountPath = mountPath;
|
||||
this.mountType = mountType;
|
||||
const { namespace_path, mount_path, mount_type } = this.args.appliedFilters;
|
||||
this.namespace_path = namespace_path;
|
||||
this.mount_path = mount_path;
|
||||
this.mount_type = mount_type;
|
||||
}
|
||||
|
||||
get anyFilters() {
|
||||
return (
|
||||
Object.keys(this.args.appliedFilters).every((f) => this.supportedFilter(f)) &&
|
||||
Object.keys(this.args.appliedFilters).every((f) => filterIsSupported(f)) &&
|
||||
Object.values(this.args.appliedFilters).some((v) => !!v)
|
||||
);
|
||||
}
|
||||
@ -46,9 +46,9 @@ export default class ClientsFilterToolbar extends Component<Args> {
|
||||
if (filterKey) {
|
||||
this[filterKey] = '';
|
||||
} else {
|
||||
this.nsLabel = '';
|
||||
this.mountPath = '';
|
||||
this.mountType = '';
|
||||
this.namespace_path = '';
|
||||
this.mount_path = '';
|
||||
this.mount_type = '';
|
||||
}
|
||||
// Fire callback so URL query params update when filters are cleared
|
||||
this.applyFilters();
|
||||
@ -57,13 +57,9 @@ export default class ClientsFilterToolbar extends Component<Args> {
|
||||
@action
|
||||
applyFilters() {
|
||||
this.args.onFilter({
|
||||
nsLabel: this.nsLabel,
|
||||
mountPath: this.mountPath,
|
||||
mountType: this.mountType,
|
||||
namespace_path: this.namespace_path,
|
||||
mount_path: this.mount_path,
|
||||
mount_type: this.mount_type,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function
|
||||
supportedFilter = (f: string): f is ClientFilterTypes =>
|
||||
Object.values(this.filterTypes).includes(f as ClientFilterTypes);
|
||||
}
|
||||
|
||||
@ -26,7 +26,6 @@ import { task } from 'ember-concurrency';
|
||||
* @param {string} [startTimestamp] - ISO timestamp of start time, to be passed to export request
|
||||
* @param {string} [endTimestamp] - ISO timestamp of end time, to be passed to export request
|
||||
* @param {number} [retentionMonths = 48] - number of months for historical billing, to be passed to date picker
|
||||
* @param {string} [namespace] - namespace filter. Will be appended to the current namespace in the export request.
|
||||
* @param {string} [upgradesDuringActivity] - array of objects containing version history upgrade data
|
||||
* @param {boolean} [noData = false] - when true, export button will hide regardless of capabilities
|
||||
* @param {function} [onChange] - callback when a new date range is saved, to be passed to date picker
|
||||
@ -45,7 +44,7 @@ export default class ClientsPageHeaderComponent extends Component {
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.getExportCapabilities(this.args.namespace);
|
||||
this.getExportCapabilities();
|
||||
}
|
||||
|
||||
get showExportButton() {
|
||||
@ -54,7 +53,8 @@ export default class ClientsPageHeaderComponent extends Component {
|
||||
}
|
||||
|
||||
@waitFor
|
||||
async getExportCapabilities(ns = '') {
|
||||
async getExportCapabilities() {
|
||||
const ns = this.namespace.path;
|
||||
try {
|
||||
// selected namespace usually ends in /
|
||||
const url = ns
|
||||
@ -89,16 +89,10 @@ export default class ClientsPageHeaderComponent extends Component {
|
||||
get formattedCsvFileName() {
|
||||
const endRange = this.showEndDate ? `-${this.formattedEndDate}` : '';
|
||||
const csvDateRange = this.formattedStartDate ? `_${this.formattedStartDate + endRange}` : '';
|
||||
const ns = this.namespaceFilter ? `_${this.namespaceFilter}` : '';
|
||||
const ns = this.namespace.path ? `_${this.namespace.path}` : '';
|
||||
return `clients_export${ns}${csvDateRange}`;
|
||||
}
|
||||
|
||||
get namespaceFilter() {
|
||||
const currentNs = this.namespace.path;
|
||||
const { namespace } = this.args;
|
||||
return namespace ? sanitizePath(`${currentNs}/${namespace}`) : sanitizePath(currentNs);
|
||||
}
|
||||
|
||||
get showCommunity() {
|
||||
return this.version.isCommunity && !!this.formattedStartDate && !!this.formattedEndDate;
|
||||
}
|
||||
@ -111,14 +105,10 @@ export default class ClientsPageHeaderComponent extends Component {
|
||||
format: this.exportFormat === 'jsonl' ? 'json' : 'csv',
|
||||
start_time: startTimestamp,
|
||||
end_time: endTimestamp,
|
||||
namespace: this.namespaceFilter,
|
||||
namespace: this.namespace.path,
|
||||
});
|
||||
}
|
||||
|
||||
parseAPITimestamp = (timestamp, format) => {
|
||||
return parseAPITimestamp(timestamp, format);
|
||||
};
|
||||
|
||||
exportChartData = task({ drop: true }, async (filename) => {
|
||||
try {
|
||||
const contents = await this.getExportData();
|
||||
@ -145,4 +135,9 @@ export default class ClientsPageHeaderComponent extends Component {
|
||||
setEditModalVisible(visible) {
|
||||
this.showEditModal = visible;
|
||||
}
|
||||
|
||||
// LOCAL TEMPLATE HELPERS
|
||||
parseAPITimestamp = (timestamp, format) => {
|
||||
return parseAPITimestamp(timestamp, format);
|
||||
};
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ export default class ClientsClientListPageComponent extends ActivityComponent {
|
||||
|
||||
@action
|
||||
handleFilter(filters: Record<ClientFilterTypes, string>) {
|
||||
const { nsLabel, mountPath, mountType } = filters;
|
||||
this.args.onFilterChange({ nsLabel, mountPath, mountType });
|
||||
const { namespace_path, mount_path, mount_type } = filters;
|
||||
this.args.onFilterChange({ namespace_path, mount_path, mount_type });
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
@activityTimestamp={{@activity.responseTimestamp}}
|
||||
@startTimestamp={{@startTimestamp}}
|
||||
@endTimestamp={{@endTimestamp}}
|
||||
@namespace={{@namespace}}
|
||||
@upgradesDuringActivity={{this.upgradesDuringActivity}}
|
||||
@noData={{not @activity.total.clients}}
|
||||
@onChange={{this.onDateChange}}
|
||||
@ -35,52 +34,7 @@
|
||||
</Hds::Alert>
|
||||
{{/if}}
|
||||
|
||||
{{#if (or @namespace this.namespaces @mountPath this.mountPaths)}}
|
||||
<Hds::Text::Display @tag="p">
|
||||
Filters
|
||||
</Hds::Text::Display>
|
||||
<Hds::Text::Body @tag="p" @color="faint">
|
||||
Apply a filter to look at data from a specific namespace and drill down by mount. The mount filter includes auth
|
||||
methods, KV engines, and PKI engines. Each mount type generates a different type of client and will not be applicable
|
||||
to every tab.
|
||||
</Hds::Text::Body>
|
||||
<Toolbar aria-label="toolbar for filtering client count data" class="has-bottom-margin-m" data-test-clients-filter-bar>
|
||||
<ToolbarFilters>
|
||||
{{#if (or @namespace this.namespaces)}}
|
||||
<SearchSelect
|
||||
@id="namespace-search-select"
|
||||
@options={{this.namespaces}}
|
||||
@inputValue={{if @namespace (array @namespace)}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{fn this.setFilterValue "ns"}}
|
||||
@placeholder="Namespace within {{this.namespacePathForFilter}}"
|
||||
@displayInherit={{true}}
|
||||
class="is-marginless"
|
||||
data-test-counts-namespaces
|
||||
/>
|
||||
<div class="has-left-margin-xs"></div>
|
||||
{{/if}}
|
||||
{{#if (or @mountPath this.mountPaths)}}
|
||||
<SearchSelect
|
||||
@id="mounts-search-select"
|
||||
@options={{this.mountPaths}}
|
||||
@inputValue={{if @mountPath (array @mountPath)}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{fn this.setFilterValue "mountPath"}}
|
||||
@placeholder="Mount path within {{this.namespacePathForFilter}}"
|
||||
@displayInherit={{true}}
|
||||
data-test-counts-auth-mounts
|
||||
/>
|
||||
{{/if}}
|
||||
</ToolbarFilters>
|
||||
</Toolbar>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.totalUsageCounts}}
|
||||
{{#if @activity.total}}
|
||||
{{#if this.upgradeExplanations}}
|
||||
<Hds::Alert data-test-clients-upgrade-warning @type="inline" @color="warning" class="has-bottom-margin-m" as |A|>
|
||||
<A.Title>
|
||||
@ -133,11 +87,9 @@
|
||||
@message={{if
|
||||
this.version.isCommunity
|
||||
"Select a start and end date above to query client count data."
|
||||
"Update the filter values or click the button to reset them."
|
||||
"Select a different date range to view client count data."
|
||||
}}
|
||||
>
|
||||
<Hds::Button @text="Reset filters" @color="tertiary" @icon="reload" {{on "click" this.resetFilters}} />
|
||||
</EmptyState>
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -8,8 +8,7 @@ import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isSameMonth, isAfter } from 'date-fns';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { filteredTotalForMount, filterVersionHistory, TotalClients } from 'core/utils/client-count-utils';
|
||||
import { sanitizePath } from 'core/utils/sanitize-path';
|
||||
import { filterVersionHistory } from 'core/utils/client-count-utils';
|
||||
|
||||
import type AdapterError from '@ember-data/adapter/error';
|
||||
import type FlagsService from 'vault/services/flags';
|
||||
@ -18,15 +17,12 @@ import type VersionService from 'vault/services/version';
|
||||
import type ClientsActivityModel from 'vault/models/clients/activity';
|
||||
import type ClientsConfigModel from 'vault/models/clients/config';
|
||||
import type ClientsVersionHistoryModel from 'vault/models/clients/version-history';
|
||||
import type NamespaceService from 'vault/services/namespace';
|
||||
|
||||
interface Args {
|
||||
activity: ClientsActivityModel;
|
||||
activityError?: AdapterError;
|
||||
config: ClientsConfigModel;
|
||||
endTimestamp: string; // ISO format
|
||||
mountPath: string;
|
||||
namespace: string;
|
||||
onFilterChange: CallableFunction;
|
||||
startTimestamp: string; // ISO format
|
||||
versionHistory: ClientsVersionHistoryModel[];
|
||||
@ -35,7 +31,6 @@ interface Args {
|
||||
export default class ClientsCountsPageComponent extends Component<Args> {
|
||||
@service declare readonly flags: FlagsService;
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly namespace: NamespaceService;
|
||||
@service declare readonly store: Store;
|
||||
|
||||
get formattedStartDate() {
|
||||
@ -112,54 +107,6 @@ export default class ClientsCountsPageComponent extends Component<Args> {
|
||||
};
|
||||
}
|
||||
|
||||
// path of the filtered namespace OR current one, for filtering relevant data
|
||||
get namespacePathForFilter() {
|
||||
const { namespace } = this.args;
|
||||
const currentNs = this.namespace.currentNamespace;
|
||||
return sanitizePath(namespace || currentNs || 'root');
|
||||
}
|
||||
|
||||
// activityForNamespace gets the byNamespace data for the selected or current namespace so we can get the list of mounts from that namespace for attribution
|
||||
get activityForNamespace() {
|
||||
const { activity } = this.args;
|
||||
const nsPath = this.namespacePathForFilter;
|
||||
// we always return activity for namespace, either the selected filter or the current
|
||||
return activity?.byNamespace?.find((ns) => sanitizePath(ns.label) === nsPath);
|
||||
}
|
||||
|
||||
// duplicate of the method found in the activity component, so that we render the child only when there is activity to view
|
||||
get totalUsageCounts(): TotalClients | undefined {
|
||||
const { namespace, mountPath, activity } = this.args;
|
||||
if (mountPath) {
|
||||
// only do this if we have a mountPath filter.
|
||||
// namespace is filtered on API layer
|
||||
return filteredTotalForMount(activity.byNamespace, namespace, mountPath);
|
||||
}
|
||||
return activity?.total;
|
||||
}
|
||||
|
||||
// namespace list for the search-select filter
|
||||
get namespaces() {
|
||||
return this.args.activity?.byNamespace
|
||||
? this.args.activity.byNamespace
|
||||
.map((namespace) => ({
|
||||
name: namespace.label,
|
||||
id: namespace.label,
|
||||
}))
|
||||
.filter((ns) => sanitizePath(ns.name) !== this.namespacePathForFilter)
|
||||
: [];
|
||||
}
|
||||
|
||||
// mounts within the current/filtered namespace for the sesarch-select filter
|
||||
get mountPaths() {
|
||||
return (
|
||||
this.activityForNamespace?.mounts.map((mount) => ({
|
||||
id: mount.label,
|
||||
name: mount.label,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
// banner contents shown if startTime returned from activity API (which matches the first month with data) is after the queried startTime
|
||||
get startTimeDiscrepancy() {
|
||||
const { activity, config } = this.args;
|
||||
@ -190,27 +137,4 @@ export default class ClientsCountsPageComponent extends Component<Args> {
|
||||
onDateChange(params: { start_time: number | undefined; end_time: number | undefined }) {
|
||||
this.args.onFilterChange(params);
|
||||
}
|
||||
|
||||
@action
|
||||
setFilterValue(type: 'ns' | 'mountPath', [value]: [string]) {
|
||||
const params = { [type]: value };
|
||||
if (type === 'ns' && !value) {
|
||||
// unset mountPath value when namespace is cleared
|
||||
params['mountPath'] = '';
|
||||
} else if (type === 'mountPath' && !this.args.namespace) {
|
||||
// set namespace when mountPath set without namespace already set
|
||||
params['ns'] = this.namespacePathForFilter;
|
||||
}
|
||||
this.args.onFilterChange(params);
|
||||
}
|
||||
|
||||
@action
|
||||
resetFilters() {
|
||||
this.args.onFilterChange({
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
ns: '',
|
||||
mountPath: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,16 +8,18 @@
|
||||
@byMonthNewClients={{this.byMonthNewClients}}
|
||||
@isHistoricalMonth={{and (not this.isCurrentMonth) (not this.isDateRange)}}
|
||||
@isCurrentMonth={{this.isCurrentMonth}}
|
||||
@runningTotals={{this.totalUsageCounts}}
|
||||
@upgradeData={{this.upgradesDuringActivity}}
|
||||
@mountPath={{@mountPath}}
|
||||
@runningTotals={{@activity.total}}
|
||||
/>
|
||||
|
||||
{{#if this.hasAttributionData}}
|
||||
<Clients::CountsCard @title="Client attribution" @legend={{this.chartLegend}}>
|
||||
{{! by_namespace is an empty array when there is no client count activity data }}
|
||||
{{#if @activity.byNamespace}}
|
||||
<Clients::CountsCard
|
||||
@title="Client attribution"
|
||||
@description="Select a month to view the client count per mount for that month."
|
||||
>
|
||||
<:subheader>
|
||||
<Hds::Form::Select::Base
|
||||
class="has-bottom-margin-m"
|
||||
class="has-top-margin-m"
|
||||
aria-label="Month"
|
||||
name="month"
|
||||
{{on "input" this.selectMonth}}
|
||||
@ -28,10 +30,25 @@
|
||||
<S.Options>
|
||||
<option value="">Select month</option>
|
||||
{{#each this.months as |m|}}
|
||||
<option value={{m.value}} selected={{eq m.value this.selectedMonth}}>{{m.display}}</option>
|
||||
<option value={{m.timestamp}} selected={{eq m.timestamp this.selectedMonth}}>{{m.display}}</option>
|
||||
{{/each}}
|
||||
</S.Options>
|
||||
</Hds::Form::Select::Base>
|
||||
|
||||
{{#if this.selectedMonth}}
|
||||
<div>
|
||||
<Hds::Text::Body class="has-top-margin-l has-bottom-margin-m" @tag="p" @size="100" @color="faint">Use the filters
|
||||
to view the clients attributed by path.
|
||||
</Hds::Text::Body>
|
||||
<Clients::FilterToolbar
|
||||
@namespaces={{this.namespaceLabels}}
|
||||
@mountPaths={{this.mountPaths}}
|
||||
@mountTypes={{this.mountTypes}}
|
||||
@onFilter={{this.handleFilter}}
|
||||
@appliedFilters={{@filterQueryParams}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</:subheader>
|
||||
|
||||
<:table>
|
||||
@ -40,23 +57,13 @@
|
||||
@data={{this.tableData}}
|
||||
@columns={{this.tableColumns}}
|
||||
@initiallySortBy={{hash column="clients" direction="desc"}}
|
||||
@setPageSize={{10}}
|
||||
@showPaginationSizeSelector={{true}}
|
||||
>
|
||||
<:emptyState>
|
||||
<Hds::ApplicationState as |A|>
|
||||
<A.Header
|
||||
@title={{if
|
||||
this.selectedMonth
|
||||
"No data is available for the selected month"
|
||||
"Select a month to view client attribution"
|
||||
}}
|
||||
/>
|
||||
<A.Body
|
||||
@text="View the namespace mount breakdown of clients by selecting {{if
|
||||
this.selectedMonth
|
||||
'another'
|
||||
'a'
|
||||
}} month."
|
||||
/>
|
||||
<A.Header @title="No data found" />
|
||||
<A.Body @text="Clear or change filters to view client count data." />
|
||||
<A.Footer as |F|>
|
||||
<F.LinkStandalone
|
||||
@icon="file-text"
|
||||
@ -70,5 +77,4 @@
|
||||
</Clients::Table>
|
||||
</:table>
|
||||
</Clients::CountsCard>
|
||||
|
||||
{{/if}}
|
||||
@ -5,11 +5,12 @@
|
||||
|
||||
import ActivityComponent from '../activity';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { cached, tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { HTMLElementEvent } from 'vault/forms';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { formatTableData, TableData } from 'core/utils/client-count-utils';
|
||||
|
||||
import { filterTableData, flattenMounts, type MountClients } from 'core/utils/client-count-utils';
|
||||
import type FlagsService from 'vault/services/flags';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
|
||||
@ -19,35 +20,64 @@ export default class ClientsOverviewPageComponent extends ActivityComponent {
|
||||
|
||||
@tracked selectedMonth = '';
|
||||
|
||||
get hasAttributionData() {
|
||||
// we hide attribution table when mountPath filter present
|
||||
// or if there's no data
|
||||
return !this.args.mountPath && this.totalUsageCounts.clients;
|
||||
@cached
|
||||
get clientsByMount() {
|
||||
const namespaceData = this.selectedMonth
|
||||
? // Find the namespace data for the selected month
|
||||
this.byMonthNewClients.find((m) => m.timestamp === this.selectedMonth)?.namespaces
|
||||
: // If no month is selected the table displays all of the by_namespace activity for the queried date range
|
||||
this.args.activity.byNamespace;
|
||||
|
||||
// Get the array of "mounts" data nested in each namespace object and flatten
|
||||
return flattenMounts(namespaceData || []);
|
||||
}
|
||||
|
||||
// DROPDOWN GETTERS
|
||||
@cached
|
||||
get months() {
|
||||
return this.byMonthNewClients.reverse().map((m) => ({
|
||||
display: parseAPITimestamp(m.timestamp, 'MMMM yyyy'),
|
||||
value: m.month,
|
||||
}));
|
||||
return this.byMonthNewClients
|
||||
.reverse()
|
||||
.map((m) => ({ timestamp: m.timestamp, display: parseAPITimestamp(m.timestamp, 'MMMM yyyy') }));
|
||||
}
|
||||
|
||||
get tableData(): TableData[] | undefined {
|
||||
if (!this.selectedMonth) return undefined;
|
||||
return formatTableData(this.byMonthNewClients, this.selectedMonth);
|
||||
@cached
|
||||
get namespaceLabels() {
|
||||
return this.args.activity.byNamespace.map((n) => n.label);
|
||||
}
|
||||
|
||||
@cached
|
||||
get mountPaths() {
|
||||
return [...new Set(this.clientsByMount.map((m: MountClients) => m.label))];
|
||||
}
|
||||
|
||||
@cached
|
||||
get mountTypes() {
|
||||
return [...new Set(this.clientsByMount.map((m: MountClients) => m.mount_type))];
|
||||
}
|
||||
// end dropdown getters
|
||||
|
||||
get tableData() {
|
||||
if (this.clientsByMount?.length) {
|
||||
return filterTableData(this.clientsByMount, this.args.filterQueryParams);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get tableColumns() {
|
||||
return [
|
||||
{ key: 'namespace_path', label: 'Namespace', isSortable: true },
|
||||
{ key: 'label', label: 'Mount path', isSortable: true },
|
||||
{ key: 'mount_path', label: 'Mount path', isSortable: true },
|
||||
{ key: 'mount_type', label: 'Mount type', isSortable: true },
|
||||
{ key: 'clients', label: 'Counts', isSortable: true },
|
||||
{ key: 'clients', label: 'Client count', isSortable: true },
|
||||
];
|
||||
}
|
||||
|
||||
@action
|
||||
selectMonth(e: HTMLElementEvent<HTMLInputElement>) {
|
||||
this.selectedMonth = e.target.value;
|
||||
// Reset filters when no month is selected
|
||||
if (this.selectedMonth === '') {
|
||||
this.resetFilters();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@
|
||||
{{! Renders when viewing the current month or for activity log data that predates the monthly breakdown added in 1.11 }}
|
||||
<Clients::UsageStats
|
||||
@title="Total usage"
|
||||
@description="These totals are within this {{or @mountPath 'namespace and all its children'}}. {{if
|
||||
@description="These totals are within this namespace and all its children. {{if
|
||||
@isCurrentMonth
|
||||
"Only totals are available when viewing the current month. To see a breakdown of new and total clients for this month, select the 'Current billing period' filter."
|
||||
}}"
|
||||
|
||||
@ -7,7 +7,6 @@ import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
import type { ByMonthNewClients, TotalClients } from 'core/utils/client-count-utils';
|
||||
import type ClientsVersionHistoryModel from 'vault/vault/models/clients/version-history';
|
||||
|
||||
interface Args {
|
||||
isSecretsSyncActivated: boolean;
|
||||
@ -15,9 +14,6 @@ interface Args {
|
||||
isHistoricalMonth: boolean;
|
||||
isCurrentMonth: boolean;
|
||||
runningTotals: TotalClients;
|
||||
upgradesDuringActivity: ClientsVersionHistoryModel[];
|
||||
responseTimestamp: string;
|
||||
mountPath: string;
|
||||
}
|
||||
|
||||
export default class RunningTotal extends Component<Args> {
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
@sortBy={{this.sortColumn}}
|
||||
@sortOrder={{this.sortDirection}}
|
||||
@onSort={{this.updateSort}}
|
||||
{{did-update this.resetPagination @data}}
|
||||
...attributes
|
||||
>
|
||||
<:body as |B|>
|
||||
@ -29,17 +30,21 @@
|
||||
</:body>
|
||||
</Hds::AdvancedTable>
|
||||
|
||||
<Hds::Pagination::Numbered
|
||||
@currentPage={{this.currentPage}}
|
||||
@currentPageSize={{this.pageSize}}
|
||||
@onPageChange={{fn this.handlePaginationChange "currentPage"}}
|
||||
@onPageSizeChange={{fn this.handlePaginationChange "pageSize"}}
|
||||
@pageSizes={{array 5 10 50 100}}
|
||||
@showSizeSelector={{or @showPaginationSizeSelector false}}
|
||||
@totalItems={{@data.length}}
|
||||
data-test-pagination
|
||||
class="has-top-margin-m"
|
||||
/>
|
||||
{{! WORKAROUND to manually re-render Hds::Pagination::Numbered to force update @currentPage }}
|
||||
{{#if this.renderPagination}}
|
||||
<Hds::Pagination::Numbered
|
||||
@currentPage={{this.currentPage}}
|
||||
@currentPageSize={{this.pageSize}}
|
||||
@onPageChange={{fn this.handlePaginationChange "currentPage"}}
|
||||
@onPageSizeChange={{fn this.handlePaginationChange "pageSize"}}
|
||||
@pageSizes={{array 5 10 50 100}}
|
||||
@showSizeSelector={{or @showPaginationSizeSelector false}}
|
||||
@totalItems={{@data.length}}
|
||||
data-test-pagination
|
||||
class="has-top-margin-m"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l" data-test-card="table empty state">
|
||||
{{#if (has-block "emptyState")}}
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import Ember from 'ember';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { cached, tracked } from '@glimmer/tracking';
|
||||
import { paginate } from 'core/utils/paginate-list';
|
||||
import { next } from '@ember/runloop';
|
||||
|
||||
/**
|
||||
* @module ClientsTable
|
||||
@ -49,24 +49,37 @@ interface Args {
|
||||
|
||||
export default class ClientsTable extends Component<Args> {
|
||||
@tracked currentPage = 1;
|
||||
@tracked pageSize = Ember.testing ? 3 : 10; // lower in tests to test pagination without seeding more data
|
||||
@tracked pageSize = 5; // Can be overridden by @setPageSize
|
||||
@tracked sortColumn = '';
|
||||
@tracked sortDirection: SortDirection;
|
||||
@tracked sortDirection: SortDirection = 'asc'; // default is 'asc' for consistency with HDS defaults
|
||||
|
||||
// WORKAROUND to manually re-render Hds::Pagination::Numbered to force update @currentPage
|
||||
@tracked renderPagination = true;
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
|
||||
const { column = '', direction = 'asc' } = this.args.initiallySortBy || {};
|
||||
this.sortColumn = column;
|
||||
this.sortDirection = direction; // default is 'asc' for consistency with HDS defaults
|
||||
const { column, direction } = this.args.initiallySortBy || {};
|
||||
if (column) {
|
||||
this.sortColumn = column;
|
||||
}
|
||||
if (direction) {
|
||||
this.sortDirection = direction;
|
||||
}
|
||||
|
||||
// Override default page size with a custom amount.
|
||||
// pageSize can be updated by the end user if @showPaginationSizeSelector is true
|
||||
// pageSize can be updated by the user if @showPaginationSizeSelector is true
|
||||
if (this.args.setPageSize) {
|
||||
this.pageSize = this.args.setPageSize;
|
||||
}
|
||||
}
|
||||
|
||||
@cached
|
||||
get columnKeys() {
|
||||
return this.args.columns.map((k: TableColumn) => k['key']);
|
||||
}
|
||||
|
||||
@cached
|
||||
get paginatedTableData(): Record<string, any>[] {
|
||||
const sorted = this.sortTableData(this.args.data);
|
||||
const paginated = paginate(sorted, {
|
||||
@ -77,10 +90,6 @@ export default class ClientsTable extends Component<Args> {
|
||||
return paginated;
|
||||
}
|
||||
|
||||
get columnKeys() {
|
||||
return this.args.columns.map((k: TableColumn) => k['key']);
|
||||
}
|
||||
|
||||
// This table is paginated so we cannot use any out of the box filtering
|
||||
// from the HDS component and must manually sort data.
|
||||
sortTableData(data: Record<string, any>[]): Record<string, any>[] {
|
||||
@ -102,6 +111,18 @@ export default class ClientsTable extends Component<Args> {
|
||||
this[action] = value;
|
||||
}
|
||||
|
||||
@action
|
||||
async resetPagination() {
|
||||
// setPageSize is intentionally NOT reset here so user changes to page size
|
||||
// are preserved regardless of whether or not the table data updates.
|
||||
this.renderPagination = false;
|
||||
this.currentPage = 1;
|
||||
// WORKAROUND to manually re-render Hds::Pagination::Numbered to force update @currentPage
|
||||
next(() => {
|
||||
this.renderPagination = true;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
updateSort(column: string, direction: SortDirection) {
|
||||
// Update tracked variables so paginatedTableData recomputes
|
||||
|
||||
@ -10,7 +10,7 @@ import { ClientFilters } from 'core/utils/client-count-utils';
|
||||
import type { ClientsCountsRouteParams } from 'vault/routes/vault/cluster/clients/counts';
|
||||
|
||||
// these params refire the request to /sys/internal/counters/activity
|
||||
const ACTIVITY_QUERY_PARAMS = ['start_time', 'end_time', 'ns'];
|
||||
const ACTIVITY_QUERY_PARAMS = ['start_time', 'end_time'];
|
||||
// these params client-side filter table data
|
||||
const DROPDOWN_FILTERS = Object.values(ClientFilters);
|
||||
const queryParamKeys = [...ACTIVITY_QUERY_PARAMS, ...DROPDOWN_FILTERS];
|
||||
@ -19,10 +19,9 @@ export default class ClientsCountsController extends Controller {
|
||||
|
||||
start_time: string | number | undefined = undefined;
|
||||
end_time: string | number | undefined = undefined;
|
||||
ns: string | undefined = undefined; // TODO delete when filter toolbar is removed
|
||||
nsLabel = '';
|
||||
mountPath = '';
|
||||
mountType = '';
|
||||
namespace_path = '';
|
||||
mount_path = '';
|
||||
mount_type = '';
|
||||
|
||||
// using router.transitionTo to update the query params results in the model hook firing each time
|
||||
// this happens when the queryParams object is not added to the route or refreshModel is explicitly set to false
|
||||
|
||||
@ -20,15 +20,14 @@ import type ClientsActivityModel from 'vault/vault/models/clients/activity';
|
||||
export interface ClientsCountsRouteParams {
|
||||
start_time?: string | number | undefined;
|
||||
end_time?: string | number | undefined;
|
||||
ns?: string | undefined;
|
||||
mountPath?: string;
|
||||
mountType?: string;
|
||||
namespace_path?: string;
|
||||
mount_path?: string;
|
||||
mount_type?: string;
|
||||
}
|
||||
|
||||
interface ActivityAdapterQuery {
|
||||
start_time: { timestamp: number } | undefined;
|
||||
end_time: { timestamp: number } | undefined;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export type ClientsCountsRouteModel = ModelFrom<ClientsCountsRoute>;
|
||||
@ -40,10 +39,13 @@ export default class ClientsCountsRoute extends Route {
|
||||
@service declare readonly version: VersionService;
|
||||
|
||||
queryParams = {
|
||||
// These query params make a new request to the API
|
||||
start_time: { refreshModel: true, replace: true },
|
||||
end_time: { refreshModel: true, replace: true },
|
||||
ns: { refreshModel: true, replace: true },
|
||||
mountPath: { refreshModel: false, replace: true },
|
||||
// These query params just filter client-side data
|
||||
namespace_path: { refreshModel: false, replace: true },
|
||||
mount_path: { refreshModel: false, replace: true },
|
||||
mount_type: { refreshModel: false, replace: true },
|
||||
};
|
||||
|
||||
beforeModel() {
|
||||
@ -81,10 +83,6 @@ export default class ClientsCountsRoute extends Route {
|
||||
start_time: this.formatTimeQuery(params?.start_time),
|
||||
end_time: this.formatTimeQuery(params?.end_time),
|
||||
};
|
||||
if (params?.ns) {
|
||||
// only set explicit namespace if it's a query param
|
||||
query.namespace = params.ns;
|
||||
}
|
||||
try {
|
||||
activity = await this.store.queryRecord('clients/activity', query);
|
||||
} catch (error) {
|
||||
@ -131,8 +129,9 @@ export default class ClientsCountsRoute extends Route {
|
||||
controller.setProperties({
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
ns: undefined,
|
||||
mountPath: '',
|
||||
namespace_path: '',
|
||||
mount_path: '',
|
||||
mount_type: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,11 @@
|
||||
*/
|
||||
|
||||
// LEGEND STYLING (positioning is in chart-container.scss)
|
||||
$green-cyan: #06d092;
|
||||
$cerulean: #02a8ef;
|
||||
$blue-500: var(--token-color-palette-blue-500);
|
||||
$purple-300: var(--token-color-palette-purple-300);
|
||||
$blue-500: #1c345f;
|
||||
$secret_syncs: #6cc5b0;
|
||||
$acme_clients: #ff725c;
|
||||
$entity_clients: #4269d0;
|
||||
$non_entity_clients: #efb117;
|
||||
|
||||
.legend-container {
|
||||
.dots {
|
||||
@ -23,16 +24,16 @@ $purple-300: var(--token-color-palette-purple-300);
|
||||
background-color: $blue-500;
|
||||
}
|
||||
.legend-entity_clients {
|
||||
background-color: $blue-500;
|
||||
background-color: $entity_clients;
|
||||
}
|
||||
.legend-non_entity_clients {
|
||||
background-color: $green-cyan;
|
||||
background-color: $non_entity_clients;
|
||||
}
|
||||
.legend-secret_syncs {
|
||||
background-color: $purple-300;
|
||||
background-color: $secret_syncs;
|
||||
}
|
||||
.legend-acme_clients {
|
||||
background-color: $cerulean;
|
||||
background-color: $acme_clients;
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,18 +100,18 @@ $purple-300: var(--token-color-palette-purple-300);
|
||||
|
||||
// @colorScale arg for Lineal::VBars is "stacked-bar", indices are added by lineal
|
||||
.stacked-bar-1 {
|
||||
color: $blue-500;
|
||||
fill: $blue-500;
|
||||
color: $entity_clients;
|
||||
fill: $entity_clients;
|
||||
}
|
||||
.stacked-bar-2 {
|
||||
color: $green-cyan;
|
||||
fill: $green-cyan;
|
||||
color: $non_entity_clients;
|
||||
fill: $non_entity_clients;
|
||||
}
|
||||
.stacked-bar-3 {
|
||||
color: $purple-300;
|
||||
fill: $purple-300;
|
||||
color: $secret_syncs;
|
||||
fill: $secret_syncs;
|
||||
}
|
||||
.stacked-bar-4 {
|
||||
color: $cerulean;
|
||||
fill: $cerulean;
|
||||
color: $acme_clients;
|
||||
fill: $acme_clients;
|
||||
}
|
||||
|
||||
@ -8,8 +8,6 @@
|
||||
@activityError={{this.model.activityError}}
|
||||
@config={{this.model.config}}
|
||||
@endTimestamp={{this.model.endTimestamp}}
|
||||
@mountPath={{this.mountPath}}
|
||||
@namespace={{this.ns}}
|
||||
@onFilterChange={{this.updateQueryParams}}
|
||||
@startTimestamp={{this.model.startTimestamp}}
|
||||
@versionHistory={{this.model.versionHistory}}
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
@activity={{this.model.activity}}
|
||||
@onFilterChange={{this.countsController.updateQueryParams}}
|
||||
@filterQueryParams={{hash
|
||||
nsLabel=this.countsController.nsLabel
|
||||
mountPath=this.countsController.mountPath
|
||||
mountType=this.countsController.mountType
|
||||
namespace_path=this.countsController.namespace_path
|
||||
mount_path=this.countsController.mount_path
|
||||
mount_type=this.countsController.mount_type
|
||||
}}
|
||||
/>
|
||||
@ -6,8 +6,10 @@
|
||||
<Clients::Page::Overview
|
||||
@activity={{this.model.activity}}
|
||||
@versionHistory={{this.model.versionHistory}}
|
||||
@startTimestamp={{this.model.startTimestamp}}
|
||||
@endTimestamp={{this.model.endTimestamp}}
|
||||
@namespace={{this.countsController.ns}}
|
||||
@mountPath={{this.countsController.mountPath}}
|
||||
@onFilterChange={{this.countsController.updateQueryParams}}
|
||||
@filterQueryParams={{hash
|
||||
namespace_path=this.countsController.namespace_path
|
||||
mount_path=this.countsController.mount_path
|
||||
mount_type=this.countsController.mount_type
|
||||
}}
|
||||
/>
|
||||
@ -3,10 +3,9 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import isEmpty from '@ember/utils/lib/is_empty';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { sanitizePath } from 'core/utils/sanitize-path';
|
||||
import { compareAsc, getUnixTime, isWithinInterval } from 'date-fns';
|
||||
|
||||
import type ClientsVersionHistoryModel from 'vault/vault/models/clients/version-history';
|
||||
|
||||
/*
|
||||
@ -29,22 +28,13 @@ export type ClientTypes = (typeof CLIENT_TYPES)[number];
|
||||
|
||||
// map to dropdowns for filtering client count tables
|
||||
export enum ClientFilters {
|
||||
NAMESPACE = 'nsLabel',
|
||||
MOUNT_PATH = 'mountPath',
|
||||
MOUNT_TYPE = 'mountType',
|
||||
NAMESPACE = 'namespace_path',
|
||||
MOUNT_PATH = 'mount_path',
|
||||
MOUNT_TYPE = 'mount_type',
|
||||
}
|
||||
|
||||
export type ClientFilterTypes = (typeof ClientFilters)[keyof typeof ClientFilters];
|
||||
|
||||
// generates a block of total clients with 0's for use as defaults
|
||||
function emptyCounts() {
|
||||
return CLIENT_TYPES.reduce((prev, type) => {
|
||||
const key = type;
|
||||
prev[key as ClientTypes] = 0;
|
||||
return prev;
|
||||
}, {} as TotalClientsSometimes) as TotalClients;
|
||||
}
|
||||
|
||||
// returns array of VersionHistoryModels for noteworthy upgrades: 1.9, 1.10
|
||||
// that occurred between timestamps (i.e. queried activity data)
|
||||
export const filterVersionHistory = (
|
||||
@ -84,83 +74,6 @@ export const filterVersionHistory = (
|
||||
return [];
|
||||
};
|
||||
|
||||
// This method is used to return totals relevant only to the specified
|
||||
// mount path within the specified namespace.
|
||||
export const filteredTotalForMount = (
|
||||
byNamespace: ByNamespaceClients[],
|
||||
nsPath: string,
|
||||
mountPath: string
|
||||
): TotalClients => {
|
||||
if (!nsPath || !mountPath || isEmpty(byNamespace)) return emptyCounts();
|
||||
return (
|
||||
byNamespace
|
||||
.find((namespace) => sanitizePath(namespace.label) === sanitizePath(nsPath))
|
||||
?.mounts.find((mount: MountClients) => sanitizePath(mount.label) === sanitizePath(mountPath)) ||
|
||||
emptyCounts()
|
||||
);
|
||||
};
|
||||
|
||||
// This method is used to filter byMonth data and return data for only
|
||||
// the specified mount within the specified namespace. If data exists
|
||||
// for the month but not the mount, it should return zero'd data. If
|
||||
// no data exists for the month is returns the month as-is.
|
||||
export const filterByMonthDataForMount = (
|
||||
byMonth: ByMonthClients[],
|
||||
namespacePath: string,
|
||||
mountPath: string
|
||||
): ByMonthClients[] => {
|
||||
if (byMonth && namespacePath && mountPath) {
|
||||
const months: ByMonthClients[] = JSON.parse(JSON.stringify(byMonth));
|
||||
return [...months].map((m) => {
|
||||
if (m?.clients === undefined) {
|
||||
// if the month doesn't have data we can just return the block
|
||||
return m;
|
||||
}
|
||||
|
||||
const nsData = m.namespaces?.find((ns) => sanitizePath(ns.label) === sanitizePath(namespacePath));
|
||||
const mountData = nsData?.mounts.find((mount) => sanitizePath(mount.label) === sanitizePath(mountPath));
|
||||
if (mountData) {
|
||||
// if we do have mount data, we need to add in new_client namespace information
|
||||
const nsNew = m.new_clients?.namespaces?.find(
|
||||
(ns) => sanitizePath(ns.label) === sanitizePath(namespacePath)
|
||||
);
|
||||
const mountNew =
|
||||
nsNew?.mounts.find((mount) => sanitizePath(mount.label) === sanitizePath(mountPath)) ||
|
||||
emptyCounts();
|
||||
return {
|
||||
month: m.month,
|
||||
timestamp: m.timestamp,
|
||||
...mountData,
|
||||
namespaces: [], // this is just for making TS happy, matching the ByMonthClients shape
|
||||
new_clients: {
|
||||
month: m.month,
|
||||
timestamp: m.timestamp,
|
||||
label: mountPath,
|
||||
namespaces: [], // this is just for making TS happy, matching the ByMonthClients shape
|
||||
...mountNew,
|
||||
},
|
||||
} as ByMonthClients;
|
||||
}
|
||||
// if the month has data but none for this mount, return mocked zeros
|
||||
return {
|
||||
month: m.month,
|
||||
timestamp: m.timestamp,
|
||||
label: mountPath,
|
||||
namespaces: [], // this is just for making TS happy, matching the ByMonthClients shape
|
||||
...emptyCounts(),
|
||||
new_clients: {
|
||||
timestamp: m.timestamp,
|
||||
month: m.month,
|
||||
label: mountPath,
|
||||
namespaces: [], // this is just for making TS happy, matching the ByMonthClients shape
|
||||
...emptyCounts(),
|
||||
},
|
||||
} as ByMonthClients;
|
||||
});
|
||||
}
|
||||
return byMonth;
|
||||
};
|
||||
|
||||
// METHODS FOR SERIALIZING ACTIVITY RESPONSE
|
||||
export const formatDateObject = (dateObj: { monthIdx: number; year: number }, isEnd: boolean) => {
|
||||
const { year, monthIdx } = dateObj;
|
||||
@ -173,29 +86,25 @@ export const formatDateObject = (dateObj: { monthIdx: number; year: number }, is
|
||||
export const formatByMonths = (monthsArray: ActivityMonthBlock[]): ByMonthNewClients[] => {
|
||||
const sortedPayload = sortMonthsByTimestamp(monthsArray);
|
||||
return sortedPayload?.map((m) => {
|
||||
const month = parseAPITimestamp(m.timestamp, 'M/yy') as string;
|
||||
const { timestamp } = m;
|
||||
if (monthIsEmpty(m)) {
|
||||
// empty month
|
||||
return {
|
||||
month,
|
||||
timestamp,
|
||||
namespaces: [],
|
||||
new_clients: { month, timestamp, namespaces: [] },
|
||||
new_clients: { timestamp, namespaces: [] },
|
||||
};
|
||||
}
|
||||
|
||||
let newClients: ByMonthNewClients = { month, timestamp, namespaces: [] };
|
||||
let newClients: ByMonthNewClients = { timestamp, namespaces: [] };
|
||||
if (monthWithAllCounts(m)) {
|
||||
newClients = {
|
||||
month,
|
||||
timestamp,
|
||||
...destructureClientCounts(m?.new_clients.counts),
|
||||
namespaces: formatByNamespace(m.new_clients.namespaces),
|
||||
};
|
||||
}
|
||||
return {
|
||||
month,
|
||||
timestamp,
|
||||
...destructureClientCounts(m.counts),
|
||||
namespaces: formatByNamespace(m.namespaces),
|
||||
@ -217,6 +126,7 @@ export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByN
|
||||
mounts = ns.mounts.map((m) => ({
|
||||
label: m.mount_path,
|
||||
namespace_path: nsLabel,
|
||||
mount_path: m.mount_path,
|
||||
mount_type: m.mount_type,
|
||||
...destructureClientCounts(m.counts),
|
||||
}));
|
||||
@ -229,23 +139,6 @@ export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByN
|
||||
});
|
||||
};
|
||||
|
||||
export const formatTableData = (byMonthNewClients: ByMonthNewClients[], month: string): TableData[] => {
|
||||
const monthData = byMonthNewClients.find((m) => m.month === month);
|
||||
const namespaces = monthData?.namespaces;
|
||||
|
||||
let data: TableData[] = [];
|
||||
// iterate over namespaces to add "namespace" to each mount object
|
||||
namespaces?.forEach((n) => {
|
||||
const mounts: TableData[] = n.mounts.map((m) => {
|
||||
// add namespace to mount block
|
||||
return { ...m, namespace_path: n.label };
|
||||
});
|
||||
data = [...data, ...mounts];
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// This method returns only client types from the passed object, excluding other keys such as "label".
|
||||
// when querying historical data the response will always contain the latest client type keys because the activity log is
|
||||
// constructed based on the version of Vault the user is on (key values will be 0)
|
||||
@ -266,7 +159,30 @@ export const sortMonthsByTimestamp = (monthsArray: ActivityMonthBlock[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
// type guards for conditionals
|
||||
export const filterTableData = (
|
||||
data: MountClients[],
|
||||
filters: Record<ClientFilterTypes, string>
|
||||
): MountClients[] => {
|
||||
// Return original data if no filters are specified
|
||||
if (!filters || Object.values(filters).every((v) => !v)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return data.filter((datum) => {
|
||||
// Datum must satisfy every filter
|
||||
return Object.entries(filters).every(([filterKey, filterValue]) => {
|
||||
// If no filter is specified for that key, return true
|
||||
if (!filterValue) return true;
|
||||
// Otherwise only return true if the datum matches the filter
|
||||
return datum[filterKey as ClientFilterTypes] === filterValue;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const flattenMounts = (namespaceArray: ByNamespaceClients[]) =>
|
||||
namespaceArray.map((n) => n.mounts).flat();
|
||||
|
||||
// TYPE GUARDS FOR CONDITIONALS
|
||||
function monthIsEmpty(month: ActivityMonthBlock): month is ActivityMonthEmpty {
|
||||
return !month || month?.counts === null;
|
||||
}
|
||||
@ -275,6 +191,10 @@ function monthWithAllCounts(month: ActivityMonthBlock): month is ActivityMonthSt
|
||||
return month?.counts !== null && month?.new_clients?.counts !== null;
|
||||
}
|
||||
|
||||
export function filterIsSupported(f: string): f is ClientFilterTypes {
|
||||
return Object.values(ClientFilters).includes(f as ClientFilterTypes);
|
||||
}
|
||||
|
||||
export function hasMountsKey(
|
||||
obj: ByMonthNewClients | NamespaceNewClients | MountNewClients
|
||||
): obj is NamespaceNewClients {
|
||||
@ -312,52 +232,44 @@ export interface ByNamespaceClients extends TotalClients {
|
||||
|
||||
export interface MountClients extends TotalClients {
|
||||
label: string;
|
||||
mount_path: string;
|
||||
mount_type: string;
|
||||
namespace_path: string;
|
||||
}
|
||||
|
||||
export interface ByMonthClients extends TotalClients {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
namespaces: ByNamespaceClients[];
|
||||
new_clients: ByMonthNewClients;
|
||||
}
|
||||
|
||||
export interface ByMonthNewClients extends TotalClientsSometimes {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
namespaces: ByNamespaceClients[];
|
||||
}
|
||||
|
||||
export interface NamespaceByKey extends TotalClients {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
new_clients: NamespaceNewClients;
|
||||
}
|
||||
|
||||
export interface NamespaceNewClients extends TotalClientsSometimes {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
label: string;
|
||||
mounts: MountClients[];
|
||||
}
|
||||
|
||||
export interface MountByKey extends TotalClients {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
label: string;
|
||||
new_clients: MountNewClients;
|
||||
}
|
||||
|
||||
export interface MountNewClients extends TotalClientsSometimes {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface TableData extends MountClients {
|
||||
namespace_path: string;
|
||||
}
|
||||
|
||||
// API RESPONSE SHAPE (prior to serialization)
|
||||
|
||||
export interface NamespaceObject {
|
||||
|
||||
@ -66,8 +66,11 @@ function generateMountBlock(path, counts) {
|
||||
obj[key] = 0;
|
||||
return obj;
|
||||
}, {});
|
||||
// this logic is random nonsense just to have some mounts be "deleted"
|
||||
const setMountType = () => (counts.clients % 5 <= 1 ? 'deleted mount' : path.split('/')[1]);
|
||||
return {
|
||||
mount_path: path,
|
||||
mount_type: setMountType(),
|
||||
counts: {
|
||||
...baseObject,
|
||||
// object contains keys for which 0-values of base object to overwrite
|
||||
@ -97,13 +100,13 @@ function generateNamespaceBlock(idx = 0, isLowerCounts = false, ns, skipCounts =
|
||||
|
||||
// each mount type generates a different type of client
|
||||
return [
|
||||
generateMountBlock(`auth/authid/${idx}`, {
|
||||
generateMountBlock(`auth/token/${idx}`, {
|
||||
clients: non_entity_clients + entity_clients,
|
||||
non_entity_clients,
|
||||
entity_clients,
|
||||
}),
|
||||
generateMountBlock(`kvv2-engine-${idx}`, { clients: secret_syncs, secret_syncs }),
|
||||
generateMountBlock(`pki-engine-${idx}`, { clients: acme_clients, acme_clients }),
|
||||
generateMountBlock(`secrets/kv/${idx}`, { clients: secret_syncs, secret_syncs }),
|
||||
generateMountBlock(`acme/pki/${idx}`, { clients: acme_clients, acme_clients }),
|
||||
];
|
||||
};
|
||||
|
||||
@ -167,10 +170,9 @@ function generateActivityResponse(startDate, endDate) {
|
||||
d.namespaces.find((n) => n.namespace_path === ns.namespace_path)
|
||||
);
|
||||
const mountCounts = nsData.flatMap((d) => d.mounts);
|
||||
const paths = nsData[0].mounts.map(({ mount_path }) => mount_path);
|
||||
ns.mounts = paths.map((path) => {
|
||||
const counts = getTotalCounts(mountCounts.filter((m) => m.mount_path === path));
|
||||
return { mount_path: path, counts };
|
||||
ns.mounts = nsData[0].mounts.map((mount) => {
|
||||
const counts = getTotalCounts(mountCounts.filter((m) => m.mount_path === mount.mount_path));
|
||||
return { ...mount, counts };
|
||||
});
|
||||
ns.counts = getTotalCounts(nsData);
|
||||
});
|
||||
@ -254,7 +256,7 @@ function filterMonths(months, namespacePath) {
|
||||
/**
|
||||
* Util to mock filter namespace data from the activity response, matching what the API does
|
||||
*/
|
||||
export function filterActivityResponse(originalData, namespacePath) {
|
||||
function filterActivityResponse(originalData, namespacePath) {
|
||||
// make a deep copy of the object so we don't mutate the original
|
||||
const data = JSON.parse(JSON.stringify(originalData));
|
||||
if (!namespacePath) return data;
|
||||
|
||||
@ -42,10 +42,10 @@ module('Acceptance | clients | counts | client list', function (hooks) {
|
||||
test('filters are preset if URL includes query params', async function (assert) {
|
||||
assert.expect(4);
|
||||
const ns = 'ns1';
|
||||
const mPath = 'auth/userpass-0';
|
||||
const mPath = 'auth/userpass/0';
|
||||
const mType = 'userpass';
|
||||
await visit(
|
||||
`vault/clients/counts/client-list?nsLabel=${ns}&mountPath=${mPath}&mountType=${mType}&&start_time=1717113600`
|
||||
`vault/clients/counts/client-list?namespace_path=${ns}&mount_path=${mPath}&mount_type=${mType}&start_time=1717113600`
|
||||
);
|
||||
assert.dom(FILTERS.tag()).exists({ count: 3 }, '3 filter tags render');
|
||||
assert.dom(FILTERS.tag(ClientFilters.NAMESPACE, ns)).exists();
|
||||
@ -56,7 +56,7 @@ module('Acceptance | clients | counts | client list', function (hooks) {
|
||||
test('selecting filters update URL query params', async function (assert) {
|
||||
assert.expect(3);
|
||||
const ns = 'ns1';
|
||||
const mPath = 'auth/userpass-0';
|
||||
const mPath = 'auth/userpass/0';
|
||||
const mType = 'userpass';
|
||||
const url = '/vault/clients/counts/client-list';
|
||||
await visit(url);
|
||||
@ -73,7 +73,7 @@ module('Acceptance | clients | counts | client list', function (hooks) {
|
||||
await click(GENERAL.button('Apply filters'));
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`${url}?mountPath=${encodeURIComponent(mPath)}&mountType=${mType}&nsLabel=${ns}`,
|
||||
`${url}?mount_path=${encodeURIComponent(mPath)}&mount_type=${mType}&namespace_path=${ns}`,
|
||||
'url query params match filters'
|
||||
);
|
||||
await click(GENERAL.button('Clear filters'));
|
||||
|
||||
@ -17,12 +17,11 @@ import sinon from 'sinon';
|
||||
import { visit, click, findAll, fillIn, currentURL } from '@ember/test-helpers';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { CHARTS, CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||
import { formatNumber } from 'core/helpers/format-number';
|
||||
import { CHARTS, CLIENT_COUNT, FILTERS } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||
import timestamp from 'core/utils/timestamp';
|
||||
import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
||||
import { selectChoose } from 'ember-power-select/test-support';
|
||||
import { format } from 'date-fns';
|
||||
import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers';
|
||||
import { ClientFilters, flattenMounts } from 'core/utils/client-count-utils';
|
||||
|
||||
module('Acceptance | clients | overview', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@ -31,217 +30,7 @@ module('Acceptance | clients | overview', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW));
|
||||
clientsHandler(this.server);
|
||||
// stub secrets sync being activated
|
||||
this.server.get('/sys/activation-flags', function () {
|
||||
return {
|
||||
data: {
|
||||
activated: ['secrets-sync'],
|
||||
unactivated: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
this.store = this.owner.lookup('service:store');
|
||||
await login();
|
||||
return visit('/vault/clients/counts/overview');
|
||||
});
|
||||
|
||||
test('it should render charts', async function (assert) {
|
||||
assert
|
||||
.dom(`${GENERAL.flashMessage}.is-info`)
|
||||
.includesText(
|
||||
'counts returned in this usage period are an estimate',
|
||||
'Shows warning from API about client count estimations'
|
||||
);
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
|
||||
.hasText('July 2023', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
|
||||
.hasText('January 2024', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.card('Client usage trends for selected billing period')} ${CHARTS.xAxisLabel}`)
|
||||
.hasText('7/23', 'x-axis labels start with billing start date');
|
||||
assert.dom(CHARTS.xAxisLabel).exists({ count: 7 }, 'chart months matches query');
|
||||
});
|
||||
|
||||
test('it should update charts when querying date ranges', async function (assert) {
|
||||
// query for single, historical month with no new counts (July 2023)
|
||||
const service = this.owner.lookup('service:version');
|
||||
service.type = 'community';
|
||||
|
||||
const licenseStartMonth = format(LICENSE_START, 'yyyy-MM');
|
||||
const upgradeMonth = format(UPGRADE_DATE, 'yyyy-MM');
|
||||
const endMonth = format(STATIC_PREVIOUS_MONTH, 'yyyy-MM');
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), licenseStartMonth);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), licenseStartMonth);
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
assert
|
||||
.dom(CLIENT_COUNT.usageStats('Vault client counts'))
|
||||
.doesNotExist('running total single month stat boxes do not show');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.doesNotExist('running total month over month charts do not show');
|
||||
|
||||
// change to start on month/year of upgrade to 1.10
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), upgradeMonth);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), endMonth);
|
||||
await click(GENERAL.submitButton);
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
|
||||
.hasText('September 2023', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.card('Client usage trends for selected billing period')} ${CHARTS.xAxisLabel}`)
|
||||
.hasText('9/23', 'x-axis labels start with queried start month (upgrade date)');
|
||||
assert.dom(CHARTS.xAxisLabel).exists({ count: 4 }, 'chart months matches query');
|
||||
|
||||
// query for single, historical month (upgrade month)
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), upgradeMonth);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), upgradeMonth);
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert
|
||||
.dom(CLIENT_COUNT.usageStats('Vault client counts'))
|
||||
.exists('running total single month usage stats show');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.doesNotExist('running total month over month charts do not show');
|
||||
|
||||
// query historical date range (from September 2023 to December 2023)
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2023-09');
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), '2023-12');
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
|
||||
.hasText('September 2023', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
|
||||
.hasText('December 2023', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
|
||||
assert.dom(CHARTS.xAxisLabel).exists({ count: 4 }, 'chart months matches query');
|
||||
const xAxisLabels = findAll(CHARTS.xAxisLabel);
|
||||
assert
|
||||
.dom(xAxisLabels[xAxisLabels.length - 1])
|
||||
.hasText('12/23', 'x-axis labels end with queried end month');
|
||||
|
||||
// query month older than count start date
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2020-07');
|
||||
await click(GENERAL.submitButton);
|
||||
assert
|
||||
.dom(CLIENT_COUNT.counts.startDiscrepancy)
|
||||
.hasTextContaining(
|
||||
'You requested data from July 2020. We only have data from January 2023, and that is what is being shown here.',
|
||||
'warning banner displays that date queried was prior to count start date'
|
||||
);
|
||||
});
|
||||
|
||||
test('totals filter correctly with full data', async function (assert) {
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
|
||||
const response = await this.store.peekRecord('clients/activity', 'some-activity-id');
|
||||
const orderedNs = response.byNamespace.sort((a, b) => b.clients - a.clients);
|
||||
const topNamespace = orderedNs[0];
|
||||
// the namespace dropdown excludes the current namespace, so use second-largest if that's the case
|
||||
const filterNamespace = topNamespace.label === 'root' ? orderedNs[1] : topNamespace;
|
||||
const topMount = filterNamespace?.mounts.sort((a, b) => b.clients - a.clients)[0];
|
||||
|
||||
// Filter by top namespace
|
||||
await selectChoose(CLIENT_COUNT.nsFilter, filterNamespace.label);
|
||||
assert.dom(CLIENT_COUNT.selectedNs).hasText(filterNamespace.label, 'selects top namespace');
|
||||
|
||||
let expectedStats = {
|
||||
Entity: formatNumber([filterNamespace.entity_clients]),
|
||||
'Non-entity': formatNumber([filterNamespace.non_entity_clients]),
|
||||
ACME: formatNumber([filterNamespace.acme_clients]),
|
||||
'Secret sync': formatNumber([filterNamespace.secret_syncs]),
|
||||
};
|
||||
for (const label in expectedStats) {
|
||||
assert
|
||||
.dom(CLIENT_COUNT.statTextValue(label))
|
||||
.includesText(`${expectedStats[label]}`, `label: ${label} renders accurate namespace client counts`);
|
||||
}
|
||||
|
||||
// FILTER BY AUTH METHOD
|
||||
await selectChoose(CLIENT_COUNT.mountFilter, topMount.label);
|
||||
assert.dom(CLIENT_COUNT.selectedAuthMount).hasText(topMount.label, 'selects top mount');
|
||||
|
||||
expectedStats = {
|
||||
Entity: formatNumber([topMount.entity_clients]),
|
||||
'Non-entity': formatNumber([topMount.non_entity_clients]),
|
||||
ACME: formatNumber([topMount.acme_clients]),
|
||||
'Secret sync': formatNumber([topMount.secret_syncs]),
|
||||
};
|
||||
for (const label in expectedStats) {
|
||||
assert
|
||||
.dom(CLIENT_COUNT.statTextValue(label))
|
||||
.includesText(`${expectedStats[label]}`, `label: "${label} "renders accurate mount client counts`);
|
||||
}
|
||||
|
||||
// Remove namespace filter without first removing auth method filter
|
||||
await click(GENERAL.searchSelect.removeSelected);
|
||||
assert.strictEqual(currentURL(), '/vault/clients/counts/overview', 'removes both query params');
|
||||
|
||||
expectedStats = {
|
||||
Entity: formatNumber([response.total.entity_clients]),
|
||||
'Non-entity': formatNumber([response.total.non_entity_clients]),
|
||||
ACME: formatNumber([response.total.acme_clients]),
|
||||
'Secret sync': formatNumber([response.total.secret_syncs]),
|
||||
};
|
||||
for (const label in expectedStats) {
|
||||
assert
|
||||
.dom(CLIENT_COUNT.statTextValue(label))
|
||||
.includesText(`${expectedStats[label]}`, `label: ${label} is back to unfiltered value`);
|
||||
}
|
||||
});
|
||||
|
||||
test('it updates export button visibility as namespace is filtered', async function (assert) {
|
||||
const ns = 'ns7';
|
||||
// create a user that only has export access for specific namespace
|
||||
const userToken = await runCmd(
|
||||
tokenWithPolicyCmd(
|
||||
'cc-export',
|
||||
`
|
||||
path "${ns}/sys/internal/counters/activity/export" {
|
||||
capabilities = ["sudo"]
|
||||
}
|
||||
`
|
||||
)
|
||||
);
|
||||
await login(userToken);
|
||||
await visit('/vault/clients/counts/overview');
|
||||
assert.dom(CLIENT_COUNT.exportButton).doesNotExist();
|
||||
|
||||
// FILTER BY ALLOWED NAMESPACE
|
||||
await selectChoose('#namespace-search-select', ns);
|
||||
|
||||
assert.dom(CLIENT_COUNT.exportButton).exists();
|
||||
});
|
||||
});
|
||||
|
||||
module('Acceptance | clients | overview | secrets sync', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW));
|
||||
clientsHandler(this.server);
|
||||
});
|
||||
|
||||
test('it should hide secrets sync stats when feature is NOT on license', async function (assert) {
|
||||
@ -256,13 +45,238 @@ module('Acceptance | clients | overview | secrets sync', function (hooks) {
|
||||
assert.dom(CHARTS.legend).hasText('Entity clients Non-entity clients Acme clients');
|
||||
});
|
||||
|
||||
module('feature is on license', function (hooks) {
|
||||
// These tests use the clientsHandler which dynamically generates activity data, used for asserting date querying, etc
|
||||
module('dynamic data', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
// stub secrets sync being activated
|
||||
this.server.get('/sys/activation-flags', function () {
|
||||
return {
|
||||
data: {
|
||||
activated: ['secrets-sync'],
|
||||
unactivated: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.activity = await this.store.findRecord('clients/activity', 'some-activity-id');
|
||||
this.mostRecentMonth = this.activity.byMonth[this.activity.byMonth.length - 1];
|
||||
await login();
|
||||
return visit('/vault/clients/counts/overview');
|
||||
});
|
||||
|
||||
test('it should render charts', async function (assert) {
|
||||
assert
|
||||
.dom(`${GENERAL.flashMessage}.is-info`)
|
||||
.includesText(
|
||||
'counts returned in this usage period are an estimate',
|
||||
'Shows warning from API about client count estimations'
|
||||
);
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
|
||||
.hasText('July 2023', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
|
||||
.hasText('January 2024', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.card('Client usage trends for selected billing period')} ${CHARTS.xAxisLabel}`)
|
||||
.hasText('7/23', 'x-axis labels start with billing start date');
|
||||
assert.dom(CHARTS.xAxisLabel).exists({ count: 7 }, 'chart months matches query');
|
||||
});
|
||||
|
||||
test('it should update charts when querying date ranges', async function (assert) {
|
||||
// query for single, historical month with no new counts (July 2023)
|
||||
const service = this.owner.lookup('service:version');
|
||||
service.type = 'community';
|
||||
|
||||
const licenseStartMonth = format(LICENSE_START, 'yyyy-MM');
|
||||
const upgradeMonth = format(UPGRADE_DATE, 'yyyy-MM');
|
||||
const endMonth = format(STATIC_PREVIOUS_MONTH, 'yyyy-MM');
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), licenseStartMonth);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), licenseStartMonth);
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
assert
|
||||
.dom(CLIENT_COUNT.usageStats('Vault client counts'))
|
||||
.doesNotExist('running total single month stat boxes do not show');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.doesNotExist('running total month over month charts do not show');
|
||||
|
||||
// change to start on month/year of upgrade to 1.10
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), upgradeMonth);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), endMonth);
|
||||
await click(GENERAL.submitButton);
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
|
||||
.hasText('September 2023', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.card('Client usage trends for selected billing period')} ${CHARTS.xAxisLabel}`)
|
||||
.hasText('9/23', 'x-axis labels start with queried start month (upgrade date)');
|
||||
assert.dom(CHARTS.xAxisLabel).exists({ count: 4 }, 'chart months matches query');
|
||||
|
||||
// query for single, historical month (upgrade month)
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), upgradeMonth);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), upgradeMonth);
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert
|
||||
.dom(CLIENT_COUNT.usageStats('Vault client counts'))
|
||||
.exists('running total single month usage stats show');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.doesNotExist('running total month over month charts do not show');
|
||||
|
||||
// query historical date range (from September 2023 to December 2023)
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2023-09');
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), '2023-12');
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
|
||||
.hasText('September 2023', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
|
||||
.hasText('December 2023', 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
|
||||
assert.dom(CHARTS.xAxisLabel).exists({ count: 4 }, 'chart months matches query');
|
||||
const xAxisLabels = findAll(CHARTS.xAxisLabel);
|
||||
assert
|
||||
.dom(xAxisLabels[xAxisLabels.length - 1])
|
||||
.hasText('12/23', 'x-axis labels end with queried end month');
|
||||
|
||||
// query month older than count start date
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2020-07');
|
||||
await click(GENERAL.submitButton);
|
||||
assert
|
||||
.dom(CLIENT_COUNT.counts.startDiscrepancy)
|
||||
.hasTextContaining(
|
||||
'You requested data from July 2020. We only have data from January 2023, and that is what is being shown here.',
|
||||
'warning banner displays that date queried was prior to count start date'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// * FILTERING ASSERTIONS
|
||||
// These tests use the static data from the ACTIVITY_RESPONSE_STUB to assert filtering
|
||||
// Filtering tests are split between integration and acceptance tests
|
||||
// because changing filters updates the URL query params.
|
||||
module('static data', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.server.get('sys/internal/counters/activity', () => {
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: ACTIVITY_RESPONSE_STUB,
|
||||
};
|
||||
});
|
||||
const staticActivity = await this.store.findRecord('clients/activity', 'some-activity-id');
|
||||
this.staticMostRecentMonth = staticActivity.byMonth[staticActivity.byMonth.length - 1];
|
||||
await login();
|
||||
return visit('/vault/clients/counts/overview');
|
||||
});
|
||||
|
||||
test('it filters attribution table when filters are applied', async function (assert) {
|
||||
const url = '/vault/clients/counts/overview';
|
||||
const topMount = flattenMounts(this.staticMostRecentMonth.new_clients.namespaces)[0];
|
||||
const { namespace_path, mount_type, mount_path } = topMount;
|
||||
assert.strictEqual(currentURL(), url, 'URL does not contain query params');
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), this.staticMostRecentMonth.timestamp);
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||
await click(FILTERS.dropdownItem(namespace_path));
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
|
||||
await click(FILTERS.dropdownItem(mount_path));
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||
await click(FILTERS.dropdownItem(mount_type));
|
||||
await click(GENERAL.button('Apply filters'));
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`${url}?mount_path=${encodeURIComponent(
|
||||
mount_path
|
||||
)}&mount_type=${mount_type}&namespace_path=${namespace_path}`,
|
||||
'url query params match filters'
|
||||
);
|
||||
assert.dom(FILTERS.tag()).exists({ count: 3 }, '3 filter tags render');
|
||||
assert.dom(GENERAL.tableRow()).exists({ count: 1 }, 'it only renders the filtered table row');
|
||||
assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText(namespace_path);
|
||||
assert.dom(GENERAL.tableData(0, 'mount_type')).hasText(mount_type);
|
||||
assert.dom(GENERAL.tableData(0, 'mount_path')).hasText(mount_path);
|
||||
});
|
||||
|
||||
test('it updates table when filters are cleared', async function (assert) {
|
||||
const url = '/vault/clients/counts/overview';
|
||||
const mounts = flattenMounts(this.staticMostRecentMonth.new_clients.namespaces);
|
||||
const { namespace_path, mount_type, mount_path } = mounts[0];
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), this.staticMostRecentMonth.timestamp);
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||
await click(FILTERS.dropdownItem(namespace_path));
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
|
||||
await click(FILTERS.dropdownItem(mount_path));
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.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');
|
||||
await click(FILTERS.clearTag(namespace_path));
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`${url}?mount_path=${encodeURIComponent(mount_path)}&mount_type=${mount_type}`,
|
||||
'url does not have namespace_path query param'
|
||||
);
|
||||
assert.dom(GENERAL.tableRow()).exists({ count: 2 }, 'it renders 2 data rows that match filters');
|
||||
assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText('root');
|
||||
assert.dom(GENERAL.tableData(0, 'mount_type')).hasText(mount_type);
|
||||
assert.dom(GENERAL.tableData(1, 'namespace_path')).hasText('ns1');
|
||||
assert.dom(GENERAL.tableData(1, 'mount_type')).hasText(mount_type);
|
||||
assert.dom(GENERAL.tableData(1, 'mount_path')).hasText(mount_path);
|
||||
await click(GENERAL.button('Clear filters'));
|
||||
assert.strictEqual(currentURL(), url, 'url does not have any query params');
|
||||
assert
|
||||
.dom(GENERAL.tableRow())
|
||||
.exists({ count: mounts.length }, 'it renders all data when filters are cleared');
|
||||
});
|
||||
|
||||
test('it clears query params when month is unselected', async function (assert) {
|
||||
const url = '/vault/clients/counts/overview';
|
||||
const mounts = flattenMounts(this.staticMostRecentMonth.new_clients.namespaces);
|
||||
const { namespace_path, mount_type, mount_path } = mounts[0];
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), this.staticMostRecentMonth.timestamp);
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||
await click(FILTERS.dropdownItem(namespace_path));
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH));
|
||||
await click(FILTERS.dropdownItem(mount_path));
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE));
|
||||
await click(FILTERS.dropdownItem(mount_type));
|
||||
await click(GENERAL.button('Apply filters'));
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`${url}?mount_path=${encodeURIComponent(
|
||||
mount_path
|
||||
)}&mount_type=${mount_type}&namespace_path=${namespace_path}`,
|
||||
'url query params match filters'
|
||||
);
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), '');
|
||||
assert.strictEqual(currentURL(), url, 'url query params clear when month is not selected');
|
||||
});
|
||||
});
|
||||
|
||||
module('license includes secrets sync feature', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
syncHandler(this.server);
|
||||
});
|
||||
|
||||
test('it should show secrets sync stats when the feature is activated', async function (assert) {
|
||||
syncHandler(this.server);
|
||||
await login();
|
||||
await visit('/vault/clients/counts/overview');
|
||||
assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists('shows secret sync data on overview');
|
||||
|
||||
@ -45,7 +45,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
@ -56,18 +56,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 4810,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 4810,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 5699,
|
||||
@ -77,6 +66,17 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 4810,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 4810,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -91,7 +91,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
@ -102,7 +102,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
@ -113,7 +113,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 4003,
|
||||
@ -155,18 +155,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
@ -177,7 +166,18 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
@ -211,18 +211,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
@ -233,7 +222,18 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
@ -270,18 +270,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
@ -292,7 +281,18 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
@ -332,7 +332,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_path: 'acme/pki/0',
|
||||
counts: {
|
||||
acme_clients: 934,
|
||||
clients: 934,
|
||||
@ -342,7 +342,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 890,
|
||||
@ -352,7 +352,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 157,
|
||||
@ -375,7 +375,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_path: 'acme/pki/0',
|
||||
counts: {
|
||||
acme_clients: 994,
|
||||
clients: 994,
|
||||
@ -385,7 +385,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 872,
|
||||
@ -395,7 +395,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 81,
|
||||
@ -428,7 +428,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 91,
|
||||
clients: 91,
|
||||
@ -438,7 +439,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 75,
|
||||
@ -448,7 +450,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 25,
|
||||
@ -471,7 +474,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 96,
|
||||
@ -481,7 +485,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 53,
|
||||
clients: 53,
|
||||
@ -491,7 +496,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 24,
|
||||
@ -555,7 +561,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
},
|
||||
],
|
||||
@ -607,7 +613,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
},
|
||||
],
|
||||
@ -652,7 +658,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
},
|
||||
],
|
||||
@ -685,7 +691,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 4810,
|
||||
mounts: [
|
||||
{
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
namespace_path: 'ns1',
|
||||
acme_clients: 0,
|
||||
@ -695,17 +702,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
namespace_path: 'ns1',
|
||||
acme_clients: 0,
|
||||
clients: 4810,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 4810,
|
||||
},
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
label: 'acme/pki/0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
namespace_path: 'ns1',
|
||||
acme_clients: 5699,
|
||||
@ -714,6 +712,17 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'secrets/kv/0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
namespace_path: 'ns1',
|
||||
acme_clients: 0,
|
||||
clients: 4810,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 4810,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -725,7 +734,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 4290,
|
||||
mounts: [
|
||||
{
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
namespace_path: 'root',
|
||||
acme_clients: 0,
|
||||
@ -735,7 +745,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
label: 'secrets/kv/0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
namespace_path: 'root',
|
||||
acme_clients: 0,
|
||||
@ -745,7 +756,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 4290,
|
||||
},
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
label: 'acme/pki/0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
namespace_path: 'root',
|
||||
acme_clients: 4003,
|
||||
@ -759,17 +771,14 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
],
|
||||
by_month: [
|
||||
{
|
||||
month: '6/23',
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
namespaces: [],
|
||||
new_clients: {
|
||||
month: '6/23',
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
namespaces: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
month: '7/23',
|
||||
timestamp: '2023-07-01T00:00:00Z',
|
||||
acme_clients: 100,
|
||||
clients: 400,
|
||||
@ -786,18 +795,9 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 100,
|
||||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
namespace_path: 'root',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
namespace_path: 'root',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 200,
|
||||
@ -806,8 +806,20 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
label: 'acme/pki/0',
|
||||
namespace_path: 'root',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'secrets/kv/0',
|
||||
namespace_path: 'root',
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 100,
|
||||
@ -819,7 +831,6 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
month: '7/23',
|
||||
timestamp: '2023-07-01T00:00:00Z',
|
||||
acme_clients: 100,
|
||||
clients: 400,
|
||||
@ -836,17 +847,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 100,
|
||||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
namespace_path: 'root',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
namespace_path: 'root',
|
||||
acme_clients: 0,
|
||||
@ -856,7 +858,19 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
label: 'acme/pki/0',
|
||||
mount_path: 'acme/pki/0',
|
||||
namespace_path: 'root',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'secrets/kv/0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
namespace_path: 'root',
|
||||
acme_clients: 0,
|
||||
@ -871,7 +885,6 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
},
|
||||
},
|
||||
{
|
||||
month: '8/23',
|
||||
timestamp: '2023-08-01T00:00:00Z',
|
||||
acme_clients: 100,
|
||||
clients: 400,
|
||||
@ -888,17 +901,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 100,
|
||||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
namespace_path: 'root',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
namespace_path: 'root',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
@ -908,7 +912,20 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
label: 'acme/pki/0',
|
||||
mount_path: 'acme/pki/0',
|
||||
namespace_path: 'root',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
|
||||
{
|
||||
label: 'secrets/kv/0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
namespace_path: 'root',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
@ -921,13 +938,11 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
month: '8/23',
|
||||
timestamp: '2023-08-01T00:00:00Z',
|
||||
namespaces: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
month: '9/23',
|
||||
timestamp: '2023-09-01T00:00:00Z',
|
||||
acme_clients: 1928,
|
||||
clients: 3928,
|
||||
@ -944,7 +959,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 157,
|
||||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
label: 'acme/pki/0',
|
||||
mount_path: 'acme/pki/0',
|
||||
acme_clients: 934,
|
||||
clients: 934,
|
||||
entity_clients: 0,
|
||||
@ -952,7 +968,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
acme_clients: 0,
|
||||
clients: 890,
|
||||
entity_clients: 708,
|
||||
@ -960,7 +977,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
label: 'secrets/kv/0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
acme_clients: 0,
|
||||
clients: 157,
|
||||
entity_clients: 0,
|
||||
@ -978,7 +996,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 81,
|
||||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
label: 'acme/pki/0',
|
||||
mount_path: 'acme/pki/0',
|
||||
acme_clients: 994,
|
||||
clients: 994,
|
||||
entity_clients: 0,
|
||||
@ -986,7 +1005,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
acme_clients: 0,
|
||||
clients: 872,
|
||||
entity_clients: 124,
|
||||
@ -994,7 +1014,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
label: 'secrets/kv/0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
acme_clients: 0,
|
||||
clients: 81,
|
||||
entity_clients: 0,
|
||||
@ -1005,7 +1026,6 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
month: '9/23',
|
||||
timestamp: '2023-09-01T00:00:00Z',
|
||||
acme_clients: 144,
|
||||
clients: 364,
|
||||
@ -1022,7 +1042,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 25,
|
||||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
label: 'acme/pki/0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 91,
|
||||
clients: 91,
|
||||
@ -1031,7 +1052,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 75,
|
||||
@ -1040,7 +1062,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
label: 'secrets/kv/0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 25,
|
||||
@ -1059,7 +1082,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 24,
|
||||
mounts: [
|
||||
{
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 96,
|
||||
@ -1068,7 +1092,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
label: 'acme/pki/0',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 53,
|
||||
clients: 53,
|
||||
@ -1077,7 +1102,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
label: 'secrets/kv/0',
|
||||
mount_path: 'secrets/kv/0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 24,
|
||||
|
||||
@ -54,6 +54,7 @@ export const CHARTS = {
|
||||
};
|
||||
|
||||
export const FILTERS = {
|
||||
dropdown: (name: string) => `[data-test-dropdown="${name}"]`,
|
||||
dropdownToggle: (name: string) => `[data-test-dropdown="${name}"] button`,
|
||||
dropdownItem: (name: string) => `[data-test-dropdown-item="${name}"]`,
|
||||
tag: (filter?: string, value?: string) =>
|
||||
|
||||
@ -20,7 +20,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
||||
this.mountPaths = ['auth/token/', 'auth/auto/eng/core/auth/core-gh-auth/', 'auth/userpass-root/'];
|
||||
this.mountTypes = ['token/', 'userpass/', 'ns_token/'];
|
||||
this.onFilter = sinon.spy();
|
||||
this.appliedFilters = { nsLabel: '', mountPath: '', mountType: '' };
|
||||
this.appliedFilters = { namespace_path: '', mount_path: '', mount_type: '' };
|
||||
|
||||
this.renderComponent = async () => {
|
||||
await render(hbs`
|
||||
@ -34,7 +34,11 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
||||
};
|
||||
|
||||
this.presetFilters = () => {
|
||||
this.appliedFilters = { nsLabel: 'admin/', mountPath: 'auth/userpass-root/', mountType: 'token/' };
|
||||
this.appliedFilters = {
|
||||
namespace_path: 'admin/',
|
||||
mount_path: 'auth/userpass-root/',
|
||||
mount_type: 'token/',
|
||||
};
|
||||
};
|
||||
|
||||
this.selectFilters = async () => {
|
||||
@ -121,7 +125,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
||||
});
|
||||
|
||||
test('it applies updated filters when filters are preset', async function (assert) {
|
||||
this.appliedFilters = { mountPath: 'auth/token/', mountType: 'ns_token/', nsLabel: 'ns1' };
|
||||
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'));
|
||||
@ -133,7 +137,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
||||
const [afterUpdate] = this.onFilter.lastCall.args;
|
||||
assert.propEqual(
|
||||
afterUpdate,
|
||||
{ mountPath: 'auth/userpass-root/', mountType: 'token/', nsLabel: 'admin/' },
|
||||
{ namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/' },
|
||||
'callback fires with updated selection'
|
||||
);
|
||||
});
|
||||
@ -159,7 +163,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
||||
const [beforeClear] = this.onFilter.lastCall.args;
|
||||
assert.propEqual(
|
||||
beforeClear,
|
||||
{ mountPath: 'auth/userpass-root/', mountType: 'token/', nsLabel: 'admin/' },
|
||||
{ namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/' },
|
||||
'callback fires with preset filters'
|
||||
);
|
||||
// now clear filters and confirm values are cleared
|
||||
@ -167,7 +171,7 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
||||
const [afterClear] = this.onFilter.lastCall.args;
|
||||
assert.propEqual(
|
||||
afterClear,
|
||||
{ mountPath: '', mountType: '', nsLabel: '' },
|
||||
{ namespace_path: '', mount_path: '', mount_type: '' },
|
||||
'onFilter callback has empty values when "Clear filters" is clicked'
|
||||
);
|
||||
});
|
||||
@ -180,15 +184,15 @@ module('Integration | Component | clients/filter-toolbar', function (hooks) {
|
||||
const [beforeClear] = this.onFilter.lastCall.args;
|
||||
assert.propEqual(
|
||||
beforeClear,
|
||||
{ mountPath: 'auth/userpass-root/', mountType: 'token/', nsLabel: 'admin/' },
|
||||
{ namespace_path: 'admin/', mount_path: 'auth/userpass-root/', mount_type: 'token/' },
|
||||
'callback fires with preset filters'
|
||||
);
|
||||
await click(FILTERS.clearTag('admin/'));
|
||||
const afterClear = this.onFilter.lastCall.args[0];
|
||||
assert.propEqual(
|
||||
afterClear,
|
||||
{ mountPath: 'auth/userpass-root/', mountType: 'token/', nsLabel: '' },
|
||||
'onFilter callback fires with empty nsLabel'
|
||||
{ namespace_path: '', mount_path: 'auth/userpass-root/', mount_type: 'token/' },
|
||||
'onFilter callback fires with empty namespace_path'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -24,7 +24,6 @@ module('Integration | Component | clients/page-header', function (hooks) {
|
||||
this.downloadStub = Sinon.stub(this.owner.lookup('service:download'), 'download');
|
||||
this.startTimestamp = '2022-06-01T23:00:11.050Z';
|
||||
this.endTimestamp = '2022-12-01T23:00:11.050Z';
|
||||
this.selectedNamespace = undefined;
|
||||
this.upgradesDuringActivity = [];
|
||||
this.noData = undefined;
|
||||
this.server.post('/sys/capabilities-self', () =>
|
||||
@ -36,7 +35,6 @@ module('Integration | Component | clients/page-header', function (hooks) {
|
||||
<Clients::PageHeader
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.endTimestamp}}
|
||||
@namespace={{this.selectedNamespace}}
|
||||
@upgradesDuringActivity={{this.upgradesDuringActivity}}
|
||||
@noData={{this.noData}}
|
||||
/>`);
|
||||
@ -141,36 +139,6 @@ module('Integration | Component | clients/page-header', function (hooks) {
|
||||
await click(CLIENT_COUNT.exportButton);
|
||||
await click(GENERAL.confirmButton);
|
||||
});
|
||||
test('it sends the selected namespace in export request', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.get('/sys/internal/counters/activity/export', function (_, req) {
|
||||
assert.strictEqual(req.requestHeaders['X-Vault-Namespace'], 'foobar');
|
||||
return new Response(200, { 'Content-Type': 'text/csv' }, '');
|
||||
});
|
||||
this.selectedNamespace = 'foobar/';
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom(CLIENT_COUNT.exportButton).exists();
|
||||
await click(CLIENT_COUNT.exportButton);
|
||||
await click(GENERAL.confirmButton);
|
||||
});
|
||||
|
||||
test('it sends the current + selected namespace in export request', async function (assert) {
|
||||
assert.expect(2);
|
||||
const namespaceSvc = this.owner.lookup('service:namespace');
|
||||
namespaceSvc.path = 'foo';
|
||||
this.server.get('/sys/internal/counters/activity/export', function (_, req) {
|
||||
assert.strictEqual(req.requestHeaders['X-Vault-Namespace'], 'foo/bar');
|
||||
return new Response(200, { 'Content-Type': 'text/csv' }, '');
|
||||
});
|
||||
this.selectedNamespace = 'bar/';
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(CLIENT_COUNT.exportButton).exists();
|
||||
await click(CLIENT_COUNT.exportButton);
|
||||
await click(GENERAL.confirmButton);
|
||||
});
|
||||
|
||||
test('it shows a no data message if export returns 204', async function (assert) {
|
||||
this.server.get('/sys/internal/counters/activity/export', () => overrideResponse(204));
|
||||
@ -258,7 +226,7 @@ module('Integration | Component | clients/page-header', function (hooks) {
|
||||
this.startTimestamp = undefined;
|
||||
this.endTimestamp = undefined;
|
||||
const namespace = this.owner.lookup('service:namespace');
|
||||
namespace.path = 'bar/';
|
||||
namespace.path = 'bar';
|
||||
|
||||
this.server.get('/sys/internal/counters/activity/export', function (_, req) {
|
||||
assert.deepEqual(req.queryParams, {
|
||||
@ -275,27 +243,5 @@ module('Integration | Component | clients/page-header', function (hooks) {
|
||||
const [filename] = this.downloadStub.lastCall.args;
|
||||
assert.strictEqual(filename, 'clients_export_bar');
|
||||
});
|
||||
|
||||
test('includes selectedNamespace', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.startTimestamp = undefined;
|
||||
this.endTimestamp = undefined;
|
||||
this.selectedNamespace = 'foo/';
|
||||
|
||||
this.server.get('/sys/internal/counters/activity/export', function (_, req) {
|
||||
assert.deepEqual(req.queryParams, {
|
||||
format: 'csv',
|
||||
});
|
||||
return new Response(200, { 'Content-Type': 'text/csv' }, '');
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
await click(CLIENT_COUNT.exportButton);
|
||||
await click(GENERAL.confirmButton);
|
||||
await waitUntil(() => this.downloadStub.calledOnce);
|
||||
const [filename] = this.downloadStub.lastCall.args;
|
||||
assert.strictEqual(filename, 'clients_export_foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -16,7 +16,6 @@ import clientsHandler, {
|
||||
import { fromUnixTime, getUnixTime } from 'date-fns';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||
import { selectChoose } from 'ember-power-select/test-support';
|
||||
import timestamp from 'core/utils/timestamp';
|
||||
import sinon from 'sinon';
|
||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
@ -53,8 +52,6 @@ module('Integration | Component | clients | Page::Counts', function (hooks) {
|
||||
@versionHistory={{this.versionHistory}}
|
||||
@startTimestamp={{this.startTimestamp}}
|
||||
@endTimestamp={{this.endTimestamp}}
|
||||
@namespace={{this.namespace}}
|
||||
@mountPath={{this.mountPath}}
|
||||
@onFilterChange={{this.onFilterChange}}
|
||||
>
|
||||
<div data-test-yield>Yield block</div>
|
||||
@ -164,41 +161,6 @@ module('Integration | Component | clients | Page::Counts', function (hooks) {
|
||||
});
|
||||
});
|
||||
|
||||
test('it should render namespace and auth mount filters', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.namespace = 'root';
|
||||
this.mountPath = 'auth/authid0';
|
||||
|
||||
let assertion = (params) =>
|
||||
assert.deepEqual(params, { ns: undefined, mountPath: '' }, 'Auth mount cleared with namespace');
|
||||
this.onFilterChange = (params) => {
|
||||
if (assertion) {
|
||||
assertion(params);
|
||||
}
|
||||
const keys = Object.keys(params);
|
||||
this.namespace = keys.includes('ns') ? params.ns : this.namespace;
|
||||
this.mountPath = keys.includes('mountPath') ? params.mountPath : this.mountPath;
|
||||
};
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(CLIENT_COUNT.counts.namespaces).includesText(this.namespace, 'Selected namespace renders');
|
||||
assert.dom(CLIENT_COUNT.counts.mountPaths).includesText(this.mountPath, 'Selected auth mount renders');
|
||||
|
||||
await click(`${CLIENT_COUNT.counts.namespaces} button`);
|
||||
// this is only necessary in tests since SearchSelect does not respond to initialValue changes
|
||||
// in the app the component is rerender on query param change
|
||||
assertion = null;
|
||||
await click(`${CLIENT_COUNT.counts.mountPaths} button`);
|
||||
assertion = (params) => assert.true(params.ns.includes('ns'), 'Namespace value sent on change');
|
||||
await selectChoose(CLIENT_COUNT.counts.namespaces, '.ember-power-select-option', 0);
|
||||
|
||||
assertion = (params) =>
|
||||
assert.true(params.mountPath.includes('auth/'), 'Auth mount value sent on change');
|
||||
await selectChoose(CLIENT_COUNT.counts.mountPaths, 'auth/authid0');
|
||||
});
|
||||
|
||||
test('it should render start time discrepancy alert', async function (assert) {
|
||||
this.startTimestamp = new Date('2022-06-01T00:00:00Z').toISOString();
|
||||
|
||||
|
||||
@ -8,159 +8,178 @@ import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { click, fillIn, findAll, render, triggerEvent } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers';
|
||||
import { filterActivityResponse } from 'vault/mirage/handlers/clients';
|
||||
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||
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, flattenMounts } from 'core/utils/client-count-utils';
|
||||
|
||||
module('Integration | Component | clients/page/overview', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.server.get('sys/internal/counters/activity', (_, req) => {
|
||||
const namespace = req.requestHeaders['X-Vault-Namespace'];
|
||||
if (namespace === 'no-data') {
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: {
|
||||
by_namespace: [],
|
||||
end_time: '2024-08-31T23:59:59Z',
|
||||
months: [],
|
||||
start_time: '2024-01-01T00:00:00Z',
|
||||
total: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
this.server.get('sys/internal/counters/activity', () => {
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: filterActivityResponse(ACTIVITY_RESPONSE_STUB, namespace),
|
||||
data: ACTIVITY_RESPONSE_STUB,
|
||||
};
|
||||
});
|
||||
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.mountPath = '';
|
||||
this.namespace = '';
|
||||
this.versionHistory = '';
|
||||
this.activity = await this.store.queryRecord('clients/activity', {});
|
||||
this.mostRecentMonth = this.activity.byMonth[this.activity.byMonth.length - 1];
|
||||
this.onFilterChange = sinon.spy();
|
||||
this.filterQueryParams = { namespace_path: '', mount_path: '', mount_type: '' };
|
||||
this.renderComponent = () =>
|
||||
render(hbs`
|
||||
<Clients::Page::Overview
|
||||
@activity={{this.activity}}
|
||||
@onFilterChange={{this.onFilterChange}}
|
||||
@filterQueryParams={{this.filterQueryParams}}
|
||||
/>`);
|
||||
});
|
||||
|
||||
// Fails on #ember-testing-container
|
||||
setRunOptions({
|
||||
rules: {
|
||||
'aria-prohibited-attr': { enabled: false },
|
||||
},
|
||||
test('it hides attribution when there is no data', async function (assert) {
|
||||
// Stub activity response when there's no activity data
|
||||
this.server.get('sys/internal/counters/activity', () => {
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: {
|
||||
by_namespace: [],
|
||||
end_time: '2024-08-31T23:59:59Z',
|
||||
months: [],
|
||||
start_time: '2024-01-01T00:00:00Z',
|
||||
total: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
this.activity = await this.store.queryRecord('clients/activity', {});
|
||||
await this.renderComponent();
|
||||
assert.dom(CLIENT_COUNT.card('Client attribution')).doesNotExist('it does not render attribution card');
|
||||
assert.dom(GENERAL.selectByAttr('attribution-month')).doesNotExist('it hides months dropdown');
|
||||
});
|
||||
|
||||
test('it shows empty state message upon initial load', async function (assert) {
|
||||
await render(hbs`<Clients::Page::Overview @activity={{this.activity}}/>`);
|
||||
|
||||
test('it shows correct state message when selected month has no data', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown');
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), '2023-06-01T00:00:00Z');
|
||||
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('table empty state'))
|
||||
.exists('shows card for table state')
|
||||
.hasText(
|
||||
'Select a month to view client attribution View the namespace mount breakdown of clients by selecting a month. Client count documentation',
|
||||
'Show initial table state message'
|
||||
);
|
||||
});
|
||||
|
||||
test('it shows correct state message when month selection has no data', async function (assert) {
|
||||
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
|
||||
|
||||
assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown');
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), '6/23');
|
||||
|
||||
assert
|
||||
.dom(CLIENT_COUNT.card('table empty state'))
|
||||
.hasText(
|
||||
'No data is available for the selected month View the namespace mount breakdown of clients by selecting another month. Client count documentation',
|
||||
'Shows correct message for a month selection with no data'
|
||||
);
|
||||
.hasText('No data found Clear or change filters to view client count data. Client count documentation');
|
||||
});
|
||||
|
||||
test('it shows table when month selection has data', async function (assert) {
|
||||
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown');
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), '9/23');
|
||||
|
||||
assert.dom(CLIENT_COUNT.card('table empty state')).doesNotExist('does not show card when table has data');
|
||||
assert.dom(GENERAL.table('attribution')).exists('shows table');
|
||||
assert.dom(GENERAL.paginationInfo).hasText('1–3 of 6', 'shows correct pagination info');
|
||||
});
|
||||
|
||||
test('it filters the table when a namespace filter is applied', async function (assert) {
|
||||
this.namespace = 'ns1';
|
||||
this.activity = await this.store.queryRecord('clients/activity', {
|
||||
namespace: this.namespace,
|
||||
});
|
||||
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} @namespace={{this.namespace}} />`);
|
||||
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), '9/23');
|
||||
|
||||
assert.dom(CLIENT_COUNT.card('table empty state')).doesNotExist('does not show card when table has data');
|
||||
assert.dom(GENERAL.table('attribution')).exists();
|
||||
assert.dom(GENERAL.paginationInfo).hasText('1–3 of 3', 'shows correct pagination info');
|
||||
});
|
||||
|
||||
test('it hides the table when a mount filter is applied', async function (assert) {
|
||||
this.namespace = 'ns1';
|
||||
this.mountPath = 'auth/userpass-0';
|
||||
this.activity = await this.store.queryRecord('clients/activity', {
|
||||
namespace: this.namespace,
|
||||
mountPath: this.mountPath,
|
||||
});
|
||||
await render(
|
||||
hbs`<Clients::Page::Overview @activity={{this.activity}} @namespace={{this.namespace}} @mountPath={{this.mountPath}}/>`
|
||||
);
|
||||
assert.dom(CLIENT_COUNT.card('table empty state')).doesNotExist('does not show card when table has data');
|
||||
assert
|
||||
.dom(GENERAL.table('attribution'))
|
||||
.doesNotExist('does not show table when a mount filter is applied');
|
||||
});
|
||||
|
||||
test('it paginates table data', async function (assert) {
|
||||
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
|
||||
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), '9/23');
|
||||
|
||||
assert
|
||||
.dom(GENERAL.tableRow())
|
||||
.exists({ count: 3 }, 'Correct number of table rows render based on page size');
|
||||
assert.dom(GENERAL.tableData(0, 'clients')).hasText('96', 'First page shows data');
|
||||
assert.dom(GENERAL.pagination).exists('shows pagination');
|
||||
assert.dom(GENERAL.paginationInfo).hasText('1–3 of 6', 'shows correct pagination info');
|
||||
|
||||
await click(GENERAL.nextPage);
|
||||
|
||||
assert.dom(GENERAL.tableData(0, 'clients')).hasText('53', 'Second page shows new data');
|
||||
assert.dom(GENERAL.paginationInfo).hasText('4–6 of 6', 'shows correct pagination info');
|
||||
assert.dom(GENERAL.paginationInfo).hasText('1–6 of 6', 'shows correct pagination info');
|
||||
assert.dom(GENERAL.paginationSizeSelector).hasValue('10', 'page size selector defaults to "10"');
|
||||
});
|
||||
|
||||
test('it shows correct month options for billing period', async function (assert) {
|
||||
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown');
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), '');
|
||||
await triggerEvent(GENERAL.selectByAttr('attribution-month'), 'change');
|
||||
|
||||
// assert that months options in select are those of selected billing period
|
||||
const expectedMonths = this.activity.byMonth.reverse().map((m) => m.month);
|
||||
|
||||
// '' represents default state of 'Select month'
|
||||
const expectedOptions = ['', ...expectedMonths];
|
||||
const expectedOptions = ['', ...this.activity.byMonth.reverse().map((m) => m.timestamp)];
|
||||
const actualOptions = findAll(`${GENERAL.selectByAttr('attribution-month')} option`).map(
|
||||
(option) => option.value
|
||||
);
|
||||
assert.deepEqual(actualOptions, expectedOptions, 'All <option> values match expected list');
|
||||
});
|
||||
|
||||
test('it initially renders attribution with by_namespace data', async function (assert) {
|
||||
await this.renderComponent();
|
||||
const topNamespace = this.activity.byNamespace[0];
|
||||
const topMount = topNamespace.mounts[0];
|
||||
// Assert table renders namespace with the highest counts at the top
|
||||
assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText(topNamespace.label);
|
||||
assert.dom(GENERAL.tableData(0, 'clients')).hasText(`${topMount.clients}`);
|
||||
});
|
||||
|
||||
test('it renders dropdown lists from activity response to filter table data', async function (assert) {
|
||||
const expectedNamespaces = this.activity.byNamespace.map((n) => n.label);
|
||||
const mounts = flattenMounts(this.mostRecentMonth.new_clients.namespaces);
|
||||
const expectedMountPaths = [...new Set(mounts.map((m) => m.mount_path))];
|
||||
const expectedMountTypes = [...new Set(mounts.map((m) => m.mount_type))];
|
||||
await this.renderComponent();
|
||||
// Assert filters do not exist until month is selected
|
||||
assert.dom(FILTERS.dropdownToggle(ClientFilters.NAMESPACE)).doesNotExist();
|
||||
assert.dom(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH)).doesNotExist();
|
||||
assert.dom(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE)).doesNotExist();
|
||||
// Select month
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), this.mostRecentMonth.timestamp);
|
||||
// Select each filter
|
||||
await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE));
|
||||
findAll(`${FILTERS.dropdown(ClientFilters.NAMESPACE)} li button`).forEach((item, idx) => {
|
||||
const expected = 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 = 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 = expectedMountTypes[idx];
|
||||
assert.dom(item).hasText(expected, `mount_type dropdown renders: ${expected}`);
|
||||
});
|
||||
});
|
||||
|
||||
// * FILTERING ASSERTIONS
|
||||
// Filtering tests are split between integration and acceptance tests
|
||||
// because changing filters updates the URL query params/
|
||||
test('it filters attribution table by month', async function (assert) {
|
||||
await this.renderComponent();
|
||||
const mostRecentMonth = this.mostRecentMonth;
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), mostRecentMonth.timestamp);
|
||||
// Drill down to new_clients then grab the first mount
|
||||
const sortedMounts = flattenMounts(mostRecentMonth.new_clients.namespaces).sort(
|
||||
(a, b) => b.clients - a.clients
|
||||
);
|
||||
const topMount = sortedMounts[0];
|
||||
assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText(topMount.namespace_path);
|
||||
assert.dom(GENERAL.tableData(0, 'clients')).hasText(`${topMount.clients}`);
|
||||
assert.dom(GENERAL.tableData(0, 'mount_path')).hasText(topMount.mount_path);
|
||||
});
|
||||
|
||||
test('it resets pagination when a month is selected change', async function (assert) {
|
||||
const attributionByMount = flattenMounts(this.activity.byNamespace);
|
||||
await this.renderComponent();
|
||||
// Decrease page size for test so we don't have to seed more data
|
||||
await fillIn(GENERAL.paginationSizeSelector, '5');
|
||||
assert.dom(GENERAL.paginationInfo).hasText(`1–5 of ${attributionByMount.length}`);
|
||||
// Change pages because we should go back to page 1 when a month is selected
|
||||
await click(GENERAL.nextPage);
|
||||
assert.dom(GENERAL.tableRow()).exists({ count: 1 }, '1 row render');
|
||||
assert.dom(GENERAL.paginationInfo).hasText(`6–6 of ${attributionByMount.length}`);
|
||||
// Select a month and assert table resets to page 1
|
||||
await fillIn(GENERAL.selectByAttr('attribution-month'), this.mostRecentMonth.timestamp);
|
||||
const monthMounts = flattenMounts(this.mostRecentMonth.new_clients.namespaces);
|
||||
assert
|
||||
.dom(GENERAL.paginationInfo)
|
||||
.hasText(`1–5 of ${monthMounts.length}`, 'pagination resets to page one');
|
||||
assert.dom(GENERAL.tableRow()).exists({ count: 5 }, '5 rows render');
|
||||
assert.dom(GENERAL.paginationSizeSelector).hasValue('5', 'size selector does not reset to 10');
|
||||
});
|
||||
});
|
||||
|
||||
@ -17,6 +17,7 @@ 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 { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
|
||||
const START_TIME = getUnixTime(LICENSE_START);
|
||||
|
||||
@ -45,7 +46,6 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
||||
@isSecretsSyncActivated={{this.isSecretsSyncActivated}}
|
||||
@byMonthNewClients={{this.byMonthNewClients}}
|
||||
@runningTotals={{this.totalUsageCounts}}
|
||||
@upgradeData={{this.upgradesDuringActivity}}
|
||||
@isHistoricalMonth={{this.isHistoricalMonth}}
|
||||
/>
|
||||
`);
|
||||
@ -88,12 +88,9 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
||||
|
||||
// assert bar chart is correct
|
||||
findAll(CHARTS.xAxisLabel).forEach((e, i) => {
|
||||
assert
|
||||
.dom(e)
|
||||
.hasText(
|
||||
`${this.byMonthNewClients[i].month}`,
|
||||
`renders x-axis labels for bar chart: ${this.byMonthNewClients[i].month}`
|
||||
);
|
||||
const timestamp = this.byMonthNewClients[i].timestamp;
|
||||
const displayMonth = parseAPITimestamp(timestamp, 'M/yy');
|
||||
assert.dom(e).hasText(displayMonth, `renders x-axis labels for bar chart: ${displayMonth}`);
|
||||
});
|
||||
assert
|
||||
.dom(CHARTS.verticalBar)
|
||||
@ -112,27 +109,24 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
||||
|
||||
// assert each legend item is correct
|
||||
const expectedLegend = [
|
||||
{ label: 'Entity clients', color: 'rgb(28, 52, 95)' },
|
||||
{ label: 'Non-entity clients', color: 'rgb(6, 208, 146)' },
|
||||
{ label: 'Secret sync clients', color: 'rgb(145, 28, 237)' },
|
||||
{ label: 'Acme clients', color: 'rgb(2, 168, 239)' },
|
||||
{ label: 'Entity clients', color: 'rgb(66, 105, 208)' },
|
||||
{ label: 'Non-entity clients', color: 'rgb(239, 177, 23)' },
|
||||
{ label: 'Secret sync clients', color: 'rgb(108, 197, 176)' },
|
||||
{ label: 'Acme clients', color: 'rgb(255, 114, 92)' },
|
||||
];
|
||||
|
||||
findAll('.legend-item').forEach((e, i) => {
|
||||
const { label, color } = expectedLegend[i];
|
||||
assert.dom(e).hasText(label, `legend renders label: ${label}`);
|
||||
const dotColor = getComputedStyle(find(CHARTS.legendDot(i + 1))).backgroundColor;
|
||||
assert.strictEqual(dotColor, color, `actual color: ${dotColor}, expected color: ${color}`);
|
||||
assert.strictEqual(dotColor, color, `${label} - actual color: ${dotColor}, expected: ${color}`);
|
||||
});
|
||||
|
||||
// assert bar chart is correct
|
||||
findAll(CHARTS.xAxisLabel).forEach((e, i) => {
|
||||
assert
|
||||
.dom(e)
|
||||
.hasText(
|
||||
`${this.byMonthNewClients[i].month}`,
|
||||
`renders x-axis labels for bar chart: ${this.byMonthNewClients[i].month}`
|
||||
);
|
||||
const timestamp = this.byMonthNewClients[i].timestamp;
|
||||
const displayMonth = parseAPITimestamp(timestamp, 'M/yy');
|
||||
assert.dom(e).hasText(`${displayMonth}`, `renders x-axis labels for bar chart: ${displayMonth}`);
|
||||
});
|
||||
|
||||
const months = this.byMonthNewClients.length;
|
||||
@ -189,16 +183,16 @@ module('Integration | Component | clients/running-total', function (hooks) {
|
||||
|
||||
// assert each legend item is correct
|
||||
const expectedLegend = [
|
||||
{ label: 'Entity clients', color: 'rgb(28, 52, 95)' },
|
||||
{ label: 'Non-entity clients', color: 'rgb(6, 208, 146)' },
|
||||
{ label: 'Acme clients', color: 'rgb(2, 168, 239)' },
|
||||
{ label: 'Entity clients', color: 'rgb(66, 105, 208)' },
|
||||
{ label: 'Non-entity clients', color: 'rgb(239, 177, 23)' },
|
||||
{ label: 'Acme clients', color: 'rgb(255, 114, 92)' },
|
||||
];
|
||||
|
||||
findAll('.legend-item').forEach((e, i) => {
|
||||
const { label, color } = expectedLegend[i];
|
||||
assert.dom(e).hasText(label, `legend renders label: ${label}`);
|
||||
const dotColor = getComputedStyle(find(CHARTS.legendDot(i + 1))).backgroundColor;
|
||||
assert.strictEqual(dotColor, color, `actual color: ${dotColor}, expected color: ${color}`);
|
||||
assert.strictEqual(dotColor, color, `${label} - actual color: ${dotColor}, expected: ${color}`);
|
||||
});
|
||||
|
||||
const months = this.byMonthNewClients.length;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { click, fillIn, findAll, render } from '@ember/test-helpers';
|
||||
import { click, fillIn, findAll, render, waitFor } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||
@ -14,8 +14,9 @@ const MOCK_DATA = [
|
||||
{ island: 'Maldives', visit_length: 5, is_booked: false, trip_date: '2025-06-22T00:00:00.000Z' },
|
||||
{ island: 'Bora Bora', visit_length: 7, is_booked: true, trip_date: '2025-03-15T00:00:00.000Z' },
|
||||
{ island: 'Fiji', visit_length: 10, is_booked: true, trip_date: '2025-09-08T00:00:00.000Z' },
|
||||
{ island: 'Seychelles', visit_length: 6, is_booked: false, trip_date: '2025-12-03T00:00:00.000Z' },
|
||||
{ island: 'Santorini', visit_length: 4, is_booked: false, trip_date: '2026-04-10T00:00:00.000Z' },
|
||||
{ island: 'Maui', visit_length: 8, is_booked: true, trip_date: '2026-01-18T00:00:00.000Z' },
|
||||
{ island: 'Seychelles', visit_length: 6, is_booked: false, trip_date: '2025-12-03T00:00:00.000Z' },
|
||||
];
|
||||
module('Integration | Component | clients/table', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@ -70,7 +71,7 @@ module('Integration | Component | clients/table', function (hooks) {
|
||||
this.data = MOCK_DATA;
|
||||
await this.renderComponent();
|
||||
assert.dom(CLIENT_COUNT.card('table empty state')).doesNotExist();
|
||||
assert.dom(GENERAL.paginationInfo).hasText(`1–3 of ${this.data.length}`);
|
||||
assert.dom(GENERAL.paginationInfo).hasText(`1–5 of ${this.data.length}`);
|
||||
await click(GENERAL.nextPage);
|
||||
assert.dom(GENERAL.tableData(0, 'island')).hasText('Seychelles', 'it paginates the data');
|
||||
});
|
||||
@ -88,30 +89,39 @@ module('Integration | Component | clients/table', function (hooks) {
|
||||
await this.renderComponent();
|
||||
const [firstColumn, secondColumn, thirdColumn, fourthColumn] = findAll(GENERAL.icon('swap-vertical'));
|
||||
await click(firstColumn);
|
||||
assertSortOrder(['Bora Bora', 'Fiji', 'Maldives'], { column: 'island', page: 1 });
|
||||
assertSortOrder(['Bora Bora', 'Fiji', 'Maldives', 'Maui', 'Santorini'], { column: 'island', page: 1 });
|
||||
await click(GENERAL.nextPage);
|
||||
assertSortOrder(['Maui', 'Seychelles'], { column: 'island', page: 2 });
|
||||
assertSortOrder(['Seychelles'], { column: 'island', page: 2 });
|
||||
|
||||
await click(GENERAL.prevPage);
|
||||
await click(secondColumn);
|
||||
assertSortOrder(['5', '6', '7'], { column: 'visit_length', page: 1 });
|
||||
assertSortOrder(['4', '5', '6', '7', '8'], { column: 'visit_length', page: 1 });
|
||||
await click(GENERAL.nextPage);
|
||||
assertSortOrder(['8', '10'], { column: 'visit_length', page: 2 });
|
||||
assertSortOrder(['10'], { column: 'visit_length', page: 2 });
|
||||
|
||||
await click(GENERAL.prevPage);
|
||||
await click(thirdColumn);
|
||||
assertSortOrder(['false', 'false', 'true'], { column: 'is_booked', page: 1 });
|
||||
assertSortOrder(['false', 'false', 'false', 'true', 'true'], { column: 'is_booked', page: 1 });
|
||||
await click(GENERAL.nextPage);
|
||||
assertSortOrder(['true', 'true'], { column: 'is_booked', page: 2 });
|
||||
assertSortOrder(['true'], { column: 'is_booked', page: 2 });
|
||||
|
||||
await click(GENERAL.prevPage);
|
||||
await click(fourthColumn);
|
||||
assertSortOrder(['2025-03-15T00:00:00.000Z', '2025-06-22T00:00:00.000Z', '2025-09-08T00:00:00.000Z'], {
|
||||
column: 'trip_date',
|
||||
page: 1,
|
||||
});
|
||||
assertSortOrder(
|
||||
[
|
||||
'2025-03-15T00:00:00.000Z',
|
||||
'2025-06-22T00:00:00.000Z',
|
||||
'2025-09-08T00:00:00.000Z',
|
||||
'2025-12-03T00:00:00.000Z',
|
||||
'2026-01-18T00:00:00.000Z',
|
||||
],
|
||||
{
|
||||
column: 'trip_date',
|
||||
page: 1,
|
||||
}
|
||||
);
|
||||
await click(GENERAL.nextPage);
|
||||
assertSortOrder(['2025-12-03T00:00:00.000Z', '2026-01-18T00:00:00.000Z'], {
|
||||
assertSortOrder(['2026-04-10T00:00:00.000Z'], {
|
||||
column: 'trip_date',
|
||||
page: 2,
|
||||
});
|
||||
@ -125,12 +135,12 @@ module('Integration | Component | clients/table', function (hooks) {
|
||||
assert
|
||||
.dom(`${GENERAL.tableColumnHeader(2, { isAdvanced: true })} ${GENERAL.icon('arrow-down')}`)
|
||||
.exists();
|
||||
const firstPage = ['Fiji', 'Maui', 'Bora Bora'];
|
||||
const firstPage = ['Fiji', 'Maui', 'Bora Bora', 'Seychelles', 'Maldives'];
|
||||
firstPage.forEach((value, idx) => {
|
||||
assert.dom(GENERAL.tableData(idx, 'island')).hasText(value, `page 1, row ${idx} has ${value}`);
|
||||
});
|
||||
await click(GENERAL.nextPage);
|
||||
const secondPage = ['Seychelles', 'Maldives'];
|
||||
const secondPage = ['Santorini'];
|
||||
secondPage.forEach((value, idx) => {
|
||||
assert.dom(GENERAL.tableData(idx, 'island')).hasText(value, `page 2, row ${idx} has ${value}`);
|
||||
});
|
||||
@ -186,4 +196,36 @@ module('Integration | Component | clients/table', function (hooks) {
|
||||
.exists('it renders a badge for the deleted mount')
|
||||
.hasText('Deleted');
|
||||
});
|
||||
|
||||
test('it resets pagination when data changes', async function (assert) {
|
||||
// We need more than 5 rows, so here's more mock data!
|
||||
const moreData = [
|
||||
{ island: 'Tahiti', visit_length: 12, is_booked: true, trip_date: '2025-05-10T00:00:00.000Z' },
|
||||
{ island: 'Barbados', visit_length: 6, is_booked: false, trip_date: '2025-08-25T00:00:00.000Z' },
|
||||
{ island: 'Cyprus', visit_length: 9, is_booked: true, trip_date: '2026-03-12T00:00:00.000Z' },
|
||||
{ island: 'Jamaica', visit_length: 7, is_booked: false, trip_date: '2025-11-05T00:00:00.000Z' },
|
||||
{ island: 'Crete', visit_length: 11, is_booked: true, trip_date: '2026-06-18T00:00:00.000Z' },
|
||||
{ island: 'Aruba', visit_length: 5, is_booked: false, trip_date: '2025-10-14T00:00:00.000Z' },
|
||||
];
|
||||
this.data = [...MOCK_DATA, ...moreData];
|
||||
this.showPaginationSizeSelector = true;
|
||||
await this.renderComponent();
|
||||
await fillIn(GENERAL.paginationSizeSelector, '10'); // Default is 5, so change to something else
|
||||
await click(GENERAL.nextPage);
|
||||
assert.dom(GENERAL.paginationInfo).hasText(`11–12 of ${this.data.length}`, 'it navigates to next page');
|
||||
assert.dom(GENERAL.tableRow()).exists({ count: 2 }, '2 row renders');
|
||||
// Changing the @data arg should trigger an update and reset pagination
|
||||
// We have to use `this.set` to trigger did-update
|
||||
this.set('data', [
|
||||
{ island: 'Palawan', visit_length: 9, is_booked: true, trip_date: '2025-11-14T00:00:00.000Z' },
|
||||
{ island: 'Mykonos', visit_length: 3, is_booked: false, trip_date: '2026-02-28T00:00:00.000Z' },
|
||||
]);
|
||||
|
||||
// There's a workaround using next() from @ember/runloop because the Hds::Pagination::Numbered component
|
||||
// doesn't re-render when @currentPage updates. When that's fixed at the source we should be able to remove waitFor
|
||||
await waitFor(GENERAL.paginationInfo);
|
||||
assert.dom(GENERAL.paginationInfo).hasText(`1–2 of ${this.data.length}`);
|
||||
assert.dom(GENERAL.tableRow()).exists({ count: 2 }, '2 rows render');
|
||||
assert.dom(GENERAL.paginationSizeSelector).hasValue('10', 'page selector is unchanged when data updates');
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,8 +12,8 @@ import {
|
||||
formatByNamespace,
|
||||
destructureClientCounts,
|
||||
sortMonthsByTimestamp,
|
||||
filterByMonthDataForMount,
|
||||
filteredTotalForMount,
|
||||
flattenMounts,
|
||||
filterTableData,
|
||||
} from 'core/utils/client-count-utils';
|
||||
import clientsHandler from 'vault/mirage/handlers/clients';
|
||||
import {
|
||||
@ -125,6 +125,23 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
});
|
||||
});
|
||||
|
||||
test('flattenMounts: it flattens mount data', async function (assert) {
|
||||
assert.expect(2);
|
||||
const original = [...SERIALIZED_ACTIVITY_RESPONSE.by_namespace];
|
||||
const expected = [
|
||||
...SERIALIZED_ACTIVITY_RESPONSE.by_namespace[0].mounts,
|
||||
...SERIALIZED_ACTIVITY_RESPONSE.by_namespace[1].mounts,
|
||||
];
|
||||
const actual = flattenMounts(SERIALIZED_ACTIVITY_RESPONSE.by_namespace);
|
||||
|
||||
assert.propEqual(actual, expected, 'it returns mounts from each namespace object into a single array');
|
||||
assert.propEqual(
|
||||
SERIALIZED_ACTIVITY_RESPONSE.by_namespace,
|
||||
original,
|
||||
'it does not modify original by_namespace array'
|
||||
);
|
||||
});
|
||||
|
||||
test('formatByMonths: it formats the months array', async function (assert) {
|
||||
assert.expect(7);
|
||||
const original = [...RESPONSE.months];
|
||||
@ -233,6 +250,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
clients: 30,
|
||||
entity_clients: 10,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: '',
|
||||
namespace_path: 'root',
|
||||
non_entity_clients: 20,
|
||||
@ -266,6 +284,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
clients: 2,
|
||||
entity_clients: 2,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: 'no mount path (pre-1.10 upgrade?)',
|
||||
namespace_path: 'root',
|
||||
non_entity_clients: 0,
|
||||
@ -275,7 +294,8 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
entity_clients: 1,
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
namespace_path: 'root',
|
||||
non_entity_clients: 0,
|
||||
@ -294,7 +314,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
entity_clients: 3,
|
||||
month: '4/24',
|
||||
|
||||
namespaces: [
|
||||
{
|
||||
acme_clients: 0,
|
||||
@ -307,6 +327,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
clients: 2,
|
||||
entity_clients: 2,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: 'no mount path (pre-1.10 upgrade?)',
|
||||
namespace_path: 'root',
|
||||
non_entity_clients: 0,
|
||||
@ -316,7 +337,8 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
entity_clients: 1,
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
namespace_path: 'root',
|
||||
non_entity_clients: 0,
|
||||
@ -335,237 +357,111 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
);
|
||||
});
|
||||
|
||||
module('filterByMonthDataForMount', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.getExpected = (label, count = 0, newCount = 0) => {
|
||||
return {
|
||||
month: '6/23',
|
||||
namespaces: [],
|
||||
label,
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
acme_clients: count,
|
||||
clients: count,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
new_clients: {
|
||||
month: '6/23',
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
namespaces: [],
|
||||
label,
|
||||
acme_clients: newCount,
|
||||
clients: newCount,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
module('filterTableData', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const activityByMount = flattenMounts(SERIALIZED_ACTIVITY_RESPONSE.by_namespace);
|
||||
this.mockMountData = [...activityByMount];
|
||||
// copy mock data before using the filterTableData function to assert filtering doesn't modify the original array
|
||||
const original = [...this.mockMountData];
|
||||
this.assertOriginal = (assert) =>
|
||||
assert.propEqual(this.mockMountData, original, 'filtering does not mutate dataset');
|
||||
});
|
||||
|
||||
test('it works when month has no data', async function (assert) {
|
||||
const months = [
|
||||
test('it returns original data if no filters are passed', async function (assert) {
|
||||
const emptyObject = filterTableData(this.mockMountData, {});
|
||||
assert.propEqual(emptyObject, this.mockMountData, 'when filters arg is an empty object');
|
||||
this.assertOriginal(assert);
|
||||
|
||||
const emptyValues = filterTableData(this.mockMountData, {
|
||||
namespace_path: '',
|
||||
mount_path: '',
|
||||
mount_type: '',
|
||||
});
|
||||
assert.propEqual(emptyValues, this.mockMountData, 'when filters have are empty strings');
|
||||
this.assertOriginal(assert);
|
||||
|
||||
const nullFilter = filterTableData(this.mockMountData, null);
|
||||
assert.propEqual(nullFilter, this.mockMountData, 'returns all data when no filters are null');
|
||||
this.assertOriginal(assert);
|
||||
});
|
||||
|
||||
test('it filters data for a single filter', async function (assert) {
|
||||
const namespaceFilter = filterTableData(this.mockMountData, {
|
||||
namespace_path: 'root',
|
||||
mount_path: '',
|
||||
mount_type: '',
|
||||
});
|
||||
const expectedNamespaceFilter = this.mockMountData.filter((m) => m.namespace_path === 'root');
|
||||
assert.propEqual(namespaceFilter, expectedNamespaceFilter, 'it filters by namespace_path');
|
||||
this.assertOriginal(assert);
|
||||
|
||||
const mountPathFilter = filterTableData(this.mockMountData, {
|
||||
namespace_path: '',
|
||||
mount_path: 'acme/pki/0',
|
||||
mount_type: '',
|
||||
});
|
||||
const expectedMountPathFilter = this.mockMountData.filter((m) => m.mount_path === 'acme/pki/0');
|
||||
assert.propEqual(mountPathFilter, expectedMountPathFilter, 'it filters by mount_path');
|
||||
this.assertOriginal(assert);
|
||||
|
||||
const mountTypeFilter = filterTableData(this.mockMountData, {
|
||||
namespace_path: '',
|
||||
mount_path: '',
|
||||
mount_type: 'userpass',
|
||||
});
|
||||
const expectedMountTypeFilter = this.mockMountData.filter((m) => m.mount_type === 'userpass');
|
||||
assert.propEqual(mountTypeFilter, expectedMountTypeFilter, 'it filters by mount_type');
|
||||
this.assertOriginal(assert);
|
||||
});
|
||||
|
||||
test('it filters data for a multiple filters', async function (assert) {
|
||||
const twoFilters = filterTableData(this.mockMountData, {
|
||||
namespace_path: 'root',
|
||||
mount_path: '',
|
||||
mount_type: 'userpass',
|
||||
});
|
||||
const expectedTwoFilters = this.mockMountData.filter(
|
||||
(m) => m.namespace_path === 'root' && m.mount_type === 'userpass'
|
||||
);
|
||||
assert.propEqual(twoFilters, expectedTwoFilters, 'it filters by namespace_path and mount_type');
|
||||
this.assertOriginal(assert);
|
||||
|
||||
const allFilters = filterTableData(this.mockMountData, {
|
||||
namespace_path: 'root',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
});
|
||||
const expectedAllFilters = [
|
||||
{
|
||||
month: '6/23',
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
namespaces: [],
|
||||
new_clients: {
|
||||
month: '6/23',
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
namespaces: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
const result = filterByMonthDataForMount(months, 'root', 'some-mount');
|
||||
// no data is different than zero, it implies no data was being saved at that time
|
||||
// so we don't fill in missing data with zeros to differentiate those two states
|
||||
assert.deepEqual(result[0], months[0], 'does not change month when no data');
|
||||
});
|
||||
|
||||
test('it works when month has no new clients', async function (assert) {
|
||||
const months = [
|
||||
{
|
||||
month: '6/23',
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
acme_clients: 11,
|
||||
clients: 11,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
namespaces: [
|
||||
{
|
||||
label: 'root',
|
||||
acme_clients: 11,
|
||||
clients: 11,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
mounts: [
|
||||
{
|
||||
label: 'some-mount',
|
||||
acme_clients: 11,
|
||||
clients: 11,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
month: '6/23',
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
namespaces: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let result = filterByMonthDataForMount(months, 'root', 'some-mount');
|
||||
assert.propEqual(result[0], this.getExpected('some-mount', 11), 'works when mount is found');
|
||||
result = filterByMonthDataForMount(months, 'root', 'another-mount');
|
||||
assert.deepEqual(result[0], this.getExpected('another-mount', 0), 'works when mount is not found');
|
||||
result = filterByMonthDataForMount(months, 'unknown-child', 'some-mount');
|
||||
assert.deepEqual(result[0], this.getExpected('some-mount', 0), 'works when namespace is not found');
|
||||
});
|
||||
|
||||
test('it works when month has new clients', async function (assert) {
|
||||
const months = [
|
||||
{
|
||||
month: '6/23',
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
acme_clients: 22,
|
||||
clients: 22,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
namespaces: [
|
||||
{
|
||||
label: 'root',
|
||||
acme_clients: 22,
|
||||
clients: 22,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
mounts: [
|
||||
{
|
||||
label: 'some-mount',
|
||||
acme_clients: 22,
|
||||
clients: 22,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
month: '6/23',
|
||||
timestamp: '2023-06-01T00:00:00Z',
|
||||
namespaces: [
|
||||
{
|
||||
label: 'root',
|
||||
acme_clients: 11,
|
||||
clients: 11,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
mounts: [
|
||||
{
|
||||
label: 'some-mount',
|
||||
acme_clients: 11,
|
||||
clients: 11,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
let result = filterByMonthDataForMount(months, 'root', 'some-mount');
|
||||
assert.propEqual(result[0], this.getExpected('some-mount', 22, 11), 'works when mount is found');
|
||||
result = filterByMonthDataForMount(months, 'root', 'another-mount');
|
||||
assert.deepEqual(result[0], this.getExpected('another-mount', 0), 'works when mount is not found');
|
||||
result = filterByMonthDataForMount(months, 'unknown-child', 'some-mount');
|
||||
assert.deepEqual(result[0], this.getExpected('some-mount', 0), 'works when namespace is not found');
|
||||
});
|
||||
});
|
||||
|
||||
module('filteredTotalForMount', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.byNamespace = SERIALIZED_ACTIVITY_RESPONSE.by_namespace;
|
||||
this.byMonth = SERIALIZED_ACTIVITY_RESPONSE.by_month;
|
||||
});
|
||||
|
||||
const emptyCounts = {
|
||||
acme_clients: 0,
|
||||
clients: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
};
|
||||
|
||||
[
|
||||
{
|
||||
when: 'no namespace filter passed',
|
||||
result: 'it returns empty counts',
|
||||
ns: '',
|
||||
mount: 'auth/userpass-0',
|
||||
expected: emptyCounts,
|
||||
},
|
||||
{
|
||||
when: 'no mount filter passed',
|
||||
result: 'it returns empty counts',
|
||||
ns: 'ns1',
|
||||
mount: '',
|
||||
expected: emptyCounts,
|
||||
},
|
||||
{
|
||||
when: 'no matching ns/mount exists',
|
||||
result: 'it returns empty counts',
|
||||
ns: 'ns1',
|
||||
mount: 'auth/userpass-1',
|
||||
expected: emptyCounts,
|
||||
},
|
||||
{
|
||||
when: 'mount and label have extra slashes',
|
||||
result: 'it returns the data sanitized',
|
||||
ns: 'ns1/',
|
||||
mount: 'auth/userpass-0',
|
||||
expected: {
|
||||
label: 'auth/userpass-0',
|
||||
label: 'auth/userpass/0',
|
||||
mount_path: 'auth/userpass/0',
|
||||
mount_type: 'userpass',
|
||||
namespace_path: 'ns1',
|
||||
acme_clients: 0,
|
||||
clients: 8394,
|
||||
entity_clients: 4256,
|
||||
non_entity_clients: 4138,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
when: 'mount within root',
|
||||
result: 'it returns the data',
|
||||
ns: 'root',
|
||||
mount: 'kvv2-engine-0',
|
||||
expected: {
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
namespace_path: 'root',
|
||||
acme_clients: 0,
|
||||
clients: 4290,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 4290,
|
||||
clients: 8091,
|
||||
entity_clients: 4002,
|
||||
non_entity_clients: 4089,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
},
|
||||
].forEach((testCase) => {
|
||||
test(`it returns correct values when ${testCase.when}`, async function (assert) {
|
||||
const actual = filteredTotalForMount(this.byNamespace, testCase.ns, testCase.mount);
|
||||
assert.deepEqual(actual, testCase.expected);
|
||||
];
|
||||
assert.propEqual(allFilters, expectedAllFilters, 'it filters by all filters');
|
||||
this.assertOriginal(assert);
|
||||
});
|
||||
|
||||
test('it returns an empty array when there are no matches', async function (assert) {
|
||||
const noMatches = filterTableData(this.mockMountData, {
|
||||
namespace_path: 'does not exist',
|
||||
mount_path: '',
|
||||
mount_type: '',
|
||||
});
|
||||
assert.propEqual(noMatches, [], 'returns an empty array when no data matches filters');
|
||||
this.assertOriginal(assert);
|
||||
});
|
||||
|
||||
test('it returns an empty array when filter includes keys the dataset does not contain', async function (assert) {
|
||||
const noMatches = filterTableData(this.mockMountData, { foo: 'root', bar: '' });
|
||||
assert.propEqual(noMatches, [], 'returns an empty array when no keys match dataset');
|
||||
this.assertOriginal(assert);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user