UI: Refactor and resuse UsageStats display template across tabs (#26562)

* refactor usage stats to use Hds::Card

* use usage stats in other tabs

* use flex row and remove verbose grid css classes

* cleanup selectors, add arg to usage stat selector

* update usage stat test

* update token tab usage stat title

* add test coverage for description and title
This commit is contained in:
claire bontempo 2024-04-19 15:14:46 -07:00 committed by GitHub
parent d44ec076b8
commit 17552aab7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 167 additions and 254 deletions

View File

@ -88,11 +88,7 @@
</Clients::ChartContainer>
{{/if}}
{{else}}
<div class="chart-wrapper" data-test-usage-stats>
<div class="chart-header has-bottom-margin-m">
<h2 class="chart-title">{{this.title}}</h2>
<p class="chart-description has-bottom-padding-m">{{this.description}}</p>
</div>
<StatText @label="Total ACME clients" @value={{this.totalUsageCounts.acme_clients}} @size="l" />
</div>
<Clients::UsageStats @title={{this.title}} @description={{this.description}}>
<StatText @label="Total ACME clients" @value={{this.totalUsageCounts.acme_clients}} @size="l" class="column" />
</Clients::UsageStats>
{{/if}}

View File

@ -54,13 +54,9 @@
</:emptyState>
</Clients::ChartContainer>
{{else}}
<div class="chart-wrapper" data-test-usage-stats>
<div class="chart-header has-bottom-margin-m">
<h2 class="chart-title">{{this.title}}</h2>
<p class="chart-description has-bottom-padding-m">{{this.description}}</p>
</div>
<StatText @label="Total sync clients" @value={{this.totalUsageCounts.secret_syncs}} @size="l" />
</div>
<Clients::UsageStats @title={{this.title}} @description={{this.description}}>
<StatText @label="Total sync clients" @value={{this.totalUsageCounts.secret_syncs}} @size="l" class="column" />
</Clients::UsageStats>
{{/if}}
{{else}}
<EmptyState

View File

@ -82,18 +82,36 @@
</:emptyState>
</Clients::ChartContainer>
{{else}}
{{! UsageStats render when viewing a single, historical month AND activity data predates new client breakdown (< v1.10.0)
or viewing the current month filtered down to auth method }}
{{! Renders when viewing a single month or 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'}}.
@title="Entity/Non-entity usage"
@description="Client counts for this
{{if @mountPath 'mount' 'namespace and all its children'}}.
{{if
this.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."
}}"
@showSecretSyncs={{false}}
@hideAcmeClients={{true}}
@totalUsageCounts={{this.tokenUsageCounts}}
/>
>
<StatText
class="column"
@label="Total clients"
@value={{this.tokenUsageCounts.clients}}
@size="l"
@subText="The number of clients which interacted with Vault during this month. This is Vaults primary billing metric."
/>
<StatText
class="column"
@label="Entity"
@value={{this.tokenUsageCounts.entity_clients}}
@size="l"
@subText="Representations of a particular user, client, or application that created a token via login."
/>
<StatText
class="column"
@label="Non-entity"
@value={{this.tokenUsageCounts.non_entity_clients}}
@size="l"
@subText="Clients created with a shared set of permissions, but not associated with an entity."
/>
</Clients::UsageStats>
{{/if}}

View File

@ -43,57 +43,42 @@
{{else}}
{{#let (get @byMonthActivityData "0") as |singleMonthData|}}
{{#if (and @isHistoricalMonth singleMonthData.new_clients.clients)}}
<div class="chart-wrapper single-month-grid" data-test-running-total="single-month-stats">
<div class="chart-header has-bottom-margin-sm">
<h2 class="chart-title">Vault client counts</h2>
<p class="chart-description">
The total billable clients in the specified date range. This includes entity, non-entity, and Secret sync
clients. The total client count number is an important consideration for Vault billing.
</p>
</div>
<div class="single-month-stats" data-test-new>
<div class="single-month-section-title">
<StatText
@label="New clients"
@subText="This is the number of clients which were created in Vault for the first time in the selected month."
@value={{singleMonthData.new_clients.clients}}
@size="l"
/>
</div>
<StatText @label="Entity" @value={{singleMonthData.new_clients.entity_clients}} @size="m" class="column-start-1" />
<Clients::UsageStats @title="Vault client counts" @description={{this.chartContainerText}}>
<div class="column" data-test-new>
<StatText
@label="Non-entity"
@value={{singleMonthData.new_clients.non_entity_clients}}
@size="m"
class="column-start-2"
@label="New clients"
@subText="This is the number of clients which were created in Vault for the first time in the selected month."
@value={{singleMonthData.new_clients.clients}}
@size="l"
class="has-bottom-margin-l"
/>
<StatText @label="ACME" @value={{singleMonthData.new_clients.acme_clients}} @size="m" class="column-start-3" />
{{#if @isSecretsSyncActivated}}
<StatText
@label="Secret sync"
@value={{singleMonthData.new_clients.secret_syncs}}
@size="m"
class="column-start-4"
/>
{{/if}}
</div>
<div class="single-month-stats" data-test-total>
<div class="single-month-section-title">
<StatText
@label="Total monthly clients"
@subText="This is the number of total clients which used Vault for the given month, both new and previous."
@value={{singleMonthData.clients}}
@size="l"
/>
<div class="flex row-wrap gap-36">
<StatText @label="Entity" @value={{singleMonthData.new_clients.entity_clients}} @size="m" />
<StatText @label="Non-entity" @value={{singleMonthData.new_clients.non_entity_clients}} @size="m" />
<StatText @label="ACME" @value={{singleMonthData.new_clients.acme_clients}} @size="m" />
{{#if @isSecretsSyncActivated}}
<StatText @label="Secret sync" @value={{singleMonthData.new_clients.secret_syncs}} @size="m" />
{{/if}}
</div>
<StatText @label="Entity" @value={{singleMonthData.entity_clients}} @size="m" class="column-start-1" />
<StatText @label="Non-entity" @value={{singleMonthData.non_entity_clients}} @size="m" class="column-start-2" />
<StatText @label="ACME" @value={{singleMonthData.acme_clients}} @size="m" class="column-start-3" />
{{#if @isSecretsSyncActivated}}
<StatText @label="Secret sync" @value={{singleMonthData.secret_syncs}} @size="m" class="column-start-4" />
{{/if}}
</div>
</div>
<div class="column" data-test-total>
<StatText
@label="Total monthly clients"
@subText="This is the number of total clients which used Vault for the given month, both new and previous."
@value={{singleMonthData.clients}}
@size="l"
class="has-bottom-margin-l"
/>
<div class="flex row-wrap gap-36">
<StatText @label="Entity" @value={{singleMonthData.entity_clients}} @size="m" />
<StatText @label="Non-entity" @value={{singleMonthData.non_entity_clients}} @size="m" />
<StatText @label="ACME" @value={{singleMonthData.acme_clients}} @size="m" />
{{#if @isSecretsSyncActivated}}
<StatText @label="Secret sync" @value={{singleMonthData.secret_syncs}} @size="m" class="column-start-4" />
{{/if}}
</div>
</div>
</Clients::UsageStats>
{{else}}
{{! Renders when viewing the current month or for activity log data that predates the monthly breakdown added in 1.11 }}
<Clients::UsageStats
@ -102,9 +87,45 @@
@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."
}}"
@showSecretSyncs={{@isSecretsSyncActivated}}
@totalUsageCounts={{@runningTotals}}
/>
>
<StatText
class="column"
@label="Total clients"
@value={{@runningTotals.clients}}
@size="l"
@subText="The number of clients which interacted with Vault during this month. This is Vaults primary billing metric."
/>
<StatText
class="column"
@label="Entity"
@value={{@runningTotals.entity_clients}}
@size="l"
@subText="Representations of a particular user, client, or application that created a token via login."
/>
<StatText
class="column"
@label="Non-entity"
@value={{@runningTotals.non_entity_clients}}
@size="l"
@subText="Clients created with a shared set of permissions, but not associated with an entity."
/>
<StatText
class="column"
@label="ACME"
@value={{@runningTotals.acme_clients}}
@size="l"
@subText="Each ACME request counts as one client."
/>
{{#if @isSecretsSyncActivated}}
<StatText
class="column"
@label="Secret sync"
@value={{@runningTotals.secret_syncs}}
@size="l"
@subText="A secret with a configured sync destination qualifies as a unique and active client."
/>
{{/if}}
</Clients::UsageStats>
{{/if}}
{{/let}}
{{/if}}

View File

@ -3,68 +3,34 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<div class="chart-wrapper" data-test-usage-stats>
<div class="chart-header has-header-link has-bottom-margin-m">
<div class="header-left">
<h2 class="chart-title">{{@title}}</h2>
<p class="chart-description"> {{or @description "These totals are within this namespace and all its children."}}</p>
</div>
<div class="header-right">
<DocLink @path="/vault/tutorials/monitoring/usage-metrics">
Learn more
</DocLink>
</div>
{{! Display only template that renders yielded components in columns, beneath a title and description }}
<Hds::Card::Container
@level="base"
@hasBorder={{true}}
data-test-usage-stats={{@title}}
class="has-padding-m has-bottom-margin-m"
>
<div class="flex row-wrap space-between">
<Hds::Text::Display @tag="h3" @weight="semibold" @size="400">
{{@title}}
</Hds::Text::Display>
<Hds::Link::Standalone
@href={{doc-link "/vault/tutorials/monitoring/usage-metrics"}}
@text="Usage metrics tutorial"
@icon="learn-link"
@iconPosition="trailing"
class="has-bottom-margin-s"
/>
</div>
{{#if @description}}
<Hds::Text::Body @tag="p">
{{@description}}
</Hds::Text::Body>
{{/if}}
<hr class="has-background-gray-100 has-bottom-margin-m" />
<div class="columns">
<div class="column">
<StatText
class="column"
@label="Total clients"
@value={{@totalUsageCounts.clients}}
@size="l"
@subText="The number of clients which interacted with Vault during this month. This is Vaults primary billing metric."
/>
</div>
<div class="column">
<StatText
class="column"
@label="Entity"
@value={{@totalUsageCounts.entity_clients}}
@size="l"
@subText="Representations of a particular user, client, or application that created a token via login."
/>
</div>
<div class="column">
<StatText
class="column"
@label="Non-entity"
@value={{@totalUsageCounts.non_entity_clients}}
@size="l"
@subText="Clients created with a shared set of permissions, but not associated with an entity."
/>
</div>
{{#unless @hideAcmeClients}}
<div class="column">
<StatText
class="column"
@label="ACME"
@value={{@totalUsageCounts.acme_clients}}
@size="l"
@subText="Each ACME request counts as one client."
/>
</div>
{{/unless}}
{{#if @showSecretSyncs}}
<div class="column">
<StatText
class="column"
@label="Secret sync"
@value={{@totalUsageCounts.secret_syncs}}
@size="l"
@subText="A secret with a configured sync destination qualifies as a unique and active client."
/>
</div>
{{/if}}
{{yield}}
</div>
</div>
</Hds::Card::Container>

View File

@ -6,9 +6,9 @@
<Dashboard::VaultVersionTitle @version={{@version}} />
<div class="has-bottom-margin-xl">
<div class="is-flex-row has-gap-l">
<div class="is-flex-row gap-24">
{{#if (and @version.isEnterprise (or @license @isRootNamespace))}}
<div class="is-flex-column is-flex-1 has-gap-l">
<div class="is-flex-column is-flex-1 gap-24">
{{#if @license}}
<Dashboard::ClientCountCard @license={{@license}} />
{{/if}}
@ -25,7 +25,7 @@
<Dashboard::SecretsEnginesCard @secretsEngines={{@secretsEngines}} />
</div>
<div class="is-flex-column is-flex-1 has-gap-l">
<div class="is-flex-column is-flex-1 gap-24">
<Dashboard::QuickActionsCard @secretsEngines={{@secretsEngines}} />
{{#if @vaultConfiguration}}
<Dashboard::VaultConfigurationDetailsCard @vaultConfiguration={{@vaultConfiguration}} />
@ -37,14 +37,14 @@
</div>
{{else}}
<div class="is-flex-column is-flex-1 has-gap-l">
<div class="is-flex-column is-flex-1 gap-24">
<Dashboard::SecretsEnginesCard @secretsEngines={{@secretsEngines}} />
<div>
<Dashboard::LearnMoreCard @isEnterprise={{@version.isEnterprise}} />
<Dashboard::SurveyLinkText />
</div>
</div>
<div class="is-flex-column is-flex-1 has-gap-l">
<div class="is-flex-column is-flex-1 gap-24">
<Dashboard::QuickActionsCard @secretsEngines={{@secretsEngines}} />
{{#if @vaultConfiguration}}
<Dashboard::VaultConfigurationDetailsCard @vaultConfiguration={{@vaultConfiguration}} />

View File

@ -23,7 +23,7 @@
<:body as |B|>
{{#each this.firstFiveSecretsEngines as |backend|}}
<B.Tr data-test-secrets-engines-row={{backend.id}}>
<B.Td class="is-flex-between is-flex-center has-gap-m">
<B.Td class="is-flex-between is-flex-center gap-16">
<div>
<div class="is-flex-center">
{{#if backend.icon}}

View File

@ -11,36 +11,6 @@
}
// GRID LAYOUT //
.single-month-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2em;
width: 100%;
}
.single-month-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1em;
width: 100%;
margin-bottom: 1rem;
.column-start-1 {
grid-column-start: 1;
}
.column-start-2 {
grid-column-start: 2;
}
.column-start-3 {
grid-column-start: 3;
}
.column-start-4 {
grid-column-start: 4;
}
}
.single-month-section-title {
grid-column-start: 1;
grid-column-end: span col3-end;
}
.single-chart-grid {
display: grid;
grid-template-columns: 1fr 0.3fr 3.7fr;

View File

@ -46,14 +46,18 @@
flex-direction: row;
}
.has-gap-m {
.gap-16 {
gap: $spacing-16;
}
.has-gap-l {
.gap-24 {
gap: $spacing-24;
}
.gap-36 {
gap: $spacing-36;
}
// Alignment of the items
.is-flex-v-centered {
display: flex;

View File

@ -39,7 +39,7 @@
{{/if}}
</div>
<div class="is-flex-row has-gap-l has-bottom-margin-m">
<div class="is-flex-row gap-24 has-bottom-margin-m">
<div>
<img src={{img-path "~/sync-landing-1.png"}} alt="Secrets sync destinations diagram" aria-describedby="sync-step-1" />
<p id="sync-step-1" class="has-top-margin-m">

View File

@ -74,7 +74,7 @@ module('Acceptance | clients | overview', function (hooks) {
await click('[data-test-previous-year]');
await click(`[data-test-calendar-month=${ARRAY_OF_MONTHS[LICENSE_START.getMonth()]}]`);
assert
.dom(CLIENT_COUNT.runningTotalMonthStats)
.dom(CLIENT_COUNT.usageStats('Vault client counts'))
.doesNotExist('running total single month stat boxes do not show');
assert
.dom(CLIENT_COUNT.chartContainer('Vault client counts'))
@ -118,7 +118,9 @@ module('Acceptance | clients | overview', function (hooks) {
assert.dom('[data-test-display-year]').hasText('2024');
await click('[data-test-previous-year]');
await click('[data-test-calendar-month="September"]');
assert.dom(CLIENT_COUNT.runningTotalMonthStats).exists('running total single month stat boxes show');
assert
.dom(CLIENT_COUNT.usageStats('Vault client counts'))
.exists('running total single month usage stats show');
assert
.dom(CLIENT_COUNT.chartContainer('Vault client counts'))
.doesNotExist('running total month over month charts do not show');

View File

@ -39,7 +39,7 @@ export const CLIENT_COUNT = {
plotPoint: '[data-test-line-chart="plot-point"]',
},
},
usageStats: '[data-test-usage-stats]',
usageStats: (title: string) => `[data-test-usage-stats="${title}"]`,
dateDisplay: '[data-test-date-display]',
attributionBlock: '[data-test-clients-attribution]',
filterBar: '[data-test-clients-filter-bar]',
@ -63,8 +63,6 @@ export const CLIENT_COUNT = {
nextYear: '[data-test-next-year]',
calendarMonth: (month: string) => `[data-test-calendar-month="${month}"]`,
},
runningTotalMonthStats: '[data-test-running-total="single-month-stats"]',
runningTotalMonthlyCharts: '[data-test-running-total="monthly-charts"]',
selectedAuthMount: 'div#mounts-search-select [data-test-selected-option] div',
selectedNs: 'div#namespace-search-select [data-test-selected-option] div',
upgradeWarning: '[data-test-clients-upgrade-warning]',

View File

@ -100,9 +100,9 @@ module('Integration | Component | clients | Clients::Page::Acme', function (hook
assert.dom(statText('Average ACME clients per month')).doesNotExist();
assert.dom(statText('Average new ACME clients per month')).doesNotExist();
assert
.dom(usageStats)
.dom(usageStats('ACME usage'))
.hasText(
`ACME usage This data can be used to understand how many ACME clients have been used for the queried month. Each ACME request is counted as one client. Total ACME clients ${expectedTotal}`,
`ACME usage Usage metrics tutorial This data can be used to understand how many ACME clients have been used for the queried month. Each ACME request is counted as one client. Total ACME clients ${expectedTotal}`,
'it renders usage stats with single month copy'
);
});
@ -127,7 +127,7 @@ module('Integration | Component | clients | Clients::Page::Acme', function (hook
assert.dom(statText('Total ACME clients')).doesNotExist();
assert.dom(statText('Average ACME clients per month')).doesNotExist();
assert.dom(statText('Average new ACME clients per month')).doesNotExist();
assert.dom(usageStats).doesNotExist();
assert.dom(usageStats('ACME usage')).doesNotExist();
});
test('it should render empty state when ACME data does not exist for a single month', async function (assert) {

View File

@ -119,9 +119,9 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook
assert.dom(charts.chart('Secrets sync usage')).doesNotExist('vertical bar chart does not render');
assert
.dom(usageStats)
.dom(usageStats('Secrets sync usage'))
.hasText(
`Secrets sync usage This data can be used to understand how many secrets sync clients have been used for this date range. Each Vault secret that is synced to at least one destination counts as one Vault client. Total sync clients ${total}`,
`Secrets sync usage Usage metrics tutorial This data can be used to understand how many secrets sync clients have been used for this date range. Each Vault secret that is synced to at least one destination counts as one Vault client. Total sync clients ${total}`,
'renders sync stats instead of chart'
);
assert.dom(statText('Average total clients per month')).doesNotExist('total sync counts does not exist');

View File

@ -11,80 +11,22 @@ import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | clients/usage-stats', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.showSecretSyncs = false;
this.counts = {};
test('it renders', async function (assert) {
await render(
hbs`
<Clients::UsageStats @title="My stats" @description="a very important description">
yielded content!
</Clients::UsageStats>`
);
this.renderComponent = async () =>
await render(
hbs`<Clients::UsageStats @title="My stats" @totalUsageCounts={{this.counts}} @showSecretSyncs={{this.showSecretSyncs}} />`
);
});
test('it renders defaults', async function (assert) {
await this.renderComponent();
assert.dom('[data-test-stat-text]').exists({ count: 4 }, 'Renders 4 Stat texts even with no data passed');
assert.dom('[data-test-stat-text="Total clients"]').exists('Total clients exists');
assert.dom('[data-test-stat-text="Total clients"] .stat-value').hasText('-', 'renders dash when no data');
assert.dom('[data-test-stat-text="Entity"]').exists('Entity exists');
assert.dom('[data-test-stat-text="Entity"] .stat-value').hasText('-', 'renders dash when no data');
assert.dom('[data-test-stat-text="Non-entity"]').exists('Non entity clients exists');
assert.dom('[data-test-stat-text="Non-entity"] .stat-value').hasText('-', 'renders dash when no data');
assert.dom('[data-test-usage-stats="My stats"]').exists();
assert.dom('h3').hasText('My stats', 'title renders in h3 tag');
assert.dom('p').hasText('a very important description', 'description renders in p tag');
assert
.dom('[data-test-usage-stats="My stats"]')
.hasTextContaining('yielded content!', 'it renders yielded content');
assert
.dom('a')
.hasAttribute('href', 'https://developer.hashicorp.com/vault/tutorials/monitoring/usage-metrics');
});
test('it renders with token data', async function (assert) {
this.counts = {
clients: 17,
entity_clients: 7,
non_entity_clients: 10,
};
await this.renderComponent();
assert.dom('[data-test-stat-text]').exists({ count: 4 }, 'Renders 4 Stat texts');
assert
.dom('[data-test-stat-text="Total clients"] .stat-value')
.hasText('17', 'Total clients shows passed value');
assert
.dom('[data-test-stat-text="Entity"] .stat-value')
.hasText('7', 'entity clients shows passed value');
assert
.dom('[data-test-stat-text="Non-entity"] .stat-value')
.hasText('10', 'non entity clients shows passed value');
});
module('it renders with full totals data', function (hooks) {
hooks.beforeEach(function () {
this.counts = {
clients: 22,
entity_clients: 7,
non_entity_clients: 10,
secret_syncs: 5,
};
});
test('with secrets sync activated', async function (assert) {
this.showSecretSyncs = true;
await this.renderComponent();
assert.dom('[data-test-stat-text]').exists({ count: 5 }, 'Renders 5 Stat texts');
assert
.dom('[data-test-stat-text="Secret sync"] .stat-value')
.hasText('5', 'secrets sync clients shows passed value');
});
test('with secrets sync NOT activated', async function (assert) {
this.showSecretSyncs = false;
await this.renderComponent();
assert.dom('[data-test-stat-text]').exists({ count: 4 }, 'Renders 4 Stat texts');
assert.dom('[data-test-stat-text="Secret sync"] .stat-value').doesNotExist();
});
});
});