Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secrets Sync Client Count Updates #24752

Merged
merged 25 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f577781
Client Count Routing Updates (#24733)
zofskeez Jan 9, 2024
9812159
Merge branch 'main' into ui/VAULT-23095/secrets-sync-client-counts
hellobontempo Jan 12, 2024
cbd4c09
UI: Adds secret_syncs to mirage /activity endpoint (#24846)
hellobontempo Jan 16, 2024
c1b5efc
UI: Set up client charts for incoming sync data (#24852)
hellobontempo Jan 18, 2024
a7cdc8f
UI: Secret sync bar chart (#24926)
hashishaw Jan 18, 2024
8697a1f
Clients Counts Parent Route (#24899)
zofskeez Jan 18, 2024
91f2706
Merge branch 'main' into ui/VAULT-23095/secrets-sync-client-counts
zofskeez Jan 18, 2024
9e9e0f5
UI: convert line-chart to lineal (#24961)
hashishaw Jan 19, 2024
60e46b3
Client Counts Overview (#24969)
zofskeez Jan 23, 2024
73cadce
Secrets sync UI: add sync page component (#24982)
hellobontempo Jan 24, 2024
8054d58
update selectors
hellobontempo Jan 24, 2024
7efb7e5
add sync page tests
hellobontempo Jan 24, 2024
91e1255
Secrets Sync UI: Add secrets syncs to csv export (#25056)
hellobontempo Jan 25, 2024
98f1c87
Clients Counts Token Route (#25019)
zofskeez Jan 25, 2024
6c37305
Clients Usage Stats/Running Total Updates (#25094)
zofskeez Jan 26, 2024
e596458
Secrets sync UI: cleanup and consolidation of components (#25090)
hellobontempo Jan 27, 2024
6482985
Merge branch 'main' into ui/VAULT-23095/secrets-sync-client-counts
zofskeez Jan 30, 2024
8b33045
updates tests
zofskeez Jan 31, 2024
8fe6e2d
adds comment for fetching license to get start date for billing
zofskeez Jan 31, 2024
acbe97e
cleans up unused client counts files (#25157)
zofskeez Jan 31, 2024
069daf4
adds changelog
zofskeez Jan 31, 2024
8d8662d
fix assertion copy
hellobontempo Jan 31, 2024
f93294a
adds changelog description
zofskeez Jan 31, 2024
42e4a24
Merge branch 'ui/VAULT-23095/secrets-sync-client-counts' of github.co…
zofskeez Jan 31, 2024
236df5f
updates enterprise sidebar nav test
zofskeez Feb 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added changelog/24752.txt
Empty file.
8 changes: 3 additions & 5 deletions ui/app/adapters/clients/activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@
*/

import ApplicationAdapter from '../application';
import { getUnixTime } from 'date-fns';
import { formatDateObject } from 'core/utils/client-count-utils';

export default class ActivityAdapter extends ApplicationAdapter {
// javascript localizes new Date() objects but all activity log data is stored in UTC
// create date object from user's input using Date.UTC() then send to backend as unix
// time params from the backend are formatted as a zulu timestamp
formatQueryParams(queryParams) {
let { start_time, end_time } = queryParams;
start_time = start_time.timestamp || getUnixTime(Date.UTC(start_time.year, start_time.monthIdx, 1));
// day=0 for Date.UTC() returns the last day of the month before
// increase monthIdx by one to get last day of queried month
end_time = end_time.timestamp || getUnixTime(Date.UTC(end_time.year, end_time.monthIdx + 1, 0));
start_time = start_time.timestamp || formatDateObject(start_time);
end_time = end_time.timestamp || formatDateObject(end_time, true);
return { start_time, end_time };
}

Expand Down
2 changes: 1 addition & 1 deletion ui/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export default class App extends Application {
services: ['flash-messages', 'router', 'store', 'version'],
externalRoutes: {
kvSecretDetails: 'vault.cluster.secrets.backend.kv.secret.details',
clientCountDashboard: 'vault.cluster.clients.dashboard',
clientCountOverview: 'vault.cluster.clients',
},
},
},
Expand Down
198 changes: 198 additions & 0 deletions ui/app/components/clients/activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

// base component for counts child routes that can be extended as needed
// contains getters that filter and extract data from activity model for use in charts

import Component from '@glimmer/component';
import { isAfter, isBefore, isSameMonth, fromUnixTime } from 'date-fns';
import { parseAPITimestamp } from 'core/utils/date-formatters';
import { calculateAverage } from 'vault/utils/chart-helpers';

import type ClientsActivityModel from 'vault/models/clients/activity';
import type {
ClientActivityNewClients,
ClientActivityMonthly,
ClientActivityResourceByKey,
} from 'vault/models/clients/activity';
import type ClientsVersionHistoryModel from 'vault/models/clients/version-history';

interface Args {
activity: ClientsActivityModel;
versionHistory: ClientsVersionHistoryModel[];
startTimestamp: number;
endTimestamp: number;
namespace: string;
mountPath: string;
}

export default class ClientsActivityComponent extends Component<Args> {
average = (
data:
| ClientActivityMonthly[]
| (ClientActivityResourceByKey | undefined)[]
| (ClientActivityNewClients | undefined)[]
| undefined,
key: string
) => {
return calculateAverage(data, key);
};

get startTimeISO() {
return fromUnixTime(this.args.startTimestamp).toISOString();
}

get endTimeISO() {
return fromUnixTime(this.args.endTimestamp).toISOString();
}

get byMonthActivityData() {
const { activity, namespace } = this.args;
return namespace ? this.filteredActivityByMonth : activity.byMonth;
}

get byMonthNewClients() {
return this.byMonthActivityData ? this.byMonthActivityData?.map((m) => m?.new_clients) : [];
}

get filteredActivityByMonth() {
const { namespace, mountPath, activity } = this.args;
if (!namespace && !mountPath) {
return activity.byMonth;
}
const namespaceData = activity.byMonth
.map((m) => m.namespaces_by_key[namespace as keyof typeof m.namespaces_by_key])
.filter((d) => d !== undefined);

if (!mountPath) {
return namespaceData.length === 0 ? undefined : namespaceData;
}

const mountData = mountPath
? namespaceData.map((namespace) => namespace?.mounts_by_key[mountPath]).filter((d) => d !== undefined)
: namespaceData;

return mountData.length === 0 ? undefined : mountData;
}

get filteredActivityByNamespace() {
const { namespace, activity } = this.args;
return activity.byNamespace.find((ns) => ns.label === namespace);
}

get filteredActivityByAuthMount() {
return this.filteredActivityByNamespace?.mounts?.find((mount) => mount.label === this.args.mountPath);
}

get filteredActivity() {
return this.args.mountPath ? this.filteredActivityByAuthMount : this.filteredActivityByNamespace;
}

get isCurrentMonth() {
const { activity } = this.args;
const current = parseAPITimestamp(activity.responseTimestamp) as Date;
const start = parseAPITimestamp(activity.startTime) as Date;
const end = parseAPITimestamp(activity.endTime) as Date;
return isSameMonth(start, current) && isSameMonth(end, current);
}

get isDateRange() {
const { activity } = this.args;
return !isSameMonth(
parseAPITimestamp(activity.startTime) as Date,
parseAPITimestamp(activity.endTime) as Date
);
}

// (object) top level TOTAL client counts for given date range
get totalUsageCounts() {
const { namespace, activity } = this.args;
return namespace ? this.filteredActivity : activity.total;
}

get upgradeDuringActivity() {
const { versionHistory, activity } = this.args;
if (versionHistory) {
// filter for upgrade data of noteworthy upgrades (1.9 and/or 1.10)
const upgradeVersionHistory = versionHistory.filter(
({ version }) => version.match('1.9') || version.match('1.10')
);
if (upgradeVersionHistory.length) {
const activityStart = parseAPITimestamp(activity.startTime) as Date;
const activityEnd = parseAPITimestamp(activity.endTime) as Date;
// filter and return all upgrades that happened within date range of queried activity
const upgradesWithinData = upgradeVersionHistory.filter(({ timestampInstalled }) => {
const upgradeDate = parseAPITimestamp(timestampInstalled) as Date;
return isAfter(upgradeDate, activityStart) && isBefore(upgradeDate, activityEnd);
});
return upgradesWithinData.length === 0 ? null : upgradesWithinData;
}
}
return null;
}

// (object) single month new client data with total counts + array of namespace breakdown
get newClientCounts() {
if (this.isDateRange || !this.byMonthActivityData) {
return null;
}
return this.byMonthActivityData[0]?.new_clients;
}

// total client data for horizontal bar chart in attribution component
get totalClientAttribution() {
const { namespace, activity } = this.args;
if (namespace) {
return this.filteredActivityByNamespace?.mounts || null;
} else {
return activity.byNamespace || null;
}
}

// new client data for horizontal bar chart
get newClientAttribution() {
// new client attribution only available in a single, historical month (not a date range or current month)
if (this.isDateRange || this.isCurrentMonth) return null;

if (this.args.namespace) {
return this.newClientCounts?.mounts || null;
} else {
return this.newClientCounts?.namespaces || null;
}
}

get hasAttributionData() {
const { mountPath, namespace } = this.args;
if (!mountPath) {
if (namespace) {
const mounts = this.filteredActivityByNamespace?.mounts?.map((mount) => ({
id: mount.label,
name: mount.label,
}));
return mounts && mounts.length > 0;
}
return !!this.totalClientAttribution && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
}

return false;
}

get upgradeExplanation() {
if (this.upgradeDuringActivity) {
if (this.upgradeDuringActivity.length === 1) {
const version = this.upgradeDuringActivity[0]?.version || '';
if (version.match('1.9')) {
return ' How we count clients changed in 1.9, so keep that in mind when looking at the data.';
}
if (version.match('1.10')) {
return ' We added monthly breakdowns and mount level attribution starting in 1.10, so keep that in mind when looking at the data.';
}
}
// return combined explanation if spans multiple upgrades
return ' How we count clients changed in 1.9 and we added monthly breakdowns and mount level attribution starting in 1.10. Keep this in mind when looking at the data.';
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<p class="chart-description">{{this.chartText.newCopy}}</p>
<Clients::HorizontalBarChart
@dataset={{this.barChartNewClients}}
@chartLegend={{@chartLegend}}
@chartLegend={{this.attributionLegend}}
@totalCounts={{@newUsageCounts}}
@noDataMessage="There are no new clients for this namespace during this time period."
/>
Expand All @@ -40,22 +40,14 @@
<div class="chart-container-right" data-test-chart-container="total-clients">
<h2 class="chart-title">Total clients</h2>
<p class="chart-description">{{this.chartText.totalCopy}}</p>
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@chartLegend={{@chartLegend}}
@totalCounts={{@totalUsageCounts}}
/>
<Clients::HorizontalBarChart @dataset={{this.barChartTotalClients}} @chartLegend={{this.attributionLegend}} />
</div>
{{else}}
<div
class={{concat (unless this.barChartTotalClients "chart-empty-state ") "chart-container-wide"}}
data-test-chart-container="single-chart"
>
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@chartLegend={{@chartLegend}}
@totalCounts={{@totalUsageCounts}}
/>
<Clients::HorizontalBarChart @dataset={{this.barChartTotalClients}} @chartLegend={{this.attributionLegend}} />
</div>
<div class="chart-subTitle">
<p class="chart-subtext" data-test-attribution-subtext>{{this.chartText.totalCopy}}</p>
Expand All @@ -71,9 +63,10 @@
<p class="data-details">{{format-number this.topClientCounts.clients}}</p>
</div>
{{/if}}
<div class="legend-center">
<span class="light-dot"></span><span class="legend-label">{{capitalize (get @chartLegend "0.label")}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize (get @chartLegend "1.label")}}</span>
<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">
Expand All @@ -96,9 +89,16 @@
</M.Header>
<M.Body>
<p class="has-bottom-margin-s">
This export will include the namespace path, authentication method path, and the associated total, entity, and
non-entity clients for the below
{{if this.formattedEndDate "date range" "month"}}.
This export will include the namespace path, mount path and associated total, entity, non-entity and secrets sync
clients for the
{{if this.formattedEndDate "date range" "month"}}
below.
</p>
<p class="has-bottom-margin-s">
The
<code>mount_path</code>
for secrets sync clients is the KV v2 engine path and for entity/non-entity clients is the corresponding
authentication method path.
</p>
<p class="has-bottom-margin-s is-subtitle-gray">SELECTED DATE {{if this.formattedEndDate " RANGE"}}</p>
<p class="has-bottom-margin-s" data-test-export-date-range>
Expand Down
27 changes: 18 additions & 9 deletions ui/app/components/clients/attribution.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { format, isSameMonth } from 'date-fns';
* @example
* ```js
* <Clients::Attribution
* @chartLegend={{this.chartLegend}}
* @totalUsageCounts={{this.totalUsageCounts}}
* @newUsageCounts={{this.newUsageCounts}}
* @totalClientAttribution={{this.totalClientAttribution}}
Expand All @@ -31,7 +30,6 @@ import { format, isSameMonth } from 'date-fns';
* @upgradeExplanation="We added monthly breakdowns and mount level attribution starting in 1.10, so keep that in mind when looking at the data."
* />
* ```
* @param {array} chartLegend - (passed to child) array of objects with key names 'key' and 'label' so data can be stacked
* @param {object} totalUsageCounts - object with total client counts for chart tooltip text
* @param {object} newUsageCounts - object with new client counts for chart tooltip text
* @param {array} totalClientAttribution - array of objects containing a label and breakdown of client counts for total clients
Expand All @@ -47,6 +45,11 @@ import { format, isSameMonth } from 'date-fns';
export default class Attribution extends Component {
@tracked showCSVDownloadModal = false;
@service download;
attributionLegend = [
{ key: 'entity_clients', label: 'entity clients' },
{ key: 'non_entity_clients', label: 'non-entity clients' },
{ key: 'secret_syncs', label: 'secrets sync clients' },
];

get formattedStartDate() {
if (!this.args.startTimestamp) return null;
Expand Down Expand Up @@ -123,10 +126,10 @@ export default class Attribution extends Component {
}

destructureCountsToArray(object) {
// destructure the namespace object {label: 'some-namespace', entity_clients: 171, non_entity_clients: 20, clients: 191}
// destructure the namespace object {label: 'some-namespace', entity_clients: 171, non_entity_clients: 20, secret_syncs: 10, clients: 201}
// to get integers for CSV file
const { clients, entity_clients, non_entity_clients } = object;
return [clients, entity_clients, non_entity_clients];
const { clients, entity_clients, non_entity_clients, secret_syncs } = object;
return [clients, entity_clients, non_entity_clients, secret_syncs];
}

constructCsvRow(namespaceColumn, mountColumn = null, totalColumns, newColumns = null) {
Expand All @@ -146,19 +149,25 @@ export default class Attribution extends Component {
const csvData = [];
// added to clarify that the row of namespace totals without an auth method (blank) are not additional clients
// but indicate the total clients for that ns, including its auth methods
const upgrade = this.args.upgradeExplanation
? `\n **data contains an upgrade, mount summation may not equal namespace totals`
: '';
const descriptionOfBlanks = this.isSingleNamespace
? ''
: `\n *namespace totals, inclusive of auth method clients`;
: `\n *namespace totals, inclusive of mount clients ${upgrade}`;
const csvHeader = [
'Namespace path',
`"Authentication method ${descriptionOfBlanks}"`,
`"Mount path ${descriptionOfBlanks}"`,
'Total clients',
'Entity clients',
'Non-entity clients',
'Secrets sync clients',
];

if (newAttribution) {
csvHeader.push('Total new clients, New entity clients, New non-entity clients');
csvHeader.push(
'Total new clients, New entity clients, New non-entity clients, New secrets sync clients'
);
}

totalAttribution.forEach((totalClientsObject) => {
Expand Down Expand Up @@ -199,7 +208,7 @@ export default class Attribution extends Component {
const endRange = this.formattedEndDate ? `-${this.formattedEndDate}` : '';
const csvDateRange = this.formattedStartDate + endRange;
return this.isSingleNamespace
? `clients_by_auth_method_${csvDateRange}`
? `clients_by_mount_path_${csvDateRange}`
: `clients_by_namespace_${csvDateRange}`;
}

Expand Down
Loading
Loading