diff --git a/changelog/28036.txt b/changelog/28036.txt new file mode 100644 index 0000000000..f47891e46c --- /dev/null +++ b/changelog/28036.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Update the client count dashboard to use API namespace filtering and other UX improvements +``` \ No newline at end of file diff --git a/ui/app/adapters/application.js b/ui/app/adapters/application.js index 917262d0da..24416aa8e2 100644 --- a/ui/app/adapters/application.js +++ b/ui/app/adapters/application.js @@ -92,7 +92,7 @@ export default RESTAdapter.extend({ controlGroup.deleteControlGroupToken(controlGroupToken.accessor); } const [resp] = args; - if (resp && resp.warnings) { + if (resp && resp.warnings && !options.skipWarnings) { const flash = this.flashMessages; resp.warnings.forEach((message) => { flash.info(message); diff --git a/ui/app/adapters/clients/activity.js b/ui/app/adapters/clients/activity.js index 7a85c9505d..317444c137 100644 --- a/ui/app/adapters/clients/activity.js +++ b/ui/app/adapters/clients/activity.js @@ -39,8 +39,15 @@ export default class ActivityAdapter extends ApplicationAdapter { queryRecord(store, type, query) { const url = `${this.buildURL()}/internal/counters/activity`; - const queryParams = this.formatQueryParams(query); - return this.ajax(url, 'GET', { data: queryParams }).then((resp) => { + const options = { + data: this.formatQueryParams(query), + }; + + if (query?.namespace) { + options.namespace = query.namespace; + } + + return this.ajax(url, 'GET', options).then((resp) => { const response = resp || {}; response.id = response.request_id || 'no-data'; return response; @@ -71,11 +78,12 @@ export default class ActivityAdapter extends ApplicationAdapter { } } - urlForFindRecord(id) { - // debug reminder so model is stored in Ember data with the same id for consistency + // Only dashboard uses findRecord, the client count dashboard uses queryRecord + findRecord(store, type, id) { if (id !== 'clients/activity') { debug(`findRecord('clients/activity') should pass 'clients/activity' as the id, you passed: '${id}'`); } - return `${this.buildURL()}/internal/counters/activity`; + const url = `${this.buildURL()}/internal/counters/activity`; + return this.ajax(url, 'GET', { skipWarnings: true }); } } diff --git a/ui/app/components/clients/activity.ts b/ui/app/components/clients/activity.ts index 10a419b2e9..b64ff76f6c 100644 --- a/ui/app/components/clients/activity.ts +++ b/ui/app/components/clients/activity.ts @@ -10,7 +10,13 @@ import Component from '@glimmer/component'; import { isSameMonth } from 'date-fns'; import { parseAPITimestamp } from 'core/utils/date-formatters'; import { calculateAverage } from 'vault/utils/chart-helpers'; -import { filterVersionHistory, hasMountsKey, hasNamespacesKey } from 'core/utils/client-count-utils'; +import { + filterByMonthDataForMount, + filteredTotalForMount, + filterVersionHistory, +} from 'core/utils/client-count-utils'; +import { service } from '@ember/service'; +import { sanitizePath } from 'core/utils/sanitize-path'; import type ClientsActivityModel from 'vault/models/clients/activity'; import type ClientsVersionHistoryModel from 'vault/models/clients/version-history'; @@ -19,7 +25,9 @@ import type { MountNewClients, NamespaceByKey, NamespaceNewClients, + TotalClients, } from 'core/utils/client-count-utils'; +import type NamespaceService from 'vault/services/namespace'; interface Args { activity: ClientsActivityModel; @@ -31,6 +39,8 @@ interface Args { } export default class ClientsActivityComponent extends Component { + @service declare readonly namespace: NamespaceService; + average = ( data: | (ByMonthNewClients | NamespaceNewClients | MountNewClients | undefined)[] @@ -40,48 +50,27 @@ export default class ClientsActivityComponent extends Component { return calculateAverage(data, key); }; + // path of the filtered namespace OR current one, for filtering relevant data + get namespacePathForFilter() { + const { namespace } = this.args; + const currentNs = this.namespace.currentNamespace; + return sanitizePath(namespace || currentNs || 'root'); + } + get byMonthActivityData() { - const { activity, namespace } = this.args; - return namespace ? this.filteredActivityByMonth : activity.byMonth; + const { activity, mountPath } = this.args; + const nsPath = this.namespacePathForFilter; + if (mountPath) { + // only do client-side filtering if we have a mountPath filter set + return filterByMonthDataForMount(activity.byMonth, nsPath, mountPath); + } + return 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]) - .filter((d) => d !== undefined); - - if (!mountPath) { - return namespaceData || []; - } - - const mountData = namespaceData - ?.map((namespace) => namespace?.mounts_by_key[mountPath]) - .filter((d) => d !== undefined); - - return 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; @@ -99,62 +88,18 @@ export default class ClientsActivityComponent extends Component { } // (object) top level TOTAL client counts for given date range - get totalUsageCounts() { - const { namespace, activity } = this.args; - return namespace ? this.filteredActivity : activity.total; + get totalUsageCounts(): TotalClients { + const { namespace, activity, mountPath } = this.args; + // only do this if we have a mountPath filter. + // namespace is filtered on API layer + if (activity?.byNamespace && namespace && mountPath) { + return filteredTotalForMount(activity.byNamespace, namespace, mountPath); + } + return activity?.total; } get upgradesDuringActivity() { const { versionHistory, activity } = this.args; return filterVersionHistory(versionHistory, activity.startTime, activity.endTime); } - - // (object) single month new client data with total counts and array of - // either namespaces or mounts - get newClientCounts() { - if (this.isDateRange || this.byMonthActivityData.length === 0) { - 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 || !this.newClientCounts) return null; - - const newCounts = this.newClientCounts; - if (this.args.namespace && hasMountsKey(newCounts)) return newCounts?.mounts; - - if (hasNamespacesKey(newCounts)) return newCounts?.namespaces; - - return 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; - } } diff --git a/ui/app/components/clients/attribution.hbs b/ui/app/components/clients/attribution.hbs index 311ba5dd9e..385f033461 100644 --- a/ui/app/components/clients/attribution.hbs +++ b/ui/app/components/clients/attribution.hbs @@ -3,97 +3,29 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -{{! only show side-by-side horizontal bar charts if data is from a single, historical month }} -
-