Skip to content

Commit 5156bf0

Browse files
authored
Merge pull request #36915 from BerriAI/litellm_shadcn_settings_0814
refactor(ui): migrate router settings and shared badges off antd and tremor
2 parents e1f3d6e + 30c1b97 commit 5156bf0

8 files changed

Lines changed: 305 additions & 212 deletions

File tree

ui/litellm-dashboard/eslint-suppressions.json

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,9 +1393,6 @@
13931393
"no-nested-ternary": {
13941394
"count": 1
13951395
},
1396-
"no-restricted-imports": {
1397-
"count": 2
1398-
},
13991396
"prefer-const": {
14001397
"count": 2
14011398
}
@@ -1721,11 +1718,6 @@
17211718
"count": 1
17221719
}
17231720
},
1724-
"src/components/BetaBadge.tsx": {
1725-
"no-restricted-imports": {
1726-
"count": 1
1727-
}
1728-
},
17291721
"src/components/CloudZeroCostTracking/CloudZeroCreateModal.tsx": {
17301722
"no-restricted-imports": {
17311723
"count": 1
@@ -1744,11 +1736,6 @@
17441736
"count": 1
17451737
}
17461738
},
1747-
"src/components/DeprecationBanner.tsx": {
1748-
"no-restricted-imports": {
1749-
"count": 1
1750-
}
1751-
},
17521739
"src/components/EntityUsageExport/ExportSummary.tsx": {
17531740
"no-restricted-imports": {
17541741
"count": 1
@@ -1917,11 +1904,6 @@
19171904
"count": 1
19181905
}
19191906
},
1920-
"src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx": {
1921-
"no-restricted-imports": {
1922-
"count": 1
1923-
}
1924-
},
19251907
"src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx": {
19261908
"local/no-complex-jsx-arrow": {
19271909
"count": 1
@@ -1939,9 +1921,6 @@
19391921
}
19401922
},
19411923
"src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx": {
1942-
"no-restricted-imports": {
1943-
"count": 2
1944-
},
19451924
"prefer-const": {
19461925
"count": 2
19471926
}

ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ const settingsRow = async (fieldName: string) => {
6262
return row as HTMLElement;
6363
};
6464

65+
const numericValueIn = (row: HTMLElement) => Number((within(row).getByRole("spinbutton") as HTMLInputElement).value);
66+
6567
describe("GeneralSettings General tab", () => {
6668
beforeEach(() => {
6769
vi.mocked(getGeneralSettingsCall).mockResolvedValue([...SETTINGS_FIXTURE.map((s) => ({ ...s }))]);
@@ -87,15 +89,15 @@ describe("GeneralSettings General tab", () => {
8789

8890
await user.click(screen.getByText("General"));
8991
const row = await settingsRow("max_ui_session_budget");
90-
expect(within(row).getByRole("spinbutton")).toHaveValue("7.50");
92+
expect(numericValueIn(row)).toBe(7.5);
9193

9294
const actionCell = row.querySelectorAll("td")[3];
9395
const resetIcon = actionCell.querySelector("svg");
9496
expect(resetIcon).not.toBeNull();
9597
await user.click(resetIcon as unknown as Element);
9698

9799
expect(deleteConfigFieldSetting).toHaveBeenCalledWith("token", "max_ui_session_budget");
98-
expect(within(row).getByRole("spinbutton")).toHaveValue("1.00");
100+
expect(numericValueIn(row)).toBe(1);
99101
});
100102
});
101103

ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx

Lines changed: 136 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
11
import React, { useState, useEffect } from "react";
2-
import {
3-
Card,
4-
Table,
5-
TableHead,
6-
TableRow,
7-
TableHeaderCell,
8-
TableCell,
9-
TableBody,
10-
Title,
11-
Text,
12-
Button,
13-
Icon,
14-
Switch,
15-
} from "@tremor/react";
2+
import { Button } from "@/components/ui/button";
3+
import { Card, CardContent, CardTitle } from "@/components/ui/card";
4+
import { Input } from "@/components/ui/input";
5+
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
6+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
7+
import { Switch } from "@/components/ui/switch";
8+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
169
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
1710
import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "@/components/networking";
18-
import { InputNumber, Select as AntdSelect } from "antd";
19-
import { TrashIcon } from "@heroicons/react/outline";
11+
import { Trash2 } from "lucide-react";
2012
import { StatusBadge } from "@/components/shared/table_cells";
2113

2214
import RouterSettings from "@/components/router_settings";
@@ -44,59 +36,78 @@ export interface generalSettingsItem {
4436
field_default_value?: any;
4537
}
4638

39+
const NUMERIC_INPUT_WIDTH = "w-36";
40+
41+
const toNumericValue = (raw: string): number | null => (raw === "" ? null : Number(raw));
42+
4743
const SettingValueEditor: React.FC<{
4844
setting: generalSettingsItem;
4945
onChange: (fieldName: string, newValue: any) => void;
5046
}> = ({ setting, onChange }) => {
5147
if (setting.field_type === "Integer") {
5248
return (
53-
<InputNumber
49+
<Input
50+
type="number"
5451
step={1}
55-
value={setting.field_value}
56-
onChange={(newValue) => onChange(setting.field_name, newValue)}
52+
className={NUMERIC_INPUT_WIDTH}
53+
value={setting.field_value ?? ""}
54+
onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))}
5755
/>
5856
);
5957
}
6058
if (setting.field_type === "Boolean") {
6159
return (
6260
<Switch
6361
checked={setting.field_value === true || setting.field_value === "true"}
64-
onChange={(checked) => onChange(setting.field_name, checked)}
62+
onCheckedChange={(checked) => onChange(setting.field_name, checked)}
6563
/>
6664
);
6765
}
6866
if (setting.field_type === "Float") {
6967
return (
70-
<InputNumber
68+
<Input
69+
type="number"
7170
min={0}
7271
max={1}
7372
step={0.05}
74-
value={setting.field_value}
75-
onChange={(newValue) => onChange(setting.field_name, newValue)}
73+
className={NUMERIC_INPUT_WIDTH}
74+
value={setting.field_value ?? ""}
75+
onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))}
7676
/>
7777
);
7878
}
7979
if (setting.field_type === "Dollar") {
8080
return (
81-
<InputNumber
82-
min={0.01}
83-
step={0.25}
84-
prefix="$"
85-
value={setting.field_value}
86-
onChange={(newValue) => onChange(setting.field_name, newValue)}
87-
/>
81+
<InputGroup className={NUMERIC_INPUT_WIDTH}>
82+
<InputGroupAddon>$</InputGroupAddon>
83+
<InputGroupInput
84+
type="number"
85+
min={0.01}
86+
step={0.25}
87+
value={setting.field_value ?? ""}
88+
onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))}
89+
/>
90+
</InputGroup>
8891
);
8992
}
9093
if (setting.field_type === "Select") {
9194
return (
92-
<AntdSelect
93-
allowClear
94-
style={{ minWidth: "8rem" }}
95-
placeholder="Default"
96-
value={setting.field_value || undefined}
97-
options={(setting.field_options ?? []).map((option) => ({ label: option, value: option }))}
98-
onChange={(newValue) => onChange(setting.field_name, newValue ?? "")}
99-
/>
95+
<Select
96+
value={setting.field_value || null}
97+
onValueChange={(newValue) => onChange(setting.field_name, newValue ?? "")}
98+
>
99+
<SelectTrigger className="min-w-32">
100+
<SelectValue placeholder="Default" />
101+
</SelectTrigger>
102+
<SelectContent>
103+
<SelectItem value={null}>Default</SelectItem>
104+
{(setting.field_options ?? []).map((option) => (
105+
<SelectItem key={option} value={option}>
106+
{option}
107+
</SelectItem>
108+
))}
109+
</SelectContent>
110+
</Select>
100111
);
101112
}
102113
return null;
@@ -131,33 +142,43 @@ export const PromptCachingPanel: React.FC<{
131142

132143
return (
133144
<Card>
134-
<Title>Prompt Caching</Title>
145+
<CardContent>
146+
<CardTitle>Prompt Caching</CardTitle>
135147

136-
<div className="mt-6 flex items-start justify-between gap-8">
137-
<div className="max-w-2xl">
138-
<Text className="font-medium">Automatic Anthropic prompt caching</Text>
139-
<p className="mt-1 text-xs text-gray-500">{enableSetting.field_description}</p>
140-
</div>
141-
<Switch checked={enabled} onChange={(checked) => persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} />
142-
</div>
143-
144-
{ttlSetting && (
145148
<div className="mt-6 flex items-start justify-between gap-8">
146-
<div className="max-w-2xl">
147-
<Text className={`font-medium ${enabled ? "" : "text-gray-400"}`}>Cache lifetime (TTL)</Text>
148-
<p className="mt-1 text-xs text-gray-500">{ttlSetting.field_description}</p>
149+
<div className="min-w-0 max-w-2xl">
150+
<p className="font-medium">Automatic Anthropic prompt caching</p>
151+
<p className="mt-1 break-words text-xs text-gray-500">{enableSetting.field_description}</p>
149152
</div>
150-
<AntdSelect
151-
allowClear
152-
disabled={!enabled}
153-
style={{ minWidth: "10rem" }}
154-
placeholder="5m (default)"
155-
value={ttlSetting.field_value || undefined}
156-
options={(ttlSetting.field_options ?? []).map((option) => ({ label: option, value: option }))}
157-
onChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")}
158-
/>
153+
<Switch checked={enabled} onCheckedChange={(checked) => persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} />
159154
</div>
160-
)}
155+
156+
{ttlSetting && (
157+
<div className="mt-6 flex items-start justify-between gap-8">
158+
<div className="min-w-0 max-w-2xl">
159+
<p className={`font-medium ${enabled ? "" : "text-gray-400"}`}>Cache lifetime (TTL)</p>
160+
<p className="mt-1 break-words text-xs text-gray-500">{ttlSetting.field_description}</p>
161+
</div>
162+
<Select
163+
disabled={!enabled}
164+
value={ttlSetting.field_value || null}
165+
onValueChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")}
166+
>
167+
<SelectTrigger className="min-w-40">
168+
<SelectValue placeholder="5m (default)" />
169+
</SelectTrigger>
170+
<SelectContent>
171+
<SelectItem value={null}>5m (default)</SelectItem>
172+
{(ttlSetting.field_options ?? []).map((option) => (
173+
<SelectItem key={option} value={option}>
174+
{option}
175+
</SelectItem>
176+
))}
177+
</SelectContent>
178+
</Select>
179+
</div>
180+
)}
181+
</CardContent>
161182
</Card>
162183
);
163184
};
@@ -254,55 +275,60 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
254275
</TabsContent>
255276
<TabsContent value="general" className="px-8 py-6">
256277
<Card>
257-
<Table>
258-
<TableHead>
259-
<TableRow>
260-
<TableHeaderCell>Setting</TableHeaderCell>
261-
<TableHeaderCell>Value</TableHeaderCell>
262-
<TableHeaderCell>Status</TableHeaderCell>
263-
<TableHeaderCell>Action</TableHeaderCell>
264-
</TableRow>
265-
</TableHead>
266-
<TableBody>
267-
{generalSettings
268-
.filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB)
269-
.map((value, index) => (
270-
<TableRow key={index}>
271-
<TableCell>
272-
<Text>{value.field_name}</Text>
273-
<p
274-
style={{
275-
fontSize: "0.65rem",
276-
color: "#808080",
277-
fontStyle: "italic",
278-
}}
279-
className="mt-1"
280-
>
281-
{value.field_description}
282-
</p>
283-
</TableCell>
284-
<TableCell>
285-
<SettingValueEditor setting={value} onChange={handleInputChange} />
286-
</TableCell>
287-
<TableCell>
288-
{value.stored_in_db == true ? (
289-
<StatusBadge tone="success" label="In DB" />
290-
) : value.stored_in_db == false ? (
291-
<StatusBadge tone="neutral" label="In Config" />
292-
) : (
293-
<StatusBadge tone="neutral" label="Not Set" />
294-
)}
295-
</TableCell>
296-
<TableCell>
297-
<Button onClick={() => handleUpdateField(value.field_name)}>Update</Button>
298-
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name)}>
299-
Reset
300-
</Icon>
301-
</TableCell>
302-
</TableRow>
303-
))}
304-
</TableBody>
305-
</Table>
278+
<CardContent>
279+
<Table>
280+
<TableHeader>
281+
<TableRow>
282+
<TableHead>Setting</TableHead>
283+
<TableHead>Value</TableHead>
284+
<TableHead>Status</TableHead>
285+
<TableHead>Action</TableHead>
286+
</TableRow>
287+
</TableHeader>
288+
<TableBody>
289+
{generalSettings
290+
.filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB)
291+
.map((value, index) => (
292+
<TableRow key={index}>
293+
<TableCell className="whitespace-normal">
294+
<p className="break-words">{value.field_name}</p>
295+
<p
296+
style={{
297+
fontSize: "0.65rem",
298+
color: "#808080",
299+
fontStyle: "italic",
300+
}}
301+
className="mt-1 break-words"
302+
>
303+
{value.field_description}
304+
</p>
305+
</TableCell>
306+
<TableCell>
307+
<SettingValueEditor setting={value} onChange={handleInputChange} />
308+
</TableCell>
309+
<TableCell>
310+
{value.stored_in_db == true ? (
311+
<StatusBadge tone="success" label="In DB" />
312+
) : value.stored_in_db == false ? (
313+
<StatusBadge tone="neutral" label="In Config" />
314+
) : (
315+
<StatusBadge tone="neutral" label="Not Set" />
316+
)}
317+
</TableCell>
318+
<TableCell>
319+
<Button onClick={() => handleUpdateField(value.field_name)}>Update</Button>
320+
<span
321+
onClick={() => handleResetField(value.field_name)}
322+
className="inline-flex shrink-0 cursor-pointer items-center justify-center px-1.5 py-1.5 text-red-500"
323+
>
324+
<Trash2 className="h-5 w-5 shrink-0" />
325+
</span>
326+
</TableCell>
327+
</TableRow>
328+
))}
329+
</TableBody>
330+
</Table>
331+
</CardContent>
306332
</Card>
307333
</TabsContent>
308334
</Tabs>

0 commit comments

Comments
 (0)