feat(dashboard): make the activity histogram the time-range selector - #445
feat(dashboard): make the activity histogram the time-range selector#445njbrake wants to merge 4 commits into
Conversation
Rework the Usage and Activity time controls around a request-volume histogram, the pattern CloudWatch Logs Insights, Kibana, and Grafana/Loki use for log exploration. The selection lives on the chart itself: full-height handles at the window edges zoom (react-aria slider thumbs, so they are pointer-captured and glide at 0.1-bucket steps), a slim strip along the axis band pans, regions outside the window dim in place, and −/+ buttons step the zoom. Zooming out past the extent promotes to the next larger preset (30d -> 90d -> 12mo), so a wrong default window is always one tap from a wider view; Reset restores the full extent. Presets set the extent the chart spans; the tiles / breakdowns / log follow the selection. This also fixes the original defects from the issue: - "All" on Usage silently meant a rolling 30 days (an omitted start hit the server's 30-day default), so the total drifted with the clock. Usage now drops "All" for bounded 90d and 12mo presets, reaching the full span the server serves without an unbounded, clock-rolling window. - The old custom picker (two datetime-local inputs) filtered only when both a date and a time were entered, and read local wall-clock times as UTC. The selection is bucket-based and UTC-aligned, matching the summary endpoint's bucketing and the existing "times in UTC" caption, so a picked range lines up with the bars and nothing is silently reinterpreted. - The bucket is chosen from the active preset's extent, and the effective window is always captioned, so a default or clamp is never invisible. Interaction details: drag feedback is a pair of CSS-positioned shades over the chart (the recharts tree never re-renders mid-drag) and the window commits once, on release, snapped outward to whole buckets. The histogram spans the preset extent independently of the selection, so there is always context to zoom back out into (one extra summary query, deduped with the active one when the selection is full). The handles are the only chart-level selector, so a stray tap cannot collapse the window; the pan strip sits over the axis band, so bar tooltips keep working; and touch-pan-y keeps mobile scrolling intact. Activity keeps a truthful "All" (its list endpoint is genuinely unbounded). Both pages share the timeline, the presets, and the window math (lib/timeRange). Active model/user/key/status/priced filters render as removable chips with an "Add filter" toggle for the pickers, the log-tool convention for keeping applied filters visible (especially on mobile). The Refresh button is an icon with an "updated Ns ago" freshness label; on Usage and Activity it rides the timeline's preset row rather than taking a dedicated header row, and stays in the header on Overview. Fixes #440 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 17 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (27)
WalkthroughChangesThe dashboard adds shared UTC time-range utilities, interactive timeline and filter-chip controls, refreshed Usage and Activity page integrations, Overview refresh behavior, and regenerated static dashboard bundles and assets. Dashboard analytics
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Four fixes from reviewing the PR diff: - Frame a drill-down window that reaches outside the preset extent. A Usage drill-through carries explicit bounds while Activity's `range` param still holds the 24h default, so the timeline highlighted "24h" and drew an unrelated 24h histogram under a 30-day window. The window itself is now the frame (no preset highlighted), and zoom-out falls back to the smallest broader preset so it never dead-ends. - Make arrow keys work on the handles. The slider's built-in arrow step is the fine pointer step (0.1 bucket), which the bucket-snapping commit immediately undid, so a single arrow press was a silent no-op. A capture-phase keydown on the wrapper reroutes arrows to whole-bucket steps before react-aria's unconditional handler sees them; PageUp/Down, Home, and End stay react-aria's. - Commit pan drags from a ref. A pointerup can fire before the last pointermove's setState re-renders, so committing from the closure value could snap to a different bucket than the one on screen. - Refresh stale comments that still referenced the removed calendar picker and its local-time semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (11)
web/src/components/FilterChips.tsx (1)
52-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winGive the ✕ control a visible focus ring.
It's tabbable and correctly labelled (nice), but only
hover:states are styled, so a keyboard user tabbing across chips can't see where they are. Either addfocus-visible:styling or lean on HeroUI'sButtonwithisIconOnly, which brings its own focus treatment.♻️ Minimal fix
- className="ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full text-[var(--otari-muted)] hover:bg-[var(--otari-line)] hover:text-[var(--otari-ink)]" + className="ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full text-[var(--otari-muted)] hover:bg-[var(--otari-line)] hover:text-[var(--otari-ink)] focus-visible:ring-2 focus-visible:ring-[var(--otari-brand)] focus-visible:outline-none"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/FilterChips.tsx` around lines 52 - 61, Add a visible keyboard focus treatment to the chip removal button in FilterChips, using focus-visible styling consistent with the existing design tokens while preserving its current hover appearance and accessibility attributes.web/src/components/ui.tsx (1)
146-174: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueOptional: let the clock idle when there's nothing to relabel.
The hook is tidy and leak-free (visibility pausing is a nice touch). One small polish: when
updatedAtis falsy no label renders, yet a 15s re-render still fires on everyRefreshButtonon the page. Passing a nullable interval (or gating the hook onupdatedAt) would make the idle case genuinely free.Also applies to: 191-191
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/ui.tsx` around lines 146 - 174, Update useDisplayClock and its RefreshButton call sites so the clock does not create an interval when updatedAt is falsy and no label renders. Accept a nullable or disabled interval, preserve the existing visibility and cleanup behavior when enabled, and ensure each RefreshButton passes the disabled state for missing updatedAt.web/src/components/ActivityTimeline.tsx (1)
19-24: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winThe recharts tree does re-render mid-drag, unlike what the comment promises.
Nice architecture overall (CSS shades instead of chart redraws is the right instinct), but
datais rebuilt on every render, and every pointermove callssetSel. So each drag frame handsBarCharta fresh array and recharts reconciles the whole histogram. This is the one place where memoizing is clearly earned by measured cost: hoist the chart element into auseMemokeyed onseries/bucketso React reuses the exact same element during drags, and the comment becomes true.♻️ Memoize the chart subtree so drags don't touch it
-import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react";const starts = series.map((p) => p.bucketStart); - const data = series.map((p) => ({ x: p.bucketStart, requests: p.requests })); const n = series.length; + const chart = useMemo(() => { + const data = series.map((p) => ({ x: p.bucketStart, requests: p.requests })); + return ( + <ResponsiveContainer width="100%" height={90}> + <BarChart data={data} margin={{ top: 4, right: 0, left: 0, bottom: 0 }}> + <XAxis + dataKey="x" + tickLine={false} + axisLine={false} + interval="preserveStartEnd" + minTickGap={40} + tickFormatter={(iso: string) => formatTick(iso, bucket)} + tick={{ fontSize: 10, fill: "var(--otari-muted)" }} + /> + <Tooltip cursor={{ fill: "var(--otari-line)", opacity: 0.35 }} content={<TimelineTooltip bucket={bucket} />} /> + <Bar dataKey="requests" fill={BRAND} radius={[2, 2, 0, 0]} isAnimationActive={false} /> + </BarChart> + </ResponsiveContainer> + ); + }, [series, bucket]);Then render
{chart}inside therole="img"wrapper in place of the inlineResponsiveContainerblock.Also applies to: 123-124, 278-295
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/ActivityTimeline.tsx` around lines 19 - 24, Memoize the Recharts subtree in ActivityTimeline using a useMemo keyed by the stable series and bucket inputs, keeping the chart data and BarChart element inside that memoized value so pointer-driven setSel updates do not rebuild it. Replace the inline ResponsiveContainer block inside the role="img" wrapper with the memoized chart element, while preserving the existing chart configuration and drag behavior.web/src/lib/timeRange.ts (1)
28-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPrefer
undefinedovernullfor these absent values.Both the unbounded preset marker and the "no series" return use
null. The frontend standards ask forundefinedin our own types, and it also lets consumers use plain optional-chaining/??patterns without mixing both nullish flavours. Note this ripples into the Usage/Activity pages (out of this cohort), so it's a follow-up rather than a blocker.As per coding guidelines: "Use
undefinedrather thannullfor absent values in own TypeScript types".♻️ Sketch of the change
- seconds: number | null; + seconds?: number;-): { startIso: string; endIso: string } | null { - if (starts.length === 0) return null; +): { startIso: string; endIso: string } | undefined { + if (starts.length === 0) return undefined;Also applies to: 95-95
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/lib/timeRange.ts` at line 28, Update the time-range type and related absent-value handling to use undefined instead of null, including the unbounded preset marker and the no-series return in the relevant time-range symbols. Keep valid numeric values unchanged and propagate the type change through local consumers without introducing null checks.Source: Coding guidelines
web/src/lib/timeRange.test.ts (1)
106-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLocale-dependent date assertions in both new suites.
formatDaydeliberately formats in the viewer's locale (correct for production), so every assertion on strings like "Jul 10" or "Jan 2" silently depends on the locale Node resolves fromLANG/LC_ALL. Pinning the locale once for the test run keeps both suites deterministic across machines and CI images.
web/src/lib/timeRange.test.ts#L106-L136: pin the locale for the suite (e.g. setprocess.env.TZ-style config in the Vitest setup, or assert via a locale-explicit expected string) instead of relying on the ambient default.web/src/components/ActivityTimeline.test.tsx#L43-L49: rely on the same pinned locale rather than hardcoding English month names.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/lib/timeRange.test.ts` around lines 106 - 136, Pin the locale used by the test environment so date assertions are deterministic instead of relying on ambient LANG/LC_ALL settings. Apply this to web/src/lib/timeRange.test.ts lines 106-136 and use the same pinned locale for web/src/components/ActivityTimeline.test.tsx lines 43-49; update the shared Vitest setup or locale-explicit expectations as appropriate, without changing production formatDay behavior.web/src/pages/OverviewPage.tsx (1)
146-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
previousis refetched but not counted as "refreshing".
refresh()kicks offprevious.refetch(), yetisRefreshingleaves it out, so the button can report "done" while the vs-prev deltas are still landing. One extra term keeps the freshness label honest.♻️ Suggested tweak
const isRefreshing = today.isFetching || period.isFetching || + previous.isFetching || health.isFetching ||🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/pages/OverviewPage.tsx` around lines 146 - 153, Update the isRefreshing calculation in OverviewPage to include previous.isFetching alongside the existing query-fetching flags, so refresh remains active until the previous-period data completes.src/gateway/static/dashboard/assets/index-C_gHYR5I.css (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider excluding generated dashboard CSS from stylelint.
Every finding here (duplicate
outline-style,tokeyframe selectors,currentColorcasing, unquotedSegoe UI) comes from Tailwind's own minified output, so there is nothing to fix in this file. Addingsrc/gateway/static/dashboard/assets/**to.stylelintignorekeeps the lint signal meaningful instead of teaching everyone to scroll past ~60 expected errors. The authored source inweb/src/styles/globals.cssstays covered.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/gateway/static/dashboard/assets/index-C_gHYR5I.css` at line 1, Update the Stylelint ignore configuration to exclude generated dashboard assets matching src/gateway/static/dashboard/assets/**, rather than modifying the minified Tailwind output. Keep authored styles such as web/src/styles/globals.css included in linting.Source: Linters/SAST tools
web/src/pages/UsagePage.test.tsx (1)
389-407: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winClass-string assertions will drift if the collapse styling changes.
aria-expandedalready proves the behavior; thehidden/flexchecks pin the test to Tailwind internals, so a switch tohidden md:flexor adata-*attribute would break it for no functional reason. Adata-state(or similar) hook on the region would make this assertion intent-revealing and style-agnostic. Totally optional, and the comment explaining the jsdom limitation is appreciated.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/pages/UsagePage.test.tsx` around lines 389 - 407, Make the filter toggle test assert the region’s semantic state rather than Tailwind class names. Update the UsagePage implementation and test around the “Add filter” toggle to expose and verify a stable data-state (or equivalent) hook that changes with aria-expanded, while preserving the collapsed and expanded behavior.web/src/pages/ActivityPage.test.tsx (1)
407-420: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winTwo new tests share the same forgiving assertion shape. Both use
listCalls(calls).some(url => !url.includes(...)), which is satisfied by any recorded request lacking the parameter rather than by the request the interaction produced, so each could pass for the wrong reason.
web/src/pages/ActivityPage.test.tsx#L407-L420: assert onlistCalls(calls).at(-1)not containingmodel=gpt-4oafter the chip is removed.web/src/pages/ActivityPage.test.tsx#L422-L433: assert onlistCalls(calls).at(-1)not containingstart_dateafter clicking "All".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/pages/ActivityPage.test.tsx` around lines 407 - 420, Strengthen the request assertions in the tests around the chip-removal case at web/src/pages/ActivityPage.test.tsx:407-420 and the “All” time-range case at web/src/pages/ActivityPage.test.tsx:422-433 by checking listCalls(calls).at(-1) after the interaction, ensuring the latest request omits model=gpt-4o and start_date respectively instead of matching any earlier request.web/src/pages/UsagePage.tsx (1)
554-560: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winManual refresh leaves the comparison and model-suggestion queries behind in custom mode.
In preset mode
setStartDateshifts the query keys, sopreviousandmodelSuggestrefetch on their own. In custom mode the keys are unchanged, so pressing Refresh updates the tiles while the "vs previous" delta and the model typeahead keep serving whatever was cached. Refetching them too keeps the whole panel consistent.♻️ Suggested tweak
const refresh = () => { if (!customMode) { setStartDate(isoAgo(preset.seconds ?? 0)); } void summary.refetch(); void contextSummary.refetch(); + if (previousFilters !== null) void previous.refetch(); + void modelSuggest.refetch(); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/pages/UsagePage.tsx` around lines 554 - 560, Update the refresh function to also refetch the comparison/previous and model-suggestion queries when manual refresh runs, including custom mode. Reuse the existing query references and preserve the current summary and context-summary refetch behavior.web/src/pages/ActivityPage.tsx (1)
347-363: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShare the option-label lookup for filter chips.
ActivityPage.tsxhaslabelFrom,UsagePage.tsxhaslabelFor, and both are the same tiny lookup.FilterSelect.tsxhas alabelFortoo, but it’s scoped inside that component. A small exported utility keeps these labels from drifting silently later; keep the existing helpers for any internal-only consumers if needed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/pages/ActivityPage.tsx` around lines 347 - 363, Extract the shared option-label lookup used by ActivityPage’s labelFrom and UsagePage’s labelFor into a small exported utility, then update both pages’ filter-chip label resolution to reuse it. Preserve the fallback to the raw value and retain FilterSelect’s component-scoped helper unless it can safely consume the shared utility without unrelated changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/gateway/static/dashboard/assets/FilterChips-Wo8I8zEd.js`:
- Line 1: Restore the missing named Z export in the generated module, mapping it
to the existing we function that implements the expected (je, ke) lookup.
Regenerate the dashboard assets from source when possible, then verify
UsagePage-BECQyz4e.js can import Z as Ze and invoke it successfully.
In `@web/src/pages/UsagePage.tsx`:
- Around line 554-560: Update the refresh handler in web/src/pages/UsagePage.tsx
lines 554-560 to also refetch previous when previousFilters is not null and
refetch modelSuggest. Update the refresh handler in
web/src/pages/ActivityPage.tsx lines 444-455 to also refetch modelSummary,
ensuring all window-scoped queries refresh together.
---
Nitpick comments:
In `@src/gateway/static/dashboard/assets/index-C_gHYR5I.css`:
- Line 1: Update the Stylelint ignore configuration to exclude generated
dashboard assets matching src/gateway/static/dashboard/assets/**, rather than
modifying the minified Tailwind output. Keep authored styles such as
web/src/styles/globals.css included in linting.
In `@web/src/components/ActivityTimeline.tsx`:
- Around line 19-24: Memoize the Recharts subtree in ActivityTimeline using a
useMemo keyed by the stable series and bucket inputs, keeping the chart data and
BarChart element inside that memoized value so pointer-driven setSel updates do
not rebuild it. Replace the inline ResponsiveContainer block inside the
role="img" wrapper with the memoized chart element, while preserving the
existing chart configuration and drag behavior.
In `@web/src/components/FilterChips.tsx`:
- Around line 52-61: Add a visible keyboard focus treatment to the chip removal
button in FilterChips, using focus-visible styling consistent with the existing
design tokens while preserving its current hover appearance and accessibility
attributes.
In `@web/src/components/ui.tsx`:
- Around line 146-174: Update useDisplayClock and its RefreshButton call sites
so the clock does not create an interval when updatedAt is falsy and no label
renders. Accept a nullable or disabled interval, preserve the existing
visibility and cleanup behavior when enabled, and ensure each RefreshButton
passes the disabled state for missing updatedAt.
In `@web/src/lib/timeRange.test.ts`:
- Around line 106-136: Pin the locale used by the test environment so date
assertions are deterministic instead of relying on ambient LANG/LC_ALL settings.
Apply this to web/src/lib/timeRange.test.ts lines 106-136 and use the same
pinned locale for web/src/components/ActivityTimeline.test.tsx lines 43-49;
update the shared Vitest setup or locale-explicit expectations as appropriate,
without changing production formatDay behavior.
In `@web/src/lib/timeRange.ts`:
- Line 28: Update the time-range type and related absent-value handling to use
undefined instead of null, including the unbounded preset marker and the
no-series return in the relevant time-range symbols. Keep valid numeric values
unchanged and propagate the type change through local consumers without
introducing null checks.
In `@web/src/pages/ActivityPage.test.tsx`:
- Around line 407-420: Strengthen the request assertions in the tests around the
chip-removal case at web/src/pages/ActivityPage.test.tsx:407-420 and the “All”
time-range case at web/src/pages/ActivityPage.test.tsx:422-433 by checking
listCalls(calls).at(-1) after the interaction, ensuring the latest request omits
model=gpt-4o and start_date respectively instead of matching any earlier
request.
In `@web/src/pages/ActivityPage.tsx`:
- Around line 347-363: Extract the shared option-label lookup used by
ActivityPage’s labelFrom and UsagePage’s labelFor into a small exported utility,
then update both pages’ filter-chip label resolution to reuse it. Preserve the
fallback to the raw value and retain FilterSelect’s component-scoped helper
unless it can safely consume the shared utility without unrelated changes.
In `@web/src/pages/OverviewPage.tsx`:
- Around line 146-153: Update the isRefreshing calculation in OverviewPage to
include previous.isFetching alongside the existing query-fetching flags, so
refresh remains active until the previous-period data completes.
In `@web/src/pages/UsagePage.test.tsx`:
- Around line 389-407: Make the filter toggle test assert the region’s semantic
state rather than Tailwind class names. Update the UsagePage implementation and
test around the “Add filter” toggle to expose and verify a stable data-state (or
equivalent) hook that changes with aria-expanded, while preserving the collapsed
and expanded behavior.
In `@web/src/pages/UsagePage.tsx`:
- Around line 554-560: Update the refresh function to also refetch the
comparison/previous and model-suggestion queries when manual refresh runs,
including custom mode. Reuse the existing query references and preserve the
current summary and context-summary refetch behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: d943fc38-67b5-4c60-ac6f-eba85ee19e54
📒 Files selected for processing (45)
src/gateway/static/dashboard/assets/ActivityPage-BFwaR2GB.jssrc/gateway/static/dashboard/assets/ActivityPage-RaXV2Jg6.jssrc/gateway/static/dashboard/assets/AliasesPage-Q7olOjhL.jssrc/gateway/static/dashboard/assets/BudgetsPage-lTuTS-F9.jssrc/gateway/static/dashboard/assets/ConfirmDialog-CWW3AYpK.jssrc/gateway/static/dashboard/assets/DataTable-Bmn_eYSV.jssrc/gateway/static/dashboard/assets/DocsPage-BAH_T1JY.jssrc/gateway/static/dashboard/assets/Field-Dwp6u3t8.jssrc/gateway/static/dashboard/assets/FilterChips-Wo8I8zEd.jssrc/gateway/static/dashboard/assets/KeysPage-bUjPPYgf.jssrc/gateway/static/dashboard/assets/ModelScopeControl-DmfR9Ylv.jssrc/gateway/static/dashboard/assets/ModelsPage-BMtuCmJv.jssrc/gateway/static/dashboard/assets/OverviewPage-CX9hXHG1.jssrc/gateway/static/dashboard/assets/OverviewPage-Deh5RtuT.jssrc/gateway/static/dashboard/assets/ProvidersPage-BRR1ZEsl.jssrc/gateway/static/dashboard/assets/ProvidersPage-DQdazSS9.jssrc/gateway/static/dashboard/assets/SettingsPage-CJyO6zWH.jssrc/gateway/static/dashboard/assets/TablePagination-r1AimUKf.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-1cTNaDRp.jssrc/gateway/static/dashboard/assets/UsagePage-BD35XJZn.jssrc/gateway/static/dashboard/assets/UsagePage-BECQyz4e.jssrc/gateway/static/dashboard/assets/UsersPage-CAU0FXTt.jssrc/gateway/static/dashboard/assets/charts-DIsd9m--.jssrc/gateway/static/dashboard/assets/heroui-BI50yK5B.jssrc/gateway/static/dashboard/assets/heroui-DZj66Arc.jssrc/gateway/static/dashboard/assets/index-73ln6Ld4.csssrc/gateway/static/dashboard/assets/index-CGnfXTEF.jssrc/gateway/static/dashboard/assets/index-CI2beHfq.jssrc/gateway/static/dashboard/assets/index-C_gHYR5I.csssrc/gateway/static/dashboard/assets/recharts-C4kRDmvS.jssrc/gateway/static/dashboard/assets/tableSelection-9hV37uhu.jssrc/gateway/static/dashboard/index.htmlweb/src/components/ActivityTimeline.test.tsxweb/src/components/ActivityTimeline.tsxweb/src/components/FilterChips.test.tsxweb/src/components/FilterChips.tsxweb/src/components/ui.test.tsxweb/src/components/ui.tsxweb/src/lib/timeRange.test.tsweb/src/lib/timeRange.tsweb/src/pages/ActivityPage.test.tsxweb/src/pages/ActivityPage.tsxweb/src/pages/OverviewPage.tsxweb/src/pages/UsagePage.test.tsxweb/src/pages/UsagePage.tsx
💤 Files with no reviewable changes (5)
- src/gateway/static/dashboard/assets/ActivityPage-RaXV2Jg6.js
- src/gateway/static/dashboard/assets/ProvidersPage-BRR1ZEsl.js
- src/gateway/static/dashboard/assets/UsagePage-BD35XJZn.js
- src/gateway/static/dashboard/assets/OverviewPage-Deh5RtuT.js
- src/gateway/static/dashboard/assets/index-CI2beHfq.js
| @@ -0,0 +1 @@ | |||
| import{j as e}from"./tanstack-query-1t81HyiD.js";import{r as y}from"./react-dgEcD0HR.js";import{B as k,S as V,$ as G,h as H,i as J}from"./heroui-BI50yK5B.js";import{R as Q,B as ee,X as te,T as ne,a as re}from"./recharts-C4kRDmvS.js";const P=3600,x=86400,ae=366,pe=[{key:"1h",label:"Last hour",seconds:P,bucket:"hour"},{key:"24h",label:"24h",seconds:x,bucket:"hour"},{key:"7d",label:"7d",seconds:7*x,bucket:"day"},{key:"30d",label:"30d",seconds:30*x,bucket:"day"},{key:"90d",label:"90d",seconds:90*x,bucket:"day"},{key:"12mo",label:"12mo",seconds:ae*x,bucket:"day"}],ve="30d",je=[{key:"1h",label:"1h",seconds:P,bucket:"hour"},{key:"24h",label:"24h",seconds:x,bucket:"hour"},{key:"7d",label:"7d",seconds:7*x,bucket:"day"},{key:"30d",label:"30d",seconds:30*x,bucket:"day"},{key:"all",label:"All",seconds:null,bucket:"day"}],ye="24h",ke="custom";function we(n,r){return n.find(a=>a.key===r)}function Te(n,r=Date.now()){return new Date(r-n*1e3).toISOString()}function se(n){return(n==="hour"?P:x)*1e3}function Ne(n,r,a=Date.now()){const i=new Date(n).getTime();return(r?new Date(r).getTime():a)-i<=x*1e3?"hour":"day"}function ie(n,r,a,i){if(n.length===0)return null;const c=Math.max(0,Math.min(r,a)),l=Math.min(n.length-1,Math.max(r,a)),o=new Date(n[c]).getTime(),m=new Date(n[l]).getTime()+se(i);return{startIso:new Date(o).toISOString(),endIso:new Date(m).toISOString()}}function oe(n,r,a){const i=n.length;if(i===0)return{startIndex:0,endIndex:0};const c=n.map(m=>new Date(m).getTime());let l=0;if(r){const m=new Date(r).getTime();for(let d=0;d<i;d++)c[d]<=m&&(l=d)}let o=i-1;if(a){const m=new Date(a).getTime();o=0;for(let d=0;d<i;d++)c[d]<m&&(o=d)}return o<l&&(o=l),{startIndex:l,endIndex:o}}function j(n,r){return new Date(n).toLocaleDateString(void 0,{month:"short",day:"numeric",...r?{year:"numeric"}:{},timeZone:"UTC"})}function le(n,r){if(!n&&!r)return"All time";if(n&&!r)return`Since ${j(new Date(n).getTime(),!0)}`;if(!n&&r)return`Up to ${j(new Date(r).getTime()-1,!0)}`;const a=new Date(n).getTime(),i=new Date(r).getTime()-1;if(j(a,!0)===j(i,!0))return j(a,!0);const l=new Date(a).getUTCFullYear()===new Date(i).getUTCFullYear();return`${j(a,!l)} – ${j(i,!0)}`}const ce="var(--otari-brand)",de=10,ue=22;function B(n,r){const a=new Date(n);return Number.isNaN(a.getTime())?n:r==="hour"?a.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit",timeZone:"UTC"}):a.toLocaleDateString(void 0,{month:"short",day:"numeric",timeZone:"UTC"})}function me({active:n,label:r,payload:a,bucket:i}){var l;const c=(l=a==null?void 0:a[0])==null?void 0:l.value;return!n||typeof c!="number"||typeof r!="string"?null:e.jsxs("div",{className:"rounded-md border border-[var(--otari-line)] bg-[var(--otari-surface)] px-2 py-1 text-xs shadow-sm",children:[e.jsxs("div",{className:"text-[var(--otari-muted)]",children:[B(r,i)," · UTC"]}),e.jsxs("div",{className:"font-medium tabular-nums text-[var(--otari-ink)]",children:[c.toLocaleString()," requests"]})]})}const he="group top-1/2 h-full w-3 cursor-ew-resize outline-none";function L({index:n,label:r}){return e.jsxs(J,{index:n,"aria-label":r,className:`${he} pointer-events-auto`,children:[e.jsx("span",{"aria-hidden":!0,className:"absolute inset-y-0 left-1/2 w-0.5 -translate-x-1/2 bg-[var(--otari-brand)]"}),e.jsx("span",{"aria-hidden":!0,className:"absolute left-1/2 top-1/2 h-6 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-[var(--otari-brand)] bg-[var(--otari-surface)] shadow-sm group-focus-visible:ring-2 group-focus-visible:ring-[var(--otari-brand)]"})]})}function Se({presets:n,extentKey:r,onPreset:a,onSelectRange:i,onSelectFull:c,series:l,bucket:o,windowStart:m,windowEnd:d,loading:F=!1,ariaLabel:Y="Request volume over the selected window",action:O}){const R=l.map(t=>t.bucketStart),W=l.map(t=>({x:t.bucketStart,requests:t.requests})),s=l.length,q=le(m,d),$=()=>{if(s===0)return[0,1];const{startIndex:t,endIndex:f}=oe(R,m,d);return[t,f+1]},[N,w]=y.useState($),S=y.useRef(!1);y.useEffect(()=>{S.current||w($())},[m,d,s]);const T=([t,f])=>{if(S.current=!1,s===0)return;const v=Math.max(0,Math.min(s-1,Math.floor(t+1e-6))),h=Math.max(v,Math.min(s-1,Math.ceil(f-1e-6)-1));if(v===0&&h===s-1){c(),w([0,s]);return}const u=ie(R,v,h,o);u&&i(u.startIso,u.endIso)},E=n.findIndex(t=>t.key===r),D=E>=0?n[E+1]:void 0,[g,b]=N,M=g<=.01&&b>=s-.01,I=t=>{const f=Math.max(1,Math.min(s,t)),v=(g+b)/2;let h=v-f/2,u=v+f/2;h<0&&(u-=h,h=0),u>s&&(h-=u-s,u=s);const z=[Math.max(0,h),Math.min(s,u)];w(z),T(z)},X=()=>{if(M){D&&a(D);return}I((b-g)*2)},K=()=>I((b-g)/2),C=y.useRef(null),p=y.useRef(null),Z=t=>{if(!p.current||!C.current)return;const f=C.current.getBoundingClientRect().width;if(f<=0)return;const v=(t.clientX-p.current.x)/f*s,h=p.current.sel[1]-p.current.sel[0],u=Math.max(0,Math.min(s-h,p.current.sel[0]+v));w([u,u+h])},A=s?Math.min(g,b)/s*100:0,U=s?Math.max(g,b)/s*100:100,_=!M;return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[n.map(t=>e.jsx(k,{size:"sm",variant:r===t.key?"primary":"outline",onPress:()=>a(t),children:t.label},t.key)),e.jsxs("div",{className:"ml-auto flex items-center gap-3",children:[e.jsxs("span",{className:"text-xs text-[var(--otari-muted)]",children:["Showing ",q," · UTC"]}),O]})]}),e.jsxs("div",{className:"rounded-xl border border-[var(--otari-line)] bg-[var(--otari-surface)] p-2",children:[e.jsxs("div",{className:"flex items-center justify-between gap-2 px-1 pb-1",children:[e.jsxs("span",{className:"text-[11px] font-medium uppercase tracking-wide text-[var(--otari-muted)]",children:["Requests / ",o==="hour"?"hour":"day"]}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"hidden text-[11px] text-[var(--otari-muted)] sm:inline",children:"drag the edges to zoom · the bottom strip to pan"}),e.jsx(k,{size:"sm",variant:"ghost",isIconOnly:!0,"aria-label":"Zoom in",isDisabled:s===0,onPress:K,children:e.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",className:"h-4 w-4","aria-hidden":"true",children:e.jsx("path",{d:"M12 5v14M5 12h14",strokeLinecap:"round"})})}),e.jsx(k,{size:"sm",variant:"ghost",isIconOnly:!0,"aria-label":"Zoom out",isDisabled:s===0||M&&!D,onPress:X,children:e.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",className:"h-4 w-4","aria-hidden":"true",children:e.jsx("path",{d:"M5 12h14",strokeLinecap:"round"})})}),_?e.jsx(k,{size:"sm",variant:"ghost",onPress:()=>T([0,s]),children:"Reset"}):null]})]}),F&&s===0?e.jsx("div",{className:"flex h-[90px] items-center justify-center",children:e.jsx(V,{size:"sm"})}):s===0?e.jsx("div",{className:"flex h-[90px] items-center justify-center text-xs text-[var(--otari-muted)]",children:"No activity in this range."}):e.jsxs("div",{ref:C,className:"relative w-full",children:[e.jsx("div",{role:"img","aria-label":Y,className:"w-full touch-pan-y select-none",children:e.jsx(Q,{width:"100%",height:90,children:e.jsxs(ee,{data:W,margin:{top:4,right:0,left:0,bottom:0},children:[e.jsx(te,{dataKey:"x",tickLine:!1,axisLine:!1,interval:"preserveStartEnd",minTickGap:40,tickFormatter:t=>B(t,o),tick:{fontSize:10,fill:"var(--otari-muted)"}}),e.jsx(ne,{cursor:{fill:"var(--otari-line)",opacity:.35},content:e.jsx(me,{bucket:o})}),e.jsx(re,{dataKey:"requests",fill:ce,radius:[2,2,0,0],isAnimationActive:!1})]})})}),_?e.jsxs(e.Fragment,{children:[e.jsx("div",{"aria-hidden":!0,className:"pointer-events-none absolute inset-y-0 left-0 bg-[var(--otari-bg)] opacity-70",style:{width:`${A}%`}}),e.jsx("div",{"aria-hidden":!0,className:"pointer-events-none absolute inset-y-0 right-0 bg-[var(--otari-bg)] opacity-70",style:{width:`${100-U}%`}})]}):null,e.jsx("div",{"aria-hidden":!0,className:"absolute bottom-0 z-[1] cursor-grab touch-none rounded bg-[var(--otari-brand)]/10 hover:bg-[var(--otari-brand)]/20 active:cursor-grabbing",style:{left:`${A}%`,width:`${Math.max(0,U-A)}%`,height:ue},onPointerDown:t=>{t.stopPropagation(),t.preventDefault(),S.current=!0,p.current={x:t.clientX,sel:[Math.min(g,b),Math.max(g,b)]},t.currentTarget.setPointerCapture(t.pointerId)},onPointerMove:Z,onPointerUp:t=>{t.currentTarget.releasePointerCapture(t.pointerId),p.current=null,T(N)},onPointerCancel:()=>{p.current=null,T(N)}}),e.jsx(G,{"aria-label":"Selected time range",minValue:0,maxValue:s,step:1/de,value:N,onChange:t=>{S.current=!0,Array.isArray(t)&&t.length===2&&w([t[0],t[1]])},onChangeEnd:t=>{Array.isArray(t)&&t.length===2&&T([t[0],t[1]])},className:"pointer-events-none absolute inset-0 z-[2]",children:e.jsxs(H,{className:"pointer-events-none relative h-full w-full",children:[e.jsx(L,{index:0,label:"Window start"}),e.jsx(L,{index:1,label:"Window end"})]})})]})]})]})}function De({chips:n,children:r,onClearAll:a}){const[i,c]=y.useState(!1),l=y.useId();return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx(k,{size:"sm",variant:"outline",onPress:()=>c(o=>!o),"aria-expanded":i,"aria-controls":l,children:i?"Done":"Add filter"}),n.map(o=>e.jsxs("span",{className:"inline-flex items-center gap-1 rounded-full border border-[var(--otari-line)] bg-[var(--otari-brand-tint)] py-0.5 pl-2.5 pr-1 text-xs text-[var(--otari-brand-dark)]",children:[e.jsxs("span",{className:"text-[var(--otari-muted)]",children:[o.label,":"]}),e.jsx("span",{className:"font-medium",children:o.value}),e.jsx("button",{type:"button",onClick:o.onClear,"aria-label":`Remove ${o.label} filter`,className:"ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full text-[var(--otari-muted)] hover:bg-[var(--otari-line)] hover:text-[var(--otari-ink)]",children:e.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",className:"h-3 w-3","aria-hidden":"true",children:e.jsx("path",{d:"M6 6l12 12M18 6L6 18",strokeLinecap:"round"})})})]},o.key)),n.length>0&&a?e.jsx(k,{size:"sm",variant:"ghost",onPress:a,children:"Clear all"}):null]}),e.jsx("div",{id:l,className:i?"flex flex-wrap items-end gap-3":"hidden",children:r})]})}export{Se as A,ke as C,De as F,ve as U,je as a,ye as b,Ne as c,pe as d,we as f,Te as i}; | |||
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Restore the missing Z export before shipping the generated bundle.
UsagePage-BECQyz4e.js imports Z as Ze and immediately calls Ze(je,ke), but this asset exports the matching function we only as f; no Z export exists. The Usage page will therefore fail during module initialization with TypeError: Ze is not a function. Regenerate the dashboard assets from the source, or restore the expected named export, then verify the generated import/export contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/gateway/static/dashboard/assets/FilterChips-Wo8I8zEd.js` at line 1,
Restore the missing named Z export in the generated module, mapping it to the
existing we function that implements the expected (je, ke) lookup. Regenerate
the dashboard assets from source when possible, then verify
UsagePage-BECQyz4e.js can import Z as Ze and invoke it successfully.
…-picker # Conflicts: # src/gateway/static/dashboard/assets/ActivityPage-Byt0czqE.js # src/gateway/static/dashboard/assets/AliasesPage-C8gXJ6jx.js # src/gateway/static/dashboard/assets/AliasesPage-CX3eAAXN.js # src/gateway/static/dashboard/assets/AliasesPage-wZMHXIpE.js # src/gateway/static/dashboard/assets/BudgetsPage-p9qxuU3G.js # src/gateway/static/dashboard/assets/ConfirmDialog-D_mZT4k-.js # src/gateway/static/dashboard/assets/ConfirmDialog-Dnahrq6G.js # src/gateway/static/dashboard/assets/ConfirmDialog-nzYO6eyy.js # src/gateway/static/dashboard/assets/DocsPage-C8AK643M.js # src/gateway/static/dashboard/assets/DocsPage-D7XUEYdI.js # src/gateway/static/dashboard/assets/DocsPage-gss9QTdF.js # src/gateway/static/dashboard/assets/KeysPage-DZwI8ovn.js # src/gateway/static/dashboard/assets/ModelScopeControl-DY7OwwJM.js # src/gateway/static/dashboard/assets/ModelScopeControl-DedlZSDh.js # src/gateway/static/dashboard/assets/ModelScopeControl-L-_tKOtj.js # src/gateway/static/dashboard/assets/ModelsPage-BynwkYFh.js # src/gateway/static/dashboard/assets/ModelsPage-CAoQKgS_.js # src/gateway/static/dashboard/assets/ModelsPage-Cw8Xv_w2.js # src/gateway/static/dashboard/assets/SettingsPage-0IuzwARi.js # src/gateway/static/dashboard/assets/TablePagination-ByewydW5.js # src/gateway/static/dashboard/assets/TablePagination-Chra1w0V.js # src/gateway/static/dashboard/assets/TablePagination-Cz-ztZ68.js # src/gateway/static/dashboard/assets/ToolsGuardrailsPage-KUSgM6cX.js # src/gateway/static/dashboard/assets/UsersPage-DPrtdYVu.js # src/gateway/static/dashboard/assets/index-C6t-OIZc.css # src/gateway/static/dashboard/index.html # web/src/components/ui.test.tsx # web/src/components/ui.tsx # web/src/pages/OverviewPage.tsx # web/src/pages/UsagePage.tsx
CodeRabbit review finding on #445: the manual refresh handlers enumerated a subset of the queries derived from the current window, so in custom mode (where the query keys do not change) the model typeahead and the period-over-period baseline silently stayed on cached data after an explicit refresh. Usage now also refetches the model-suggestion summary and, when a comparison window exists, the previous-period summary; Activity also refetches its model-suggestion summary. The previous-period refetch is guarded because refetch() ignores `enabled` and would otherwise fire a pointless request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Description
Reworks the Usage and Activity time-range controls around a request-volume histogram, the pattern CloudWatch Logs Insights, Kibana, and Grafana/Loki use for log exploration. The selection lives on the chart itself: full-height handles at the window edges zoom (react-aria slider thumbs, pointer-captured, 0.1-bucket steps), a slim strip along the axis band pans, regions outside the window dim in place, and −/+ buttons step the zoom. Zooming out past the extent promotes to the next larger preset (30d to 90d to 12mo), and Reset restores the full extent. Presets set the extent the chart spans; the tiles, breakdowns, and log follow the selection.
This fixes the three defects from the issue:
datetime-localinputs) only filtered when both a date and a time were entered, and read local wall-clock times as UTC. The selection is now bucket-based and UTC-aligned, matching the summary endpoint's bucketing and the existing "times in UTC" captions.Other changes riding along, all part of the same control rework:
web/src/lib/timeRange.ts), so the labels can't drift apart again.touch-pan-ykeeps mobile scrolling intact.Verified end-to-end against a live gateway with seeded data (drag-zoom, pan, step zoom with preset promotion, tooltips, mobile layout, drill-through to Activity).
PR Type
Relevant issues
Fixes #440
Checklist
tests/unit,tests/integration).make lint,make typecheck,make test).uv run python scripts/generate_openapi.py).Notes: this is a dashboard-only change (no gateway API contract change, so no OpenAPI regeneration needed). Frontend checks run instead:
npm --prefix web run typecheck,npm --prefix web test(327 tests), and the committed bundle was rebuilt (npm --prefix web run build).AI Usage
AI Model/Tool used:
Claude Code (Opus 4.8 and Fable 5), directed by @njbrake
Any additional AI details you'd like to share:
Design direction (histogram-as-selector, overlay handles, preset promotion on zoom-out) was decided by @njbrake across several review rounds against the running dashboard.
🤖 Generated with Claude Code
Summary