mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
refactor(ui): migrate guardrail and duration controls to shadcn (#36693)
* test(ui): characterize shared migration surfaces * refactor(ui): migrate guardrail and duration controls * fix(ui): preserve duration select callback shape * fix(ui): narrow duration selection value
This commit is contained in:
parent
e619106306
commit
4df421e058
5 changed files with 87 additions and 45 deletions
|
|
@ -2055,9 +2055,6 @@
|
|||
"src/components/GuardrailSettingsView.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/GuardrailsMonitor/LogViewer.tsx": {
|
||||
|
|
@ -2649,11 +2646,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/common_components/DurationSelect.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/common_components/Filters/FilterInput.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { renderWithProviders, screen } from "../../tests/test-utils";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import GuardrailSettingsView from "./GuardrailSettingsView";
|
||||
|
||||
describe("GuardrailSettingsView", () => {
|
||||
it("should render", () => {
|
||||
renderWithProviders(<GuardrailSettingsView globalGuardrailNames={new Set()} />);
|
||||
|
||||
expect(screen.getByText("Guardrails Settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should separate active global and team-specific guardrails", () => {
|
||||
renderWithProviders(
|
||||
<GuardrailSettingsView
|
||||
globalGuardrailNames={new Set(["global-one", "global-two"])}
|
||||
teamGuardrails={["global-one", "team-one"]}
|
||||
optedOutGlobalGuardrails={["global-two"]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("global-one")).toBeInTheDocument();
|
||||
expect(screen.getByText("team-one")).toBeInTheDocument();
|
||||
expect(screen.queryByText("global-two")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show when global guardrails are bypassed", () => {
|
||||
renderWithProviders(<GuardrailSettingsView globalGuardrailNames={new Set(["global-one"])} killSwitchOn />);
|
||||
|
||||
expect(screen.getByText("Bypassed for this team")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import React from "react";
|
||||
import { Tag } from "antd";
|
||||
import { GlobalOutlined } from "@ant-design/icons";
|
||||
import { Globe2 } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
|
||||
interface GuardrailSettingsViewProps {
|
||||
globalGuardrailNames: Set<string>;
|
||||
|
|
@ -26,40 +28,36 @@ export function GuardrailSettingsView({
|
|||
const isEmpty = !killSwitchOn && globalsRunning.length === 0 && nonGlobalOptIns.length === 0;
|
||||
|
||||
const content = isEmpty ? (
|
||||
<span className="block text-gray-500">No guardrails configured</span>
|
||||
<span className="block text-muted-foreground">No guardrails configured</span>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<span className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<GlobalOutlined style={{ marginInlineEnd: 4 }} aria-label="Global guardrail" />
|
||||
<span className="mb-2 flex items-center gap-1 text-sm font-medium text-foreground">
|
||||
<Globe2 className="size-4" aria-label="Global guardrail" />
|
||||
Global
|
||||
</span>
|
||||
{killSwitchOn ? (
|
||||
<Tag color="gold">Bypassed for this team</Tag>
|
||||
<Badge variant="outline">Bypassed for this team</Badge>
|
||||
) : globalsRunning.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{globalsRunning.map((name) => (
|
||||
<Tag key={name} color="blue">
|
||||
{name}
|
||||
</Tag>
|
||||
<Badge key={name}>{name}</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="block text-sm text-gray-500">None configured</span>
|
||||
<span className="block text-sm text-muted-foreground">None configured</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-sm font-medium text-gray-700 mb-2">Team-specific</span>
|
||||
<span className="mb-2 block text-sm font-medium text-foreground">Team-specific</span>
|
||||
{nonGlobalOptIns.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{nonGlobalOptIns.map((name) => (
|
||||
<Tag key={name} color="blue">
|
||||
{name}
|
||||
</Tag>
|
||||
<Badge key={name}>{name}</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="block text-sm text-gray-500">None configured</span>
|
||||
<span className="block text-sm text-muted-foreground">None configured</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -67,23 +65,19 @@ export function GuardrailSettingsView({
|
|||
|
||||
if (variant === "card") {
|
||||
return (
|
||||
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<div>
|
||||
<span className="block font-semibold text-gray-900">Guardrails Settings</span>
|
||||
<span className="block text-xs text-gray-500">
|
||||
Global and team-specific guardrails applied to this team
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Guardrails Settings</CardTitle>
|
||||
<CardDescription>Global and team-specific guardrails applied to this team</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>{content}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<span className="block font-medium text-gray-900 mb-3">Guardrails Settings</span>
|
||||
<div className={cn(className)}>
|
||||
<span className="mb-3 block font-medium text-foreground">Guardrails Settings</span>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import userEvent, { PointerEventsCheckLevel } from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import DurationSelect from "./DurationSelect";
|
||||
|
||||
|
|
@ -19,6 +19,9 @@ describe("DurationSelect", () => {
|
|||
expect(screen.getByText("Daily")).toBeInTheDocument();
|
||||
expect(screen.getByText("Weekly")).toBeInTheDocument();
|
||||
expect(screen.getByText("Monthly")).toBeInTheDocument();
|
||||
const dailyLabel = screen.getByText("Daily");
|
||||
const dailyOption = dailyLabel.closest('[role="option"]') ?? dailyLabel;
|
||||
await user.click(dailyOption);
|
||||
});
|
||||
|
||||
it("should apply className prop", () => {
|
||||
|
|
@ -28,14 +31,15 @@ describe("DurationSelect", () => {
|
|||
});
|
||||
|
||||
it("should call onChange when an option is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ pointerEventsCheck: PointerEventsCheckLevel.Never });
|
||||
const onChange = vi.fn();
|
||||
render(<DurationSelect onChange={onChange} />);
|
||||
|
||||
const select = screen.getByRole("combobox");
|
||||
await user.click(select);
|
||||
|
||||
const dailyOption = screen.getByText("Daily");
|
||||
const dailyLabel = screen.getByText("Daily");
|
||||
const dailyOption = dailyLabel.closest('[role="option"]') ?? dailyLabel;
|
||||
await user.click(dailyOption);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("24h", expect.any(Object));
|
||||
|
|
|
|||
|
|
@ -1,17 +1,38 @@
|
|||
import { Select } from "antd";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
interface DurationSelectProps {
|
||||
className?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onChange?: (value: string, option: { value: string; label: string }) => void;
|
||||
}
|
||||
|
||||
const DURATION_OPTIONS = [
|
||||
{ value: "24h", label: "Daily" },
|
||||
{ value: "7d", label: "Weekly" },
|
||||
{ value: "30d", label: "Monthly" },
|
||||
];
|
||||
|
||||
export default function DurationSelect({ className, value, onChange }: DurationSelectProps) {
|
||||
return (
|
||||
<Select className={className} value={value} onChange={onChange}>
|
||||
<Select.Option value="24h">Daily</Select.Option>
|
||||
<Select.Option value="7d">Weekly</Select.Option>
|
||||
<Select.Option value="30d">Monthly</Select.Option>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(nextValue) => {
|
||||
const selectedOption = DURATION_OPTIONS.find((option) => option.value === nextValue);
|
||||
if (selectedOption) {
|
||||
onChange?.(selectedOption.value, selectedOption);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className={className}>
|
||||
<SelectValue placeholder="Select duration" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DURATION_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue