Skip to content

Commit 12f2201

Browse files
committed
feat(stats): make UsageHeatmap self-fetching for independent range selection
1 parent 7d57c4b commit 12f2201

21 files changed

Lines changed: 1961 additions & 1721 deletions

File tree

webview-ui/src/components/dashboard/DashboardView.tsx

Lines changed: 130 additions & 139 deletions
Large diffs are not rendered by default.

webview-ui/src/components/stats/UsageHeatmap.tsx

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, { memo, useMemo, useState } from "react"
1+
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
22

33
import { useAppTranslation } from "@/i18n/TranslationContext"
4+
import { vscode } from "@/utils/vscode"
45
import type { StatsBucket } from "@roo-code/types"
56

67
import { Button, StandardTooltip } from "@/components/ui"
@@ -13,10 +14,6 @@ interface DailyActivity {
1314
events: number
1415
}
1516

16-
interface UsageHeatmapProps {
17-
buckets: StatsBucket[]
18-
}
19-
2017
// ── Heatmap color levels ────────────────────────────────────────────────────
2118

2219
/**
@@ -79,15 +76,81 @@ const RANGE_OPTIONS: HeatmapRange[] = ["30d", "60d", "120d", "360d"]
7976

8077
// ── UsageHeatmap ────────────────────────────────────────────────────────────
8178

82-
const UsageHeatmap = memo(({ buckets }: UsageHeatmapProps) => {
79+
const UsageHeatmap = memo(() => {
8380
const { t } = useAppTranslation()
8481
const [range, setRange] = useState<HeatmapRange>("30d")
82+
const [heatmapBuckets, setHeatmapBuckets] = useState<StatsBucket[]>([])
83+
const [loading, setLoading] = useState(true)
84+
const latestHeatmapRequestIdRef = useRef<string>("")
85+
86+
// Fetch heatmap data independently from the top-level date picker.
87+
// Sends a getUsageStats message with a "heatmap-" requestId prefix so
88+
// responses can be filtered from DashboardView's own requests.
89+
const fetchHeatmapData = useCallback((rangeArg: HeatmapRange) => {
90+
const days = RANGE_DAYS[rangeArg]
91+
const requestId = `heatmap-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
92+
latestHeatmapRequestIdRef.current = requestId
93+
setLoading(true)
94+
95+
const from = new Date(Date.now() - days * 86400000)
96+
from.setHours(0, 0, 0, 0)
97+
98+
let timezone: string
99+
try {
100+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"
101+
} catch {
102+
timezone = "UTC"
103+
}
104+
105+
vscode.postMessage({
106+
type: "getUsageStats",
107+
requestId,
108+
usageStatsQuery: {
109+
from: from.toISOString(),
110+
timezone,
111+
groupBy: ["day"],
112+
includeCancelled: false,
113+
},
114+
})
115+
}, [])
116+
117+
// Listen for responses to our heatmap requests and perform initial fetch.
118+
useEffect(() => {
119+
const handleMessage = (e: MessageEvent) => {
120+
const message = e.data
121+
122+
if (
123+
message.type === "getUsageStatsResponse" &&
124+
typeof message.requestId === "string" &&
125+
message.requestId.startsWith("heatmap-") &&
126+
message.requestId === latestHeatmapRequestIdRef.current
127+
) {
128+
if (message.usageStatsSnapshot) {
129+
setHeatmapBuckets(message.usageStatsSnapshot.buckets ?? [])
130+
}
131+
setLoading(false)
132+
}
133+
}
134+
135+
window.addEventListener("message", handleMessage)
136+
fetchHeatmapData(range) // Initial fetch
137+
138+
return () => window.removeEventListener("message", handleMessage)
139+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
140+
141+
const handleRangeChange = useCallback(
142+
(newRange: HeatmapRange) => {
143+
setRange(newRange)
144+
fetchHeatmapData(newRange)
145+
},
146+
[fetchHeatmapData],
147+
)
85148

86149
// Extract daily activity from buckets that have a "day" key
87150
const dailyMap = useMemo(() => {
88151
const map = new Map<string, DailyActivity>()
89152

90-
for (const bucket of buckets) {
153+
for (const bucket of heatmapBuckets) {
91154
const dayKey = bucket.key?.day
92155
if (!dayKey) continue
93156

@@ -105,7 +168,7 @@ const UsageHeatmap = memo(({ buckets }: UsageHeatmapProps) => {
105168
}
106169

107170
return map
108-
}, [buckets])
171+
}, [heatmapBuckets])
109172

110173
// Generate the date range for display
111174
const days = useMemo(() => {
@@ -147,27 +210,25 @@ const UsageHeatmap = memo(({ buckets }: UsageHeatmapProps) => {
147210
return (
148211
<div className="flex flex-col gap-2" data-testid="usage-heatmap">
149212
<div className="flex items-center justify-between">
150-
<h4 className="text-sm font-medium text-vscode-foreground m-0">
151-
{t("stats:heatmap.title")}
152-
</h4>
213+
<h4 className="text-sm font-medium text-vscode-foreground m-0">{t("stats:heatmap.title")}</h4>
153214
<div className="flex gap-1">
154215
{RANGE_OPTIONS.map((option) => (
155216
<Button
156217
key={option}
157218
variant={range === option ? "primary" : "ghost"}
158219
size="sm"
159-
onClick={() => setRange(option)}
220+
onClick={() => handleRangeChange(option)}
160221
data-testid={`heatmap-range-${option}`}>
161222
{t(`stats:heatmap.${option}`)}
162223
</Button>
163224
))}
164225
</div>
165226
</div>
166227

167-
{!hasData ? (
168-
<div className="text-xs text-vscode-descriptionForeground py-4">
169-
{t("stats:heatmap.noData")}
170-
</div>
228+
{loading && !hasData ? (
229+
<div className="text-xs text-vscode-descriptionForeground py-4">{t("stats:heatmap.loading")}</div>
230+
) : !hasData ? (
231+
<div className="text-xs text-vscode-descriptionForeground py-4">{t("stats:heatmap.noData")}</div>
171232
) : (
172233
<>
173234
<div

0 commit comments

Comments
 (0)