mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
UI: Replace Client Count Attribution Charts with Table (#30678)
* replace attribution charts with a table by month * update tests to include mount_type * fix another portion of tests that were missing secret sync stat and testing for old attribution charts * add tests for attribution table * add changelog and tidy * remove remaining todos * tidy * reset month query param in ce * fix tests missing month param * add margin to pagination in accordance to helios rec * remove query param, update change log, move table into own comp * remove commented code * remove month query params * tidy * update test mount paths * remove unused client attribution component * update tests
This commit is contained in:
parent
9de78a5136
commit
2e6d5b0703
17 changed files with 399 additions and 434 deletions
5
changelog/30678.txt
Normal file
5
changelog/30678.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
```release-note:change
|
||||
ui/activity: Replaces mount and namespace attribution charts with a table to allow sorting
|
||||
client count data by `namespace`, `mount_path`, `mount_type` or number of clients for
|
||||
a selected month.
|
||||
```
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<div class="chart-wrapper single-chart-grid" data-test-clients-attribution={{this.noun}}>
|
||||
<div class="chart-header has-bottom-margin-m">
|
||||
<h2 class="chart-title" data-test-attribution-title>{{capitalize this.noun}} attribution</h2>
|
||||
<p class="chart-description" data-test-attribution-description>{{this.chartText.description}}</p>
|
||||
</div>
|
||||
{{#if @attribution}}
|
||||
<div class="chart-container-wide" data-test-chart-container={{this.noun}}>
|
||||
<Clients::HorizontalBarChart @dataset={{this.topTenAttribution}} @chartLegend={{this.attributionLegend}} />
|
||||
</div>
|
||||
<div class="chart-subTitle">
|
||||
<p class="chart-subtext" data-test-attribution-subtext>{{this.chartText.subtext}}</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-top" data-test-top-attribution>
|
||||
<h3 class="data-details">Top {{this.noun}}</h3>
|
||||
<p class="data-details is-word-break">{{this.topAttribution.label}}</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-bottom" data-test-attribution-clients>
|
||||
<h3 class="data-details">Clients in {{this.noun}}</h3>
|
||||
<p class="data-details">{{format-number this.topAttribution.clients}}</p>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
{{#each this.attributionLegend as |legend idx|}}
|
||||
<span class="legend-colors dot-{{idx}}"></span><span class="legend-label">{{capitalize legend.label}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="chart-empty-state">
|
||||
<EmptyState @icon="skip" @title="No data found" @bottomBorder={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
/**
|
||||
* @module Attribution
|
||||
* Attribution components display the top 10 total client counts for namespaces or mounts during a billing period.
|
||||
* A horizontal bar chart shows on the right, with the top namespace/mount and respective client totals on the left.
|
||||
*
|
||||
* @example
|
||||
* <Clients::Attribution
|
||||
* @noun="mount"
|
||||
* @attribution={{array (hash label="my-kv" clients=100)}}
|
||||
* @isSecretsSyncActivated={{true}}
|
||||
* />
|
||||
*
|
||||
* @param {string} noun - noun which reflects the type of data and used in title. Should be "namespace" (default) or "mount"
|
||||
* @param {array} attribution - array of objects containing a label and breakdown of client counts for total clients
|
||||
* @param {boolean} isSecretsSyncActivated - boolean reflecting if secrets sync is activated. Determines the labels and data shown
|
||||
*/
|
||||
|
||||
export default class Attribution extends Component {
|
||||
get noun() {
|
||||
return this.args.noun || 'namespace';
|
||||
}
|
||||
|
||||
get attributionLegend() {
|
||||
const attributionLegend = [
|
||||
{ key: 'entity_clients', label: 'entity clients' },
|
||||
{ key: 'non_entity_clients', label: 'non-entity clients' },
|
||||
{ key: 'acme_clients', label: 'ACME clients' },
|
||||
];
|
||||
|
||||
if (this.args.isSecretsSyncActivated) {
|
||||
attributionLegend.push({ key: 'secret_syncs', label: 'secrets sync clients' });
|
||||
}
|
||||
return attributionLegend;
|
||||
}
|
||||
|
||||
get sortedAttribution() {
|
||||
if (this.args.attribution) {
|
||||
// shallow copy so it doesn't mutate the data during tests
|
||||
return this.args.attribution?.slice().sort((a, b) => b.clients - a.clients);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// truncate data before sending to chart component
|
||||
get topTenAttribution() {
|
||||
return this.sortedAttribution.slice(0, 10);
|
||||
}
|
||||
|
||||
get topAttribution() {
|
||||
// get top namespace or mount
|
||||
return this.sortedAttribution[0] ?? null;
|
||||
}
|
||||
|
||||
get chartText() {
|
||||
if (this.noun === 'namespace') {
|
||||
return {
|
||||
subtext: 'This data shows the top ten namespaces by total clients for the date range selected.',
|
||||
description:
|
||||
'This data shows the top ten namespaces by total clients and can be used to understand where clients are originating. Namespaces are identified by path.',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
subtext:
|
||||
'The total clients used by the mounts for this date range. This number is useful for identifying overall usage volume.',
|
||||
description:
|
||||
'This data shows the top ten mounts by client count within this namespace, and can be used to understand where clients are originating. Mounts are organized by path.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ interface OnChangeParams {
|
|||
start_time: number | undefined;
|
||||
end_time: number | undefined;
|
||||
}
|
||||
|
||||
interface Args {
|
||||
onChange: (callback: OnChangeParams) => void;
|
||||
setEditModalVisible: (visible: boolean) => void;
|
||||
|
|
|
|||
|
|
@ -14,16 +14,21 @@
|
|||
/>
|
||||
|
||||
{{#if this.hasAttributionData}}
|
||||
<Clients::Attribution
|
||||
@noun="namespace"
|
||||
@attribution={{@activity.byNamespace}}
|
||||
@isSecretsSyncActivated={{this.flags.secretsSyncIsActivated}}
|
||||
/>
|
||||
{{#if this.namespaceMountAttribution}}
|
||||
<Clients::Attribution
|
||||
@noun="mount"
|
||||
@attribution={{this.namespaceMountAttribution}}
|
||||
@isSecretsSyncActivated={{this.flags.secretsSyncIsActivated}}
|
||||
/>
|
||||
{{/if}}
|
||||
<Hds::Form::Select::Base
|
||||
class="has-bottom-margin-m"
|
||||
aria-label="Month"
|
||||
name="month"
|
||||
{{on "input" this.selectMonth}}
|
||||
@width="200px"
|
||||
data-test-select="attribution-month"
|
||||
as |S|
|
||||
>
|
||||
<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>
|
||||
{{/each}}
|
||||
</S.Options>
|
||||
</Hds::Form::Select::Base>
|
||||
<Clients::Table @data={{this.tableData}} />
|
||||
{{/if}}
|
||||
|
|
@ -5,23 +5,40 @@
|
|||
|
||||
import ActivityComponent from '../activity';
|
||||
import { service } from '@ember/service';
|
||||
import { sanitizePath } from 'core/utils/sanitize-path';
|
||||
import { 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 type FlagsService from 'vault/services/flags';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
|
||||
export default class ClientsOverviewPageComponent extends ActivityComponent {
|
||||
@service declare readonly flags: FlagsService;
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
|
||||
@tracked selectedMonth = '';
|
||||
|
||||
get hasAttributionData() {
|
||||
// we hide attribution data when mountPath filter present
|
||||
// we hide attribution table when mountPath filter present
|
||||
// or if there's no data
|
||||
if (this.args.mountPath || !this.totalUsageCounts.clients) return false;
|
||||
return true;
|
||||
return !this.args.mountPath && this.totalUsageCounts.clients;
|
||||
}
|
||||
|
||||
// mounts attribution
|
||||
get namespaceMountAttribution() {
|
||||
const { activity } = this.args;
|
||||
const nsLabel = this.namespacePathForFilter;
|
||||
return activity?.byNamespace?.find((ns) => sanitizePath(ns.label) === nsLabel)?.mounts || [];
|
||||
get months() {
|
||||
return this.byMonthNewClients.map((m) => ({
|
||||
display: parseAPITimestamp(m.timestamp, 'MMMM yyyy'),
|
||||
value: m.month,
|
||||
}));
|
||||
}
|
||||
|
||||
get tableData(): TableData[] | undefined {
|
||||
if (!this.selectedMonth) return undefined;
|
||||
return formatTableData(this.byMonthNewClients, this.selectedMonth);
|
||||
}
|
||||
|
||||
@action
|
||||
selectMonth(e: HTMLElementEvent<HTMLInputElement>) {
|
||||
this.selectedMonth = e.target.value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
ui/app/components/clients/table.hbs
Normal file
59
ui/app/components/clients/table.hbs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if @data}}
|
||||
<Hds::Table
|
||||
@model={{this.paginatedTableData}}
|
||||
@columns={{array
|
||||
(hash key="namespace" label="Namespace" isSortable=true)
|
||||
(hash key="label" label="Mount path" isSortable=true)
|
||||
(hash key="mount_type" label="Mount type" isSortable=true)
|
||||
(hash key="clients" label="Counts" isSortable=true)
|
||||
}}
|
||||
@sortBy="clients"
|
||||
@sortOrder="desc"
|
||||
@onSort={{this.updateSort}}
|
||||
data-test-clients-attribution-table
|
||||
>
|
||||
<:body as |B|>
|
||||
<B.Tr data-test-attribution-table-row>
|
||||
<B.Td>{{B.data.namespace}}</B.Td>
|
||||
<B.Td>{{B.data.label}}</B.Td>
|
||||
{{#if (eq B.data.mount_type "deleted mount")}}
|
||||
<B.Td><Hds::Badge @text="Deleted" @type="outlined" /></B.Td>
|
||||
{{else}}
|
||||
<B.Td>{{B.data.mount_type}}</B.Td>
|
||||
{{/if}}
|
||||
<B.Td data-test-attribution-table-counts={{B.rowIndex}}>{{B.data.clients}}</B.Td>
|
||||
</B.Tr>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
|
||||
<Hds::Pagination::Numbered
|
||||
@showSizeSelector={{false}}
|
||||
@totalItems={{@data.length}}
|
||||
@currentPage={{this.currentPage}}
|
||||
@pageSize={{this.pageSize}}
|
||||
@currentPageSize={{this.pageSize}}
|
||||
@onPageChange={{this.onPageChange}}
|
||||
data-test-pagination
|
||||
class="has-top-margin-m"
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l" data-test-card="attribution">
|
||||
<Hds::ApplicationState as |A|>
|
||||
<A.Header @title={{this.tableHeaderMessage}} />
|
||||
<A.Body @text={{this.tableBodyMessage}} />
|
||||
<A.Footer as |F|>
|
||||
<F.LinkStandalone
|
||||
@icon="file-text"
|
||||
@text="Client count documentation"
|
||||
@href="https://developer.hashicorp.com/vault/docs/concepts/client-count"
|
||||
@iconPosition="trailing"
|
||||
/>
|
||||
</A.Footer>
|
||||
</Hds::ApplicationState>
|
||||
</Hds::Card::Container>
|
||||
{{/if}}
|
||||
77
ui/app/components/clients/table.ts
Normal file
77
ui/app/components/clients/table.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import Ember from 'ember';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { paginate } from 'core/utils/paginate-list';
|
||||
import { MountClients } from 'core/utils/client-count-utils';
|
||||
|
||||
interface TableData extends MountClients {
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
interface Args {
|
||||
data: TableData[];
|
||||
}
|
||||
|
||||
type TableColumn = 'namespace' | 'label' | 'mount_type' | 'clients';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export default class Table extends Component<Args> {
|
||||
@tracked currentPage = 1;
|
||||
@tracked sortColumn: TableColumn = 'clients';
|
||||
@tracked sortDirection: SortDirection = 'desc';
|
||||
|
||||
pageSize = Ember.testing ? 3 : 10; // lower in tests to test pagination without seeding more data
|
||||
|
||||
get paginatedTableData(): TableData[] {
|
||||
const sorted = this.sortTableData(this.args.data);
|
||||
const paginated = paginate(sorted, {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
});
|
||||
|
||||
return paginated;
|
||||
}
|
||||
|
||||
get tableHeaderMessage(): string {
|
||||
return this.args.data
|
||||
? 'No data is available for the selected month'
|
||||
: 'Select a month to view client attribution';
|
||||
}
|
||||
|
||||
get tableBodyMessage(): string {
|
||||
return this.args.data
|
||||
? 'View the namespace mount breakdown of clients by selecting another month.'
|
||||
: 'View the namespace mount breakdown of clients by selecting a month.';
|
||||
}
|
||||
|
||||
sortTableData(data: TableData[]): TableData[] {
|
||||
if (this.sortColumn) {
|
||||
data = [...data].sort((a, b) => {
|
||||
const valA = a[this.sortColumn];
|
||||
const valB = b[this.sortColumn];
|
||||
|
||||
if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@action
|
||||
onPageChange(page: number) {
|
||||
this.currentPage = page;
|
||||
}
|
||||
|
||||
@action
|
||||
updateSort(column: TableColumn, direction: SortDirection) {
|
||||
this.sortColumn = column;
|
||||
this.sortDirection = direction;
|
||||
}
|
||||
}
|
||||
|
|
@ -205,7 +205,11 @@ export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByN
|
|||
// transform to an empty array for type consistency
|
||||
let mounts: MountClients[] | [] = [];
|
||||
if (Array.isArray(ns.mounts)) {
|
||||
mounts = ns.mounts.map((m) => ({ label: m.mount_path, ...destructureClientCounts(m.counts) }));
|
||||
mounts = ns.mounts.map((m) => ({
|
||||
label: m.mount_path,
|
||||
mount_type: m.mount_type,
|
||||
...destructureClientCounts(m.counts),
|
||||
}));
|
||||
}
|
||||
return {
|
||||
label,
|
||||
|
|
@ -215,6 +219,23 @@ 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: 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)
|
||||
|
|
@ -281,6 +302,7 @@ export interface ByNamespaceClients extends TotalClients {
|
|||
|
||||
export interface MountClients extends TotalClients {
|
||||
label: string;
|
||||
mount_type: string;
|
||||
}
|
||||
|
||||
export interface ByMonthClients extends TotalClients {
|
||||
|
|
@ -322,13 +344,17 @@ export interface MountNewClients extends TotalClientsSometimes {
|
|||
label: string;
|
||||
}
|
||||
|
||||
export interface TableData extends MountClients {
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
// API RESPONSE SHAPE (prior to serialization)
|
||||
|
||||
export interface NamespaceObject {
|
||||
namespace_id: string;
|
||||
namespace_path: string;
|
||||
counts: Counts;
|
||||
mounts: { mount_path: string; counts: Counts }[];
|
||||
mounts: { mount_path: string; counts: Counts; mount_type: string }[];
|
||||
}
|
||||
|
||||
type ActivityMonthStandard = {
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ module('Acceptance | clients | counts', function (hooks) {
|
|||
counts: getCounts(),
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
counts: getCounts(),
|
||||
},
|
||||
],
|
||||
|
|
@ -145,7 +145,7 @@ module('Acceptance | clients | counts', function (hooks) {
|
|||
counts: getCounts(),
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
counts: getCounts(),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ module('Acceptance | clients | counts | acme', function (hooks) {
|
|||
assert.expect(3);
|
||||
await visit('/vault/clients/counts/acme');
|
||||
await selectChoose(CLIENT_COUNT.nsFilter, this.nsPath);
|
||||
await selectChoose(CLIENT_COUNT.mountFilter, 'auth/authid/0');
|
||||
await selectChoose(CLIENT_COUNT.mountFilter, 'auth/userpass-0');
|
||||
// no data because this is an auth mount (acme_clients come from pki mounts)
|
||||
assert.dom(CLIENT_COUNT.statText('Total ACME clients')).hasTextContaining('0');
|
||||
assert.dom(`${CHARTS.chart('ACME usage')} ${CHARTS.verticalBar}`).isNotVisible();
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
|
||||
.hasText('January 2024', 'billing start month is correctly parsed from license');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
|
||||
assert
|
||||
.dom(CHARTS.container('Vault client counts'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
|
|
@ -87,9 +86,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||
assert
|
||||
.dom(CHARTS.container('Vault client counts'))
|
||||
.doesNotExist('running total month over month charts do not show');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
|
||||
assert.dom(CHARTS.container('namespace')).exists('namespace attribution chart shows');
|
||||
assert.dom(CHARTS.container('mount')).exists('mount attribution chart shows');
|
||||
|
||||
// change to start on month/year of upgrade to 1.10
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
|
|
@ -99,7 +95,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
|
||||
.hasText('September 2023', 'billing start month is correctly parsed from license');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
|
||||
assert
|
||||
.dom(CHARTS.container('Vault client counts'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
|
|
@ -120,9 +115,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||
assert
|
||||
.dom(CHARTS.container('Vault client counts'))
|
||||
.doesNotExist('running total month over month charts do not show');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
|
||||
assert.dom(CHARTS.container('namespace')).exists('namespace attribution chart shows');
|
||||
assert.dom(CHARTS.container('mount')).exists('mount attribution chart shows');
|
||||
|
||||
// query historical date range (from September 2023 to December 2023)
|
||||
await click(CLIENT_COUNT.dateRange.edit);
|
||||
|
|
@ -136,7 +128,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||
assert
|
||||
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
|
||||
.hasText('December 2023', 'billing start month is correctly parsed from license');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
|
||||
assert
|
||||
.dom(CHARTS.container('Vault client counts'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
|
|
@ -163,7 +154,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||
assert
|
||||
.dom(CHARTS.container('Vault client counts'))
|
||||
.exists('Shows running totals with monthly breakdown charts');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
|
||||
|
||||
const response = await this.store.peekRecord('clients/activity', 'some-activity-id');
|
||||
const orderedNs = response.byNamespace.sort((a, b) => b.clients - a.clients);
|
||||
|
|
@ -172,23 +162,9 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||
const filterNamespace = topNamespace.label === 'root' ? orderedNs[1] : topNamespace;
|
||||
const topMount = filterNamespace?.mounts.sort((a, b) => b.clients - a.clients)[0];
|
||||
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.attributionBlock('namespace')} [data-test-top-attribution]`)
|
||||
.includesText(`Top namespace ${topNamespace.label}`);
|
||||
// this math works because there are no nested namespaces in the mirage data
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.attributionBlock('namespace')} [data-test-attribution-clients] p`)
|
||||
.includesText(`${formatNumber([topNamespace.clients])}`, 'top attribution clients accurate');
|
||||
|
||||
// Filter by top namespace
|
||||
await selectChoose(CLIENT_COUNT.nsFilter, filterNamespace.label);
|
||||
assert.dom(CLIENT_COUNT.selectedNs).hasText(filterNamespace.label, 'selects top namespace');
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-top-attribution]`)
|
||||
.includesText('Top mount');
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-attribution-clients] p`)
|
||||
.includesText(`${formatNumber([topMount.clients])}`, 'top attribution clients accurate');
|
||||
|
||||
let expectedStats = {
|
||||
Entity: formatNumber([filterNamespace.entity_clients]),
|
||||
|
|
@ -205,7 +181,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||
// FILTER BY AUTH METHOD
|
||||
await selectChoose(CLIENT_COUNT.mountFilter, topMount.label);
|
||||
assert.dom(CLIENT_COUNT.selectedAuthMount).hasText(topMount.label, 'selects top mount');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock()).doesNotExist('Does not show attribution block');
|
||||
|
||||
expectedStats = {
|
||||
Entity: formatNumber([topMount.entity_clients]),
|
||||
|
|
@ -222,13 +197,6 @@ module('Acceptance | clients | overview', function (hooks) {
|
|||
// 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');
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
|
||||
assert
|
||||
.dom('[data-test-attribution-clients]')
|
||||
.hasTextContaining(
|
||||
`${formatNumber([topNamespace.clients])}`,
|
||||
'top attribution clients back to unfiltered value'
|
||||
);
|
||||
|
||||
expectedStats = {
|
||||
Entity: formatNumber([response.total.entity_clients]),
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 8394,
|
||||
|
|
@ -56,6 +57,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 4810,
|
||||
|
|
@ -66,6 +68,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 5699,
|
||||
clients: 5699,
|
||||
|
|
@ -88,7 +91,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 8091,
|
||||
|
|
@ -99,6 +103,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 4290,
|
||||
|
|
@ -109,6 +114,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 4003,
|
||||
clients: 4003,
|
||||
|
|
@ -150,6 +156,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
mounts: [
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
|
|
@ -159,7 +166,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 200,
|
||||
|
|
@ -170,6 +178,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 100,
|
||||
|
|
@ -203,6 +212,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
mounts: [
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
|
|
@ -212,7 +222,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 200,
|
||||
|
|
@ -223,6 +234,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 100,
|
||||
|
|
@ -259,6 +271,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
mounts: [
|
||||
{
|
||||
mount_path: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
counts: {
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
|
|
@ -268,7 +281,8 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 200,
|
||||
|
|
@ -279,6 +293,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
{
|
||||
mount_path: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 100,
|
||||
|
|
@ -327,7 +342,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 890,
|
||||
|
|
@ -370,7 +385,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 872,
|
||||
|
|
@ -423,7 +438,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 75,
|
||||
|
|
@ -456,7 +471,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/authid/0',
|
||||
mount_path: 'auth/userpass-0',
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 96,
|
||||
|
|
@ -530,6 +545,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
|||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: 'no mount path (pre-1.10 upgrade?)',
|
||||
},
|
||||
{
|
||||
counts: {
|
||||
|
|
@ -539,7 +555,8 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
|||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'auth/u/',
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
},
|
||||
],
|
||||
namespace_id: 'root',
|
||||
|
|
@ -580,6 +597,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
|||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: 'no mount path (pre-1.10 upgrade?)',
|
||||
},
|
||||
{
|
||||
counts: {
|
||||
|
|
@ -589,7 +607,8 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
|||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'auth/u/',
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
},
|
||||
],
|
||||
namespace_id: 'root',
|
||||
|
|
@ -623,6 +642,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
|||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: 'no mount path (pre-1.10 upgrade?)',
|
||||
},
|
||||
{
|
||||
counts: {
|
||||
|
|
@ -632,7 +652,8 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
|||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'auth/u/',
|
||||
mount_path: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
},
|
||||
],
|
||||
namespace_id: 'root',
|
||||
|
|
@ -664,7 +685,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
secret_syncs: 4810,
|
||||
mounts: [
|
||||
{
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 8394,
|
||||
entity_clients: 4256,
|
||||
|
|
@ -673,6 +695,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 4810,
|
||||
entity_clients: 0,
|
||||
|
|
@ -681,6 +704,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 5699,
|
||||
clients: 5699,
|
||||
entity_clients: 0,
|
||||
|
|
@ -698,7 +722,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
secret_syncs: 4290,
|
||||
mounts: [
|
||||
{
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 8091,
|
||||
entity_clients: 4002,
|
||||
|
|
@ -707,6 +732,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 4290,
|
||||
entity_clients: 0,
|
||||
|
|
@ -715,6 +741,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 4003,
|
||||
clients: 4003,
|
||||
entity_clients: 0,
|
||||
|
|
@ -754,6 +781,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
|
|
@ -761,7 +789,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 200,
|
||||
entity_clients: 100,
|
||||
|
|
@ -770,6 +799,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
|
|
@ -798,6 +828,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
|
|
@ -805,7 +836,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 200,
|
||||
entity_clients: 100,
|
||||
|
|
@ -814,6 +846,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
|
|
@ -844,6 +877,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 100,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
|
|
@ -851,7 +885,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 200,
|
||||
entity_clients: 100,
|
||||
|
|
@ -860,6 +895,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 100,
|
||||
entity_clients: 0,
|
||||
|
|
@ -901,7 +937,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
acme_clients: 0,
|
||||
clients: 890,
|
||||
entity_clients: 708,
|
||||
|
|
@ -935,7 +971,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
acme_clients: 0,
|
||||
clients: 872,
|
||||
entity_clients: 124,
|
||||
|
|
@ -972,6 +1008,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
mounts: [
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 91,
|
||||
clients: 91,
|
||||
entity_clients: 0,
|
||||
|
|
@ -979,7 +1016,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 75,
|
||||
entity_clients: 25,
|
||||
|
|
@ -988,6 +1026,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 25,
|
||||
entity_clients: 0,
|
||||
|
|
@ -1005,7 +1044,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
secret_syncs: 24,
|
||||
mounts: [
|
||||
{
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 96,
|
||||
entity_clients: 34,
|
||||
|
|
@ -1014,6 +1054,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'pki-engine-0',
|
||||
mount_type: 'pki',
|
||||
acme_clients: 53,
|
||||
clients: 53,
|
||||
entity_clients: 0,
|
||||
|
|
@ -1022,6 +1063,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||
},
|
||||
{
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 24,
|
||||
entity_clients: 0,
|
||||
|
|
|
|||
|
|
@ -26,8 +26,14 @@ export const CLIENT_COUNT = {
|
|||
statTextValue: (label: string) =>
|
||||
label ? `[data-test-stat-text="${label}"] .stat-value` : '[data-test-stat-text]',
|
||||
usageStats: (title: string) => `[data-test-usage-stats="${title}"]`,
|
||||
attributionBlock: (type: string) =>
|
||||
type ? `[data-test-clients-attribution="${type}"]` : '[data-test-clients-attribution]',
|
||||
attribution: {
|
||||
card: '[data-test-card="attribution"]',
|
||||
table: '[data-test-clients-attribution-table]',
|
||||
row: '[data-test-attribution-table-row',
|
||||
counts: (index: number) => `[data-test-attribution-table-counts="${index}"]`,
|
||||
pagination: '[data-test-pagination',
|
||||
paginationInfo: '.hds-pagination-info',
|
||||
},
|
||||
filterBar: '[data-test-clients-filter-bar]',
|
||||
nsFilter: '#namespace-search-select',
|
||||
mountFilter: '#mounts-search-select',
|
||||
|
|
|
|||
|
|
@ -1,181 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { formatRFC3339 } from 'date-fns';
|
||||
import subMonths from 'date-fns/subMonths';
|
||||
import timestamp from 'core/utils/timestamp';
|
||||
import { SERIALIZED_ACTIVITY_RESPONSE } from 'vault/tests/helpers/clients/client-count-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { CLIENT_TYPES } from 'core/utils/client-count-utils';
|
||||
|
||||
const CLIENTS_ATTRIBUTION = {
|
||||
title: '[data-test-attribution-title]',
|
||||
description: '[data-test-attribution-description]',
|
||||
subtext: '[data-test-attribution-subtext]',
|
||||
timestamp: '[data-test-attribution-timestamp]',
|
||||
chart: '[data-test-horizontal-bar-chart]',
|
||||
topItem: '[data-test-top-attribution]',
|
||||
topItemCount: '[data-test-attribution-clients]',
|
||||
yLabel: '[data-test-group="y-labels"]',
|
||||
yLabels: '[data-test-group="y-labels"] text',
|
||||
};
|
||||
module('Integration | Component | clients/attribution', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.before(function () {
|
||||
this.timestampStub = sinon.replace(timestamp, 'now', sinon.fake.returns(new Date('2018-04-03T14:15:30')));
|
||||
});
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
const mockNow = this.timestampStub();
|
||||
this.mockNow = mockNow;
|
||||
this.startTimestamp = formatRFC3339(subMonths(mockNow, 6));
|
||||
this.selectedNamespace = null;
|
||||
this.namespaceAttribution = SERIALIZED_ACTIVITY_RESPONSE.by_namespace;
|
||||
this.authMountAttribution = SERIALIZED_ACTIVITY_RESPONSE.by_namespace.find(
|
||||
(ns) => ns.label === 'ns1'
|
||||
).mounts;
|
||||
});
|
||||
|
||||
test('it renders empty state with no data', async function (assert) {
|
||||
await render(hbs`
|
||||
<Clients::Attribution />
|
||||
`);
|
||||
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('No data found');
|
||||
assert.dom(CLIENTS_ATTRIBUTION.title).hasText('Namespace attribution', 'uses default noun');
|
||||
});
|
||||
|
||||
test('it updates language based on noun', async function (assert) {
|
||||
this.noun = '';
|
||||
await render(hbs`
|
||||
<Clients::Attribution
|
||||
@noun={{this.noun}}
|
||||
@attribution={{this.namespaceAttribution}}
|
||||
/>
|
||||
`);
|
||||
|
||||
// when noun is blank, uses default
|
||||
assert.dom(CLIENTS_ATTRIBUTION.title).hasText('Namespace attribution');
|
||||
assert
|
||||
.dom(CLIENTS_ATTRIBUTION.description)
|
||||
.hasText(
|
||||
'This data shows the top ten namespaces by total clients and can be used to understand where clients are originating. Namespaces are identified by path.'
|
||||
);
|
||||
assert
|
||||
.dom(CLIENTS_ATTRIBUTION.subtext)
|
||||
.hasText('This data shows the top ten namespaces by total clients for the date range selected.');
|
||||
|
||||
// when noun is mount
|
||||
this.set('noun', 'mount');
|
||||
assert.dom(CLIENTS_ATTRIBUTION.title).hasText('Mount attribution');
|
||||
assert
|
||||
.dom(CLIENTS_ATTRIBUTION.description)
|
||||
.hasText(
|
||||
'This data shows the top ten mounts by client count within this namespace, and can be used to understand where clients are originating. Mounts are organized by path.'
|
||||
);
|
||||
assert
|
||||
.dom(CLIENTS_ATTRIBUTION.subtext)
|
||||
.hasText(
|
||||
'The total clients used by the mounts for this date range. This number is useful for identifying overall usage volume.'
|
||||
);
|
||||
|
||||
// when noun is namespace
|
||||
this.set('noun', 'namespace');
|
||||
assert.dom(CLIENTS_ATTRIBUTION.title).hasText('Namespace attribution');
|
||||
assert
|
||||
.dom(CLIENTS_ATTRIBUTION.description)
|
||||
.hasText(
|
||||
'This data shows the top ten namespaces by total clients and can be used to understand where clients are originating. Namespaces are identified by path.'
|
||||
);
|
||||
assert
|
||||
.dom(CLIENTS_ATTRIBUTION.subtext)
|
||||
.hasText('This data shows the top ten namespaces by total clients for the date range selected.');
|
||||
});
|
||||
|
||||
test('it renders with data for namespaces', async function (assert) {
|
||||
await render(hbs`
|
||||
<Clients::Attribution
|
||||
@attribution={{this.namespaceAttribution}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GENERAL.emptyStateTitle).doesNotExist();
|
||||
assert.dom(CLIENTS_ATTRIBUTION.chart).exists();
|
||||
assert.dom(CLIENTS_ATTRIBUTION.topItem).includesText('namespace').includesText('ns1');
|
||||
assert.dom(CLIENTS_ATTRIBUTION.topItemCount).includesText('namespace').includesText('18,903');
|
||||
assert
|
||||
.dom(CLIENTS_ATTRIBUTION.yLabels)
|
||||
.exists({ count: 2 }, 'bars reflect number of namespaces in single month');
|
||||
assert.dom(CLIENTS_ATTRIBUTION.yLabel).hasText('ns1root');
|
||||
});
|
||||
|
||||
test('it renders with data for mounts', async function (assert) {
|
||||
await render(hbs`
|
||||
<Clients::Attribution
|
||||
@noun="mount"
|
||||
@attribution={{this.authMountAttribution}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GENERAL.emptyStateTitle).doesNotExist();
|
||||
assert.dom(CLIENTS_ATTRIBUTION.chart).exists();
|
||||
assert.dom(CLIENTS_ATTRIBUTION.topItem).includesText('mount').includesText('auth/authid/0');
|
||||
assert.dom(CLIENTS_ATTRIBUTION.topItemCount).includesText('mount').includesText('8,394');
|
||||
assert
|
||||
.dom(CLIENTS_ATTRIBUTION.yLabels)
|
||||
.exists({ count: 3 }, 'bars reflect number of mounts in single month');
|
||||
assert.dom(CLIENTS_ATTRIBUTION.yLabel).hasText('auth/authid/0pki-engine-0kvv2-engine-0');
|
||||
});
|
||||
|
||||
test('it shows secret syncs when flag is on', async function (assert) {
|
||||
this.isSecretsSyncActivated = true;
|
||||
await render(hbs`
|
||||
<Clients::Attribution
|
||||
@attribution={{this.namespaceAttribution}}
|
||||
@isSecretsSyncActivated={{true}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-group="secret_syncs"] rect').exists({ count: 2 });
|
||||
});
|
||||
|
||||
test('it hids secret syncs when flag is off or missing', async function (assert) {
|
||||
this.isSecretsSyncActivated = true;
|
||||
await render(hbs`
|
||||
<Clients::Attribution
|
||||
@attribution={{this.namespaceAttribution}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-group="secret_syncs"]').doesNotExist();
|
||||
});
|
||||
|
||||
test('it sorts and limits before rendering bars', async function (assert) {
|
||||
this.tooManyAttributions = Array(15)
|
||||
.fill(null)
|
||||
.map((_, idx) => {
|
||||
const attr = { label: `ns${idx}` };
|
||||
CLIENT_TYPES.forEach((type) => {
|
||||
attr[type] = 10 + idx;
|
||||
});
|
||||
return attr;
|
||||
});
|
||||
await render(hbs`
|
||||
<Clients::Attribution
|
||||
@attribution={{this.tooManyAttributions}}
|
||||
/>
|
||||
`);
|
||||
assert.dom(CLIENTS_ATTRIBUTION.yLabels).exists({ count: 10 }, 'only 10 bars are shown');
|
||||
assert.dom(CLIENTS_ATTRIBUTION.topItem).includesText('ns14');
|
||||
});
|
||||
});
|
||||
|
|
@ -5,13 +5,14 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render } from '@ember/test-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 { CHARTS, CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
module('Integration | Component | clients/page/overview', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
|
@ -48,71 +49,118 @@ module('Integration | Component | clients/page/overview', function (hooks) {
|
|||
this.mountPath = '';
|
||||
this.namespace = '';
|
||||
this.versionHistory = '';
|
||||
this.activity = await this.store.queryRecord('clients/activity', {});
|
||||
|
||||
// Fails on #ember-testing-container
|
||||
setRunOptions({
|
||||
rules: {
|
||||
'scrollable-region-focusable': { enabled: false },
|
||||
'aria-prohibited-attr': { enabled: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it hides attribution data when mount filter applied', async function (assert) {
|
||||
this.mountPath = '';
|
||||
test('it shows empty state message upon initial load', async function (assert) {
|
||||
await render(hbs`<Clients::Page::Overview @activity={{this.activity}}/>`);
|
||||
|
||||
assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown');
|
||||
|
||||
assert.dom(CLIENT_COUNT.attribution.card).exists('shows card for table state');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.attribution.card)
|
||||
.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.attribution.card)
|
||||
.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'
|
||||
);
|
||||
});
|
||||
|
||||
test('it shows table when month selection has 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'), '9/23');
|
||||
|
||||
assert.dom(CLIENT_COUNT.attribution.card).doesNotExist('does not show card when table has data');
|
||||
assert.dom(CLIENT_COUNT.attribution.table).exists('shows table');
|
||||
assert.dom(CLIENT_COUNT.attribution.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: 'ns1',
|
||||
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.attribution.card).doesNotExist('does not show card when table has data');
|
||||
assert.dom(CLIENT_COUNT.attribution.table).exists();
|
||||
assert.dom(CLIENT_COUNT.attribution.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="ns1" @mountPath={{this.mountPath}} />`
|
||||
hbs`<Clients::Page::Overview @activity={{this.activity}} @namespace={{this.namespace}} @mountPath={{this.mountPath}}/>`
|
||||
);
|
||||
|
||||
assert.dom(CHARTS.container('Vault client counts')).exists('shows running totals');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock('namespace')).exists();
|
||||
assert.dom(CLIENT_COUNT.attributionBlock('mount')).exists();
|
||||
|
||||
this.set('mountPath', 'auth/authid/0');
|
||||
assert.dom(CHARTS.container('Vault client counts')).exists('shows running totals');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock('namespace')).doesNotExist();
|
||||
assert.dom(CLIENT_COUNT.attributionBlock('mount')).doesNotExist();
|
||||
assert.dom(CLIENT_COUNT.attribution.card).doesNotExist('does not show card when table has data');
|
||||
assert
|
||||
.dom(CLIENT_COUNT.attribution.table)
|
||||
.doesNotExist('does not show table when a mount filter is applied');
|
||||
});
|
||||
|
||||
test('it hides attribution data when no data returned', async function (assert) {
|
||||
this.mountPath = '';
|
||||
this.activity = await this.store.queryRecord('clients/activity', {
|
||||
namespace: 'no-data',
|
||||
});
|
||||
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
|
||||
assert.dom(CLIENT_COUNT.usageStats('Total usage')).exists();
|
||||
assert.dom(CHARTS.container('Vault client counts')).doesNotExist('usage stats instead of running totals');
|
||||
assert.dom(CLIENT_COUNT.attributionBlock('namespace')).doesNotExist();
|
||||
assert.dom(CLIENT_COUNT.attributionBlock('mount')).doesNotExist();
|
||||
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(CLIENT_COUNT.attribution.row)
|
||||
.exists({ count: 3 }, 'Correct number of table rows render based on page size');
|
||||
assert.dom(CLIENT_COUNT.attribution.counts(0)).hasText('96', 'First page shows data');
|
||||
assert.dom(CLIENT_COUNT.attribution.pagination).exists('shows pagination');
|
||||
assert.dom(CLIENT_COUNT.attribution.paginationInfo).hasText('1–3 of 6', 'shows correct pagination info');
|
||||
|
||||
await click(GENERAL.pagination.next);
|
||||
|
||||
assert.dom(CLIENT_COUNT.attribution.counts(0)).hasText('53', 'Second page shows new data');
|
||||
assert.dom(CLIENT_COUNT.attribution.paginationInfo).hasText('4–6 of 6', 'shows correct pagination info');
|
||||
});
|
||||
|
||||
test('it shows the correct mount attributions', async function (assert) {
|
||||
this.nsService = this.owner.lookup('service:namespace');
|
||||
const rootActivity = await this.store.queryRecord('clients/activity', {});
|
||||
this.activity = rootActivity;
|
||||
test('it shows correct month options for billing period', async function (assert) {
|
||||
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
|
||||
// start at "root" namespace
|
||||
let expectedMounts = rootActivity.byNamespace.find((ns) => ns.label === 'root').mounts;
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-group="y-labels"] text`)
|
||||
.exists({ count: expectedMounts.length });
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-group="y-labels"]`)
|
||||
.includesText(expectedMounts[0].label);
|
||||
|
||||
// now pretend we're querying within a child namespace
|
||||
this.nsService.path = 'ns1';
|
||||
this.activity = await this.store.queryRecord('clients/activity', {
|
||||
namespace: 'ns1',
|
||||
});
|
||||
expectedMounts = rootActivity.byNamespace.find((ns) => ns.label === 'ns1').mounts;
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-group="y-labels"] text`)
|
||||
.exists({ count: expectedMounts.length });
|
||||
assert
|
||||
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-group="y-labels"]`)
|
||||
.includesText(expectedMounts[0].label);
|
||||
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.map((m) => m.month);
|
||||
|
||||
// '' represents default state of 'Select month'
|
||||
const expectedOptions = ['', ...expectedMonths];
|
||||
const actualOptions = findAll(`${GENERAL.selectByAttr('attribution-month')} option`).map(
|
||||
(option) => option.value
|
||||
);
|
||||
assert.deepEqual(actualOptions, expectedOptions, 'All <option> values match expected list');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
clients: 30,
|
||||
},
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -233,6 +234,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
clients: 30,
|
||||
entity_clients: 10,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: '',
|
||||
non_entity_clients: 20,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
|
|
@ -264,6 +266,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
clients: 2,
|
||||
entity_clients: 2,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: 'no mount path (pre-1.10 upgrade?)',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
|
|
@ -271,7 +274,8 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
acme_clients: 0,
|
||||
clients: 1,
|
||||
entity_clients: 1,
|
||||
label: 'auth/u/',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
|
|
@ -301,6 +305,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
clients: 2,
|
||||
entity_clients: 2,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
mount_type: 'no mount path (pre-1.10 upgrade?)',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
|
|
@ -308,7 +313,8 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
acme_clients: 0,
|
||||
clients: 1,
|
||||
entity_clients: 1,
|
||||
label: 'auth/u/',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
|
|
@ -502,7 +508,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
when: 'no namespace filter passed',
|
||||
result: 'it returns empty counts',
|
||||
ns: '',
|
||||
mount: 'auth/authid/0',
|
||||
mount: 'auth/userpass-0',
|
||||
expected: emptyCounts,
|
||||
},
|
||||
{
|
||||
|
|
@ -516,16 +522,17 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
when: 'no matching ns/mount exists',
|
||||
result: 'it returns empty counts',
|
||||
ns: 'ns1',
|
||||
mount: 'auth/authid/1',
|
||||
mount: 'auth/userpass-1',
|
||||
expected: emptyCounts,
|
||||
},
|
||||
{
|
||||
when: 'mount and label have extra slashes',
|
||||
result: 'it returns the data sanitized',
|
||||
ns: 'ns1/',
|
||||
mount: 'auth/authid/0/',
|
||||
mount: 'auth/userpass-0',
|
||||
expected: {
|
||||
label: 'auth/authid/0',
|
||||
label: 'auth/userpass-0',
|
||||
mount_type: 'userpass',
|
||||
acme_clients: 0,
|
||||
clients: 8394,
|
||||
entity_clients: 4256,
|
||||
|
|
@ -540,6 +547,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||
mount: 'kvv2-engine-0',
|
||||
expected: {
|
||||
label: 'kvv2-engine-0',
|
||||
mount_type: 'kv',
|
||||
acme_clients: 0,
|
||||
clients: 4290,
|
||||
entity_clients: 0,
|
||||
|
|
|
|||
Loading…
Reference in a new issue