Skip to content

feat(ui): shadcn charts foundation with tremor-compatible wrappers - #32668

Merged
ryan-crabbe-berri merged 7 commits into
litellm_internal_stagingfrom
litellm_shadcn_charts_foundation
Jul 10, 2026
Merged

feat(ui): shadcn charts foundation with tremor-compatible wrappers#32668
ryan-crabbe-berri merged 7 commits into
litellm_internal_stagingfrom
litellm_shadcn_charts_foundation

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

QA on the pilot page (screenshots to follow in a comment):

  1. run the proxy (python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml) and the dashboard dev server (npm run dev in ui/litellm-dashboard)
  2. send a few completions through a guardrail-enabled key so the overview has data, e.g. for i in 1 2 3; do curl -s http://localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-..." -H "Content-Type: application/json" -d '{"model": "gpt-5.2", "messages": [{"role": "user", "content": "hi"}]}'; done
  3. open http://localhost:3000/?page=guardrails-monitor and compare the "Request Outcomes Over Time" chart against staging: stacked green/red bars per day, y axis with locale-formatted counts, legend on top, tooltip on hover showing passed/blocked counts
  4. with no data in the selected period the card still shows "No chart data for this period"

Before (staging, tremor BarChart) and after (this branch, recharts through the new wrapper) should look near-identical; colors map to the same green-500/red-500 the tremor palette used

before
Screenshot 2026-07-09 at 5 48 31 PM

after
Screenshot 2026-07-09 at 5 48 20 PM

Type

🆕 New Feature
🧹 Refactoring

Changes

Foundation PR for the charts track of the shadcn migration (tremor -> shadcn/recharts). Every chart in the app is a tremor chart today (36 render sites across 15 files) and tremor is what blocks the React 19 upgrade. This PR builds the plumbing once so each later page conversion is a small mechanical prop swap, and proves it by converting the smallest page (ScoreChart on the guardrails monitor)

New pieces:

recharts@3.9.2 becomes a direct pinned dependency (it previously existed only transitively inside tremor, at v2). components/ui/chart.tsx and components/ui/card.tsx are added via npx shadcn add chart, with our standard tweaks: React 18 forwardRef conversion (the setupTests tripwire enforces this) plus entries in ref-forwarding.test.tsx

components/shared/charts/ holds thin wrappers (BarChart, AreaChart, DonutChart) that expose the tremor prop surface used by the 34 Bar/Area/Donut call sites: data/index/categories/colors/valueFormatter/stack/layout/yAxisWidth/tickGap/showLegend/showXAxis/showGridLines/showTooltip/customTooltip/onValueChange. Defaults match tremor's, including legends on, so a converted site that omits a prop keeps its behavior. The two remaining render sites are deliberately not covered here: the app's single LineChart (EndpointUsage) gets a Line wrapper added in that page's conversion stage, and the single BarList (old usage page) is not a recharts chart at all and gets a small bespoke ranked-bar component in stage 8. The one site passing a title prop (cache_dashboard) moves that string into a CardTitle when its page converts. Tremor color names ("cyan", "green", ...) resolve to the same tailwind-500 palette tremor rendered, via var(--color-<name>-500, <hex>) so future theming can re-map them. The color cycle for sites that pass no colors matches tremor's themeColorRange order. shadcn's docs favor hand-composing recharts per page; with 24 near-identical BarCharts that means re-hand-rolling the same axis/tooltip/legend setup 24 times and visual drift, so we follow the wrapper-on-top-of-chart.tsx pattern production apps converged on (langfuse's chart-library is the closest analog). Bespoke charts can still drop down to raw ChartContainer

Wrapper ChartConfig entries carry labels only. Series colors flow to each Bar/Area/Cell fill directly, so ChartStyle's per-chart CSS-variable style block is never emitted for wrapper charts; a test pins that. The vendored ChartStyle is hardened anyway (CSS var names sanitized to a safe charset, structural characters stripped from values) so a future bespoke chart using the standard shadcn var(--color-<key>) pattern cannot emit malformed or block-escaping CSS from keys like metrics.total_tokens (upstream shadcn-ui/ui#6172)

CustomTooltip/CustomLegend from common_components/chartUtils.tsx are ported off tremor types onto the new module; the tremor originals stay until their last consumer converts. ValueTooltip is the default tooltip and applies each chart's valueFormatter

ScoreChart (guardrails monitor) converts as the pilot: tremor Card/Title/BarChart replaced by shadcn Card and the new BarChart wrapper, with its existing test upgraded from a mocked tremor BarChart to asserting against the real recharts render

Test infrastructure: the global jsdom ResizeObserver mock was a no-op, and recharts' ResponsiveContainer renders nothing until it observes a positive size (jsdom getBoundingClientRect is always 0x0). The mock now reports an 800x400 box on observe, but only for observers inside a shadcn ChartContainer subtree ([data-slot="chart"]); firing for every observer made tremor charts render in jsdom for the first time, which duplicated getByText targets in six existing tests and destabilized a headlessui popover click in another. Scoped this way, all shadcn chart tests, including every later page conversion PR's tests, render real recharts SVG while legacy tremor consumers keep the exact semantics their tests were written against. The scoping can be dropped when tremor is gone

Adds recharts 3.9.2 as a direct pinned dependency, the shadcn chart.tsx
and card.tsx primitives (converted to React 18 forwardRef), and thin
Bar/Area/Donut wrappers in components/shared/charts that expose the
tremor chart prop surface the app actually uses, so each page conversion
becomes a mechanical import swap. Ports CustomTooltip/CustomLegend off
tremor types. Converts ScoreChart (guardrails monitor) as the pilot.

The jsdom ResizeObserver mock now fires an initial 800x400 notification
like a real browser, since recharts ResponsiveContainer renders nothing
until it observes a size.
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR establishes the recharts/shadcn foundation for migrating the dashboard's 36 tremor chart render sites away from tremor (which blocks the React 19 upgrade), and validates the pattern by converting the ScoreChart on the guardrails monitor page as a pilot.

  • New chart primitivescomponents/ui/chart.tsx (vendored shadcn ChartContainer/ChartStyle with CSS key sanitization via cssVarName and color value sanitization via cssColorValue) and thin BarChart/AreaChart/DonutChart wrappers in components/shared/charts/ that expose the same prop surface tremor used, so future conversions are mechanical prop swaps.
  • Test infrastructure — The global ResizeObserver mock is scoped to [data-slot="chart"] subtrees so shadcn chart tests render real recharts SVG in jsdom while existing tremor/headlessui tests continue to see the old no-op; ScoreChart.test.tsx is upgraded from a mocked tremor stub to asserting against real recharts DOM (bars, legend labels, axis tick text).

Confidence Score: 5/5

Safe to merge — this is an additive infrastructure change with no modifications to server-side code, and the single converted page (ScoreChart) is a straightforward visual swap.

All changed files are frontend-only chart primitives and test plumbing. The CSS sanitization is tested against structural-character injection. The scoped ResizeObserver mock is carefully documented and does not disturb existing test suites. No auth, data, or API paths are touched.

No files require special attention.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/components/ui/chart.tsx Vendored shadcn ChartContainer/ChartStyle/ChartTooltipContent with CSS sanitization: cssVarName strips non-alphanumeric chars from config keys, cssColorValue strips structural chars from color values; ChartStyle returns null when no entry has a color
ui/litellm-dashboard/src/components/shared/charts/bar_chart.tsx Tremor-compatible BarChart wrapper: exposes matching prop surface, fills flow directly to Bar elements (no CSS vars emitted), onValueChange maps recharts click payload to tremor-style { ...datum, categoryClicked } interface
ui/litellm-dashboard/src/components/shared/charts/area_chart.tsx Tremor-compatible AreaChart wrapper with per-category linear gradients; gradientId uses React.useId() with colon removal for SVG-safe IDs; showXAxis intentionally omitted (covers only props used by existing call sites)
ui/litellm-dashboard/tests/setupTests.ts ResizeObserver mock scoped to [data-slot=chart] subtrees: fires 800x400 synthetic entry synchronously for recharts ResponsiveContainer, no-op for all other observers to prevent tremor/headlessui test interference
ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/components/ScoreChart.tsx Pilot conversion: tremor Card/Title/BarChart replaced with shadcn Card primitives and new BarChart wrapper; props map 1:1
ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/components/ScoreChart.test.tsx Test migrated from mocked tremor BarChart to real recharts SVG render; date axis assertions preserved as getAllByText; category legend labels and bar count asserted against real DOM

Reviews (3): Last reviewed commit: "fix(ui): match tremor title size, card p..." | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_shadcn_charts_foundation (1dbd6a8) with litellm_internal_staging (65d90fd)

Open in CodSpeed

Restores the data-flow assertions the tremor-mock test had; the bar
series count alone stays 2 even if the index wiring breaks, while the
x-axis tick labels only appear when the data reaches the axis.
Firing an initial notification for every observer made tremor charts
render in jsdom for the first time, duplicating getByText targets in
six tests and destabilizing a headlessui popover click in another.
Report the 800x400 box only for observers inside [data-slot=chart] so
recharts renders while every legacy consumer keeps no-op semantics.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

…end default

Wrapper ChartConfig entries now carry labels only; series colors flow
to each Bar/Area/Cell fill directly, so ChartStyle emits no per-chart
style block for wrapper charts. The vendored ChartStyle additionally
sanitizes var names and strips structural characters from values so a
future bespoke chart using the standard shadcn var pattern cannot
reintroduce malformed or escaping CSS. showLegend now defaults to true,
matching tremor, so converted call sites that omit the prop keep their
legends.
shadcn ChartLegendContent centers items by default; tremor rendered its
legend top-right.
Card size=sm made its text-sm variant beat the CardTitle text-base
override and shrank padding to 16px; tremor rendered 16px titles in
24px cards. Legend labels inherited the near-black foreground where
tremor used gray-500; ChartLegendContent now gets text-muted-foreground
from the wrappers.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

…itellm_shadcn_charts_foundation

# Conflicts:
#	ui/litellm-dashboard/eslint-metrics.json
#	ui/litellm-dashboard/eslint-suppressions.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants