Skip to content

Commit 2880ff8

Browse files
committed
fix(usage): stop a retained summary from crossing workspace scopes
The summary key now carries a workspaceId, and keepPreviousData retains across any key change — so moving between two drill-downs drew one workspace's headline, delta, and chart under the other's name until the fetch landed. - Narrow placeholderData to a period change only, matching the breakdown's existing scoped predicate, and share the one key-identity helper - Order the summary key so window is the trailing segment, making the scope a plain prefix as it already is on the breakdown - Give UsageSummary the isPlaceholderData signal UsageConsumers already takes, so retained figures dim instead of reading as fresh ones
1 parent bbd5134 commit 2880ff8

4 files changed

Lines changed: 64 additions & 16 deletions

File tree

apps/sim/ee/organization-usage/components/usage-monitoring.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ export function UsageMonitoring({
348348
summary={workspaceSummary.data}
349349
isLoading={workspaceSummary.isLoading}
350350
isError={workspaceSummary.isError}
351+
isPlaceholderData={workspaceSummary.isPlaceholderData}
351352
/>
352353
</SettingsSection>
353354
{/*
@@ -481,6 +482,7 @@ export function UsageMonitoring({
481482
}
482483
isLoading={summary.isLoading}
483484
isError={summary.isError}
485+
isPlaceholderData={summary.isPlaceholderData}
484486
/>
485487
</SettingsSection>
486488
{/*

apps/sim/ee/organization-usage/components/usage-summary.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useMemo } from 'react'
4-
import { Badge } from '@sim/emcn'
4+
import { Badge, cn } from '@sim/emcn'
55
import { BarChart } from '@/components/charts'
66
import type { OrganizationUsageSummary } from '@/lib/api/contracts/organization-usage'
77
import { formatCreditsLabel } from '@/lib/billing/credits/conversion'
@@ -16,14 +16,26 @@ interface UsageSummaryProps {
1616
limitCredits?: number | null
1717
isLoading: boolean
1818
isError: boolean
19+
/**
20+
* Dims the figures while a re-keyed fetch resolves, rather than blanking them — the
21+
* same treatment `UsageConsumers` gives a retained list. Without it the headline and
22+
* chart present the previous period's numbers as though they were the new period's.
23+
*/
24+
isPlaceholderData?: boolean
1925
}
2026

2127
function percentDelta(current: number, previous: number): number | null {
2228
if (previous <= 0) return null
2329
return ((current - previous) / previous) * 100
2430
}
2531

26-
export function UsageSummary({ summary, limitCredits, isLoading, isError }: UsageSummaryProps) {
32+
export function UsageSummary({
33+
summary,
34+
limitCredits,
35+
isLoading,
36+
isError,
37+
isPlaceholderData,
38+
}: UsageSummaryProps) {
2739
/*
2840
Stabilized so `BarChart`'s `memo()` can actually pass. Built inline it was a new
2941
array on every render of the panel — a date-picker toggle or an export click
@@ -52,7 +64,9 @@ export function UsageSummary({ summary, limitCredits, isLoading, isError }: Usag
5264
const isOverLimit = hasLimit && used > limitCredits
5365

5466
return (
55-
<div className='flex flex-col gap-3'>
67+
<div
68+
className={cn('flex flex-col gap-3', isPlaceholderData && 'opacity-50 transition-opacity')}
69+
>
5670
{/*
5771
One line, and the allowance sits beside the figure rather than under it —
5872
restating "4,958 credits used" below a "4,958 credits" headline said the same

apps/sim/hooks/queries/organization-usage.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ export const ORGANIZATION_USAGE_EVENTS_STALE_TIME = 30 * 1000
2626

2727
const EVENTS_PAGE_SIZE = 50
2828

29+
/**
30+
* A usage key with its trailing segment dropped — the identity of the question being
31+
* asked, which is what `placeholderData` has to compare on.
32+
*
33+
* Both keys put the one segment their placeholder may legitimately cross last: the
34+
* summary's window ("the same scope, a different period") and the breakdown's row limit
35+
* ("the same list, more rows"). Everything a retained answer must never cross —
36+
* organization, workspace, dimension — sits in the prefix.
37+
*/
38+
function usageKeyIdentity(key: readonly unknown[]): string {
39+
return hashKey(key.slice(0, -1))
40+
}
41+
2942
interface UseSummaryOptions {
3043
/** The panel fetches the drill-down's chart only while that view is open. */
3144
enabled?: boolean
@@ -39,8 +52,9 @@ export function useOrganizationUsageSummary(
3952
options: UseSummaryOptions = {}
4053
) {
4154
const { workspaceId } = options
55+
const queryKey = organizationUsageKeys.summary(organizationId ?? '', window, workspaceId)
4256
return useQuery({
43-
queryKey: organizationUsageKeys.summary(organizationId ?? '', window, workspaceId),
57+
queryKey,
4458
queryFn: ({ signal }): Promise<OrganizationUsageSummary> =>
4559
requestJson(getOrganizationUsageSummaryContract, {
4660
params: { id: organizationId as string },
@@ -49,8 +63,22 @@ export function useOrganizationUsageSummary(
4963
}),
5064
enabled: Boolean(organizationId) && (options.enabled ?? true),
5165
staleTime: ORGANIZATION_USAGE_SUMMARY_STALE_TIME,
52-
// Changing the period should dim the current figures rather than blank them.
53-
placeholderData: keepPreviousData,
66+
/**
67+
* Kept only across a period change — the same scope asked about a different window,
68+
* where dimming the figures beats blanking them.
69+
*
70+
* Not `keepPreviousData`, which retains across *any* key change: once the key
71+
* carries a workspace, moving between two drill-downs would draw one workspace's
72+
* headline, delta, and chart under the other's name until the fetch landed. A
73+
* figure attributed to the wrong workspace is worse than a brief skeleton, and
74+
* unlike a ranked list it carries nothing that would look out of place.
75+
*/
76+
placeholderData: (previous, previousQuery) =>
77+
previous &&
78+
previousQuery &&
79+
usageKeyIdentity(previousQuery.queryKey) === usageKeyIdentity(queryKey)
80+
? previous
81+
: undefined,
5482
})
5583
}
5684

@@ -62,14 +90,6 @@ interface UseBreakdownOptions {
6290
workspaceId?: string
6391
}
6492

65-
/**
66-
* A breakdown key with its trailing row limit removed — the identity of the list,
67-
* which is what "the same list, more rows" has to compare on.
68-
*/
69-
function breakdownListIdentity(key: readonly unknown[]): string {
70-
return hashKey(key.slice(0, -1))
71-
}
72-
7393
export function useOrganizationUsageBreakdown(
7494
organizationId: string | undefined,
7595
window: OrganizationUsageWindowKey,
@@ -110,7 +130,7 @@ export function useOrganizationUsageBreakdown(
110130
placeholderData: (previous, previousQuery) =>
111131
previous &&
112132
previousQuery &&
113-
breakdownListIdentity(previousQuery.queryKey) === breakdownListIdentity(queryKey)
133+
usageKeyIdentity(previousQuery.queryKey) === usageKeyIdentity(queryKey)
114134
? previous
115135
: undefined,
116136
})

apps/sim/hooks/queries/utils/organization-usage-keys.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,19 @@ export const organizationUsageKeys = {
2929
/** Set only inside the Workspaces drill-down, whose chart reads one workspace. */
3030
workspaceId?: string
3131
) =>
32-
[...organizationUsageKeys.all(organizationId), 'summary', window, workspaceId ?? ''] as const,
32+
[
33+
...organizationUsageKeys.all(organizationId),
34+
'summary',
35+
workspaceId ?? '',
36+
/*
37+
Last deliberately, as on `breakdown`: it is the one segment the summary's
38+
`placeholderData` may cross, so the scope's identity is a plain prefix rather
39+
than an index-based filter that would silently drop `workspaceId` if a segment
40+
were ever appended. Ordering it the other way is what let a retained summary
41+
cross workspaces.
42+
*/
43+
window,
44+
] as const,
3345
breakdowns: (organizationId: string, window: OrganizationUsageWindowKey) =>
3446
[...organizationUsageKeys.all(organizationId), 'breakdown', window] as const,
3547
breakdown: (

0 commit comments

Comments
 (0)