feat(ui): shadcn charts foundation with tremor-compatible wrappers - #32668
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis 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.
Confidence Score: 5/5Safe 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.
|
| 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
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.
|
@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.
|
@greptileai re review |
…itellm_shadcn_charts_foundation # Conflicts: # ui/litellm-dashboard/eslint-metrics.json # ui/litellm-dashboard/eslint-suppressions.json
592510e
into
litellm_internal_staging
Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito 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):
python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml) and the dashboard dev server (npm run devin ui/litellm-dashboard)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"}]}'; doneBefore (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

after

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.2becomes a direct pinned dependency (it previously existed only transitively inside tremor, at v2).components/ui/chart.tsxandcomponents/ui/card.tsxare added vianpx shadcn add chart, with our standard tweaks: React 18forwardRefconversion (the setupTests tripwire enforces this) plus entries in ref-forwarding.test.tsxcomponents/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 atitleprop (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, viavar(--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 rawChartContainerWrapper 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 likemetrics.total_tokens(upstream shadcn-ui/ui#6172)CustomTooltip/CustomLegendfrom common_components/chartUtils.tsx are ported off tremor types onto the new module; the tremor originals stay until their last consumer converts.ValueTooltipis the default tooltip and applies each chart'svalueFormatterScoreChart (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