UI: Add donut chart to client counts overview (#9040) (#9367)

* =replace client stats with donut chart viz

* update chart styling

* add a changelog entry

* test updates

* remove css changes

Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
This commit is contained in:
Vault Automation 2025-09-17 11:12:06 -04:00 committed by GitHub
parent 67b3e53325
commit 64d421da69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 54 additions and 37 deletions

4
changelog/_9040.txt Normal file
View file

@ -0,0 +1,4 @@
```release-note:improvement
**ui/activity**: Updates running total stats to be displayed via a donut chart.
```

View file

@ -11,24 +11,8 @@
>
<:dataLeft>
<StatText
@label="New client total and type distribution"
@subText="The number of new clients which interacted with Vault during the selected period."
@value={{@runningTotals.clients}}
@size="m"
@tooltipText="This number is the total for the queried date range. The chart displays a monthly breakdown of total new clients per month."
/>
<div class="has-top-padding-l is-flex-row gap-16">
<StatText @label="Entity" @value={{@runningTotals.entity_clients}} @size="m" />
<StatText @label="Non-entity" @value={{@runningTotals.non_entity_clients}} @size="m" />
</div>
<div class="has-top-padding-m is-flex-row gap-16">
<StatText @label="ACME" @value={{@runningTotals.acme_clients}} @size="m" />
{{#if this.flags.secretsSyncIsActivated}}
<StatText @label="Secret sync" @value={{@runningTotals.secret_syncs}} @size="m" />
{{/if}}
</div>
<Hds::Text::Body @tag="p">Client count and type distribution</Hds::Text::Body>
<VaultReporting::DonutChart @data={{this.donutChartData}} @title="Total Clients" class="donut-chart" />
</:dataLeft>
<:dataRight>

View file

@ -33,6 +33,17 @@ export default class RunningTotal extends Component<Args> {
}));
}
get donutChartData() {
return [
{ value: this.args.runningTotals.entity_clients, label: 'Entity clients' },
{ value: this.args.runningTotals.non_entity_clients, label: 'Non-entity clients' },
{ value: this.args.runningTotals.acme_clients, label: 'ACME clients' },
...(this.flags.secretsSyncIsActivated
? [{ value: this.args.runningTotals.secret_syncs, label: 'Secret sync clients' }]
: []),
];
}
get chartLegend() {
if (this.showStacked) {
return [

View file

@ -29,10 +29,23 @@
gap: 1rem;
.item-left {
flex: 1 1 33.3333%;
flex: 1 1 40%;
min-width: 0;
}
.item-right {
flex: 2 1 66.6666%;
flex: 2 1 60%;
min-width: 0;
}
// Stack vertically on smaller screens
@media (max-width: 1024px) {
flex-direction: column;
.item-left,
.item-right {
flex: 1 1 100%;
width: 100%;
}
}
}
}

View file

@ -44,8 +44,9 @@ module('Acceptance | clients | overview', function (hooks) {
await login();
await visit('/vault/clients/counts/overview');
assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).doesNotExist();
assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists('other stats are still visible');
assert.dom(CLIENT_COUNT.statLegendValue('Secret sync clients')).doesNotExist();
assert.dom(CLIENT_COUNT.statLegendValue('Entity clients')).exists('other stats are still visible');
await click(GENERAL.inputByAttr('toggle view'));
assert.dom(CHARTS.legend).hasText('Entity clients Non-entity clients ACME clients');
});
@ -302,7 +303,9 @@ module('Acceptance | clients | overview', function (hooks) {
test('it should show secrets sync stats when the feature is activated', async function (assert) {
await login();
await visit('/vault/clients/counts/overview');
assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists('shows secret sync data on overview');
assert
.dom(CLIENT_COUNT.statLegendValue('Secret sync clients'))
.exists('shows secret sync data on overview');
await click(GENERAL.inputByAttr('toggle view'));
assert
.dom(CHARTS.legend)
@ -321,11 +324,10 @@ module('Acceptance | clients | overview', function (hooks) {
await login();
await visit('/vault/clients/counts/overview');
assert
.dom(CLIENT_COUNT.statTextValue('Secret sync'))
.dom(CLIENT_COUNT.statLegendValue('Secret sync clients'))
.doesNotExist('stat is hidden because feature is not activated');
assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists('other stats are still visible');
assert.dom(CLIENT_COUNT.statLegendValue('Entity clients')).exists('other stats are still visible');
await click(GENERAL.inputByAttr('toggle view'));
assert
.dom(CHARTS.legend)
@ -341,7 +343,7 @@ module('Acceptance | clients | overview', function (hooks) {
await login();
await visit('/vault/clients/counts/overview');
assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists();
assert.dom(CLIENT_COUNT.statLegendValue('Secret sync clients')).exists();
await click(GENERAL.inputByAttr('toggle view'));
assert
.dom(CHARTS.legend)

View file

@ -25,6 +25,8 @@ export const CLIENT_COUNT = {
defaultRangeAlert: '[data-test-range-default-alert]',
validation: '[data-test-date-range-validation]',
},
statLegendValue: (label: string) =>
label ? `[data-test-vault-reporting-legend-item="${label}"]` : '[data-test-vault-reporting-legend-item',
statText: (label: string) => `[data-test-stat-text="${label}"]`,
statTextValue: (label: string) =>
label ? `[data-test-stat-text="${label}"] .stat-value` : '[data-test-stat-text]',

View file

@ -60,17 +60,16 @@ module('Integration | Component | clients/running-total', function (hooks) {
assert.strictEqual(color, expectedColor, `actual color: ${color}, expected color: ${expectedColor}`);
const expectedValues = {
'New client total and type distribution': formatNumber([this.activity.total.clients]),
Entity: formatNumber([this.activity.total.entity_clients]),
'Non-entity': formatNumber([this.activity.total.non_entity_clients]),
ACME: formatNumber([this.activity.total.acme_clients]),
'Secret sync': formatNumber([this.activity.total.secret_syncs]),
'Entity clients': formatNumber([this.activity.total.entity_clients]),
'Non-entity clients': formatNumber([this.activity.total.non_entity_clients]),
'ACME clients': formatNumber([this.activity.total.acme_clients]),
'Secret sync clients': formatNumber([this.activity.total.secret_syncs]),
};
for (const label in expectedValues) {
assert
.dom(CLIENT_COUNT.statTextValue(label))
.dom(CLIENT_COUNT.statLegendValue(label))
.hasText(
`${expectedValues[label]}`,
`${expectedValues[label]} ${label}`,
`stat label: ${label} renders correct total: ${expectedValues[label]}`
);
}
@ -162,9 +161,11 @@ module('Integration | Component | clients/running-total', function (hooks) {
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
.exists('running total component renders');
assert.dom(CHARTS.chart('Client usage by month')).exists('bar chart renders');
assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists();
assert.dom(CLIENT_COUNT.statTextValue('Non-entity')).exists();
assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).doesNotExist('does not render secret syncs');
assert.dom(CLIENT_COUNT.statLegendValue('Entity clients')).exists();
assert.dom(CLIENT_COUNT.statLegendValue('Non-entity clients')).exists();
assert
.dom(CLIENT_COUNT.statLegendValue('Secret sync clients'))
.doesNotExist('does not render secret syncs');
// check toggle view
await click(GENERAL.inputByAttr('toggle view'));