mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(ui): show select labels on the trigger instead of raw values
Base UI's Select.Value resolves an option's label only when the root carries an `items` prop or the Value has a child. `resolveSelectedLabel` in @base-ui/react/internals/resolveValueLabel.js falls through every branch to `stringifyAsLabel(value)` otherwise, and `state.items` is written only from the root's `items` prop, so the `<SelectItem>` children rendered inside `<SelectContent>` never populate it. A self-closing `<SelectValue />` on a root without `items` therefore renders the raw value once something is selected. The placeholder branch still works, so the trigger looked right until the user picked an option and then showed `development` for Development, `LiteLLM_VerificationToken` for Keys, `all` for All Actions, and `24h` for Daily. Pass `items` at the 20 affected sites, using the array form the other 52 call sites already use. Where a literal option sat alongside mapped ones, build one array and map the options over it so the labels and `items` cannot drift. The record-map form is avoided deliberately: `items[value]` on an object literal reaches Object.prototype, so a dynamic value named `toString` would resolve to a function and React would throw on it. The array form matches with `.find` and has no prototype lookup, which matters where the values are user-supplied model groups, team ids and key aliases. Also replace the option lookup in CompetitorIntentConfiguration's test helper, which searched by text and clicked the last match. That match is now ambiguous because the trigger carries the label too, and the helper already flaked roughly one run in six before this change.
This commit is contained in:
parent
0b82b087fd
commit
4cbceb565b
27 changed files with 452 additions and 41 deletions
|
|
@ -145,6 +145,19 @@ describe("TeamGuardrailsTab submit payload", () => {
|
|||
expect(registeredPayload().litellm_params.mode).toBe("during_call");
|
||||
});
|
||||
|
||||
it("shows the mode by its human label on the trigger", async () => {
|
||||
const user = userEvent.setup();
|
||||
await openSubmitModal(user);
|
||||
|
||||
const mode = screen.getAllByRole("combobox")[1];
|
||||
expect(mode).toHaveTextContent("Pre Call");
|
||||
|
||||
await user.click(mode);
|
||||
await user.click(await screen.findByRole("option", { name: "During Call" }));
|
||||
|
||||
expect(screen.getAllByRole("combobox")[1]).toHaveTextContent("During Call");
|
||||
});
|
||||
|
||||
it("blocks an empty submit and reports every required field", async () => {
|
||||
const user = userEvent.setup();
|
||||
await openSubmitModal(user);
|
||||
|
|
|
|||
|
|
@ -1090,7 +1090,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
</FormField>
|
||||
<FormField control={submitForm.control} name="mode" label="Mode">
|
||||
{({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => (
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<Select items={GUARDRAIL_MODES} value={value} onValueChange={onChange}>
|
||||
<SelectTrigger
|
||||
id={id}
|
||||
aria-invalid={ariaInvalid}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,7 @@ const lastConfig = (): CompetitorIntentConfig => onChange.mock.calls[onChange.mo
|
|||
|
||||
const chooseOption = async (user: ReturnType<typeof userEvent.setup>, index: number, optionText: string) => {
|
||||
await user.click(screen.getAllByRole("combobox")[index]);
|
||||
const options = await screen.findAllByText(optionText);
|
||||
await user.click(options[options.length - 1]);
|
||||
await user.click(await screen.findByRole("option", { name: optionText }));
|
||||
};
|
||||
|
||||
describe("CompetitorIntentConfiguration reported config", () => {
|
||||
|
|
@ -175,4 +174,24 @@ describe("CompetitorIntentConfiguration reported config", () => {
|
|||
expect(screen.queryAllByRole("combobox")).toHaveLength(0);
|
||||
expect(screen.queryAllByRole("spinbutton")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Type", "Airline (auto-load competitors from IATA)"],
|
||||
["Policy: Competitor comparison", "Refuse (block request)"],
|
||||
["Policy: Possible competitor comparison", "Reframe (suggest alternative to backend LLM)"],
|
||||
])("shows the human label on the %s trigger", (name, label) => {
|
||||
render(<Harness />);
|
||||
|
||||
expect(screen.getByRole("combobox", { name })).toHaveTextContent(label);
|
||||
});
|
||||
|
||||
it("shows the human label on the Type trigger after switching to generic", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness />);
|
||||
|
||||
await user.click(screen.getByRole("combobox", { name: "Type" }));
|
||||
await user.click(await screen.findByRole("option", { name: "Generic (specify competitors manually)" }));
|
||||
|
||||
expect(screen.getByRole("combobox", { name: "Type" })).toHaveTextContent("Generic (specify competitors manually)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
<Field>
|
||||
<FieldLabel htmlFor={`${fieldId}-type`}>Type</FieldLabel>
|
||||
<Select
|
||||
items={INTENT_TYPES}
|
||||
value={effectiveConfig.competitor_intent_type}
|
||||
onValueChange={(v: string | null) => v !== null && handleConfigChange("competitor_intent_type", v)}
|
||||
>
|
||||
|
|
@ -260,6 +261,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
<Field>
|
||||
<FieldLabel htmlFor={`${fieldId}-competitor-comparison`}>Policy: Competitor comparison</FieldLabel>
|
||||
<Select
|
||||
items={COMPETITOR_COMPARISON_POLICIES}
|
||||
value={effectiveConfig.policy?.competitor_comparison ?? "refuse"}
|
||||
onValueChange={(v: string | null) => v !== null && handlePolicyChange("competitor_comparison", v)}
|
||||
>
|
||||
|
|
@ -281,6 +283,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
Policy: Possible competitor comparison
|
||||
</FieldLabel>
|
||||
<Select
|
||||
items={POSSIBLE_COMPETITOR_COMPARISON_POLICIES}
|
||||
value={effectiveConfig.policy?.possible_competitor_comparison ?? "reframe"}
|
||||
onValueChange={(v: string | null) =>
|
||||
v !== null && handlePolicyChange("possible_competitor_comparison", v)
|
||||
|
|
|
|||
|
|
@ -301,4 +301,16 @@ describe("ModelRetrySettingsTab", () => {
|
|||
const result = updater({ "gpt-4": { BadRequestErrorRetries: 0 } });
|
||||
expect(result["gpt-4"]).toMatchObject({ BadRequestErrorRetries: 2 });
|
||||
});
|
||||
|
||||
it("shows the global scope by its human label rather than the raw value", () => {
|
||||
render(<ModelRetrySettingsTab {...buildProps()} />);
|
||||
|
||||
expect(screen.getByRole("combobox")).toHaveTextContent("Global Default");
|
||||
});
|
||||
|
||||
it("shows a selected model group by its own name", () => {
|
||||
render(<ModelRetrySettingsTab {...buildProps({ selectedModelGroup: "gpt-4" })} />);
|
||||
|
||||
expect(screen.getByRole("combobox")).toHaveTextContent("gpt-4");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ const ModelRetrySettingsTab = ({
|
|||
isSaving = false,
|
||||
}: ModelRetrySettingsTabProps) => {
|
||||
const isGlobalScope = selectedModelGroup === "global";
|
||||
const scopeItems = [
|
||||
{ value: "global", label: "Global Default" },
|
||||
...availableModelGroups.map((group) => ({ value: group, label: group })),
|
||||
];
|
||||
|
||||
const setGlobalValue = (retryPolicyKey: string, value: number | null) => {
|
||||
if (value == null) return;
|
||||
|
|
@ -82,6 +86,7 @@ const ModelRetrySettingsTab = ({
|
|||
<Label htmlFor="retry-policy-scope">Retry Policy Scope:</Label>
|
||||
<div className="w-48">
|
||||
<Select
|
||||
items={scopeItems}
|
||||
value={isGlobalScope ? "global" : selectedModelGroup || availableModelGroups[0]}
|
||||
onValueChange={(value) => setSelectedModelGroup(value)}
|
||||
>
|
||||
|
|
@ -89,10 +94,9 @@ const ModelRetrySettingsTab = ({
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="global">Global Default</SelectItem>
|
||||
{availableModelGroups.map((group) => (
|
||||
<SelectItem key={group} value={group}>
|
||||
{group}
|
||||
{scopeItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
|
|||
|
|
@ -96,6 +96,55 @@ describe("ChatUI", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should show the SDK type by its human label rather than its wire value", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ChatUI
|
||||
accessToken="1234567890"
|
||||
token="1234567890"
|
||||
userRole="user"
|
||||
userID="1234567890"
|
||||
disabledPersonalKeyCreation={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Key")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /get code/i }));
|
||||
|
||||
const sdkTrigger = await screen.findByLabelText("SDK Type");
|
||||
expect(sdkTrigger).toHaveTextContent("OpenAI SDK");
|
||||
|
||||
await user.click(sdkTrigger);
|
||||
await user.click(await screen.findByRole("option", { name: "Azure SDK" }));
|
||||
|
||||
expect(await screen.findByLabelText("SDK Type")).toHaveTextContent("Azure SDK");
|
||||
});
|
||||
|
||||
it("should show the voice by its human label rather than its wire value", async () => {
|
||||
render(
|
||||
<ChatUI
|
||||
accessToken="1234567890"
|
||||
token="1234567890"
|
||||
userRole="user"
|
||||
userID="1234567890"
|
||||
disabledPersonalKeyCreation={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Key")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await selectComboboxOption("Select an endpoint", "/v1/audio/speech");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Voice")).toHaveTextContent("Alloy - Professional and confident");
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow the user to select a model", async () => {
|
||||
render(
|
||||
<ChatUI
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ import {
|
|||
validateImageEditFile,
|
||||
} from "./uploadValidation";
|
||||
|
||||
const SDK_ITEMS = [
|
||||
{ value: "openai", label: "OpenAI SDK" },
|
||||
{ value: "azure", label: "Azure SDK" },
|
||||
] as const;
|
||||
|
||||
interface ChatUIProps {
|
||||
accessToken: string | null;
|
||||
token: string | null;
|
||||
|
|
@ -1314,7 +1319,11 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
<Volume2 className="mr-2 size-4" aria-hidden="true" />
|
||||
Voice
|
||||
</label>
|
||||
<ShadcnSelect value={selectedVoice} onValueChange={handleVoiceChange}>
|
||||
<ShadcnSelect
|
||||
items={OPEN_AI_VOICE_SELECT_OPTIONS}
|
||||
value={selectedVoice}
|
||||
onValueChange={handleVoiceChange}
|
||||
>
|
||||
<SelectTrigger className="w-full" size="sm" aria-label="Voice">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
|
@ -2082,13 +2091,20 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
<div className="my-2 flex items-end justify-between gap-3">
|
||||
<div>
|
||||
<p className="mb-1 text-sm font-medium text-gray-700">SDK Type</p>
|
||||
<ShadcnSelect value={selectedSdk} onValueChange={(value) => setSelectedSdk(value as "openai" | "azure")}>
|
||||
<ShadcnSelect
|
||||
items={SDK_ITEMS}
|
||||
value={selectedSdk}
|
||||
onValueChange={(value) => setSelectedSdk(value as "openai" | "azure")}
|
||||
>
|
||||
<SelectTrigger className="w-[150px]" size="sm" aria-label="SDK Type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="openai">OpenAI SDK</SelectItem>
|
||||
<SelectItem value="azure">Azure SDK</SelectItem>
|
||||
{SDK_ITEMS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</ShadcnSelect>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import userEvent, { PointerEventsCheckLevel } from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import PromptCodeSnippets from "./PromptCodeSnippets";
|
||||
|
||||
|
|
@ -20,4 +21,27 @@ describe("PromptCodeSnippets", () => {
|
|||
expect(screen.getByRole("tablist", { name: "Generated code type" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("dialog")).toHaveClass("max-h-[calc(100dvh-2rem)]", "overflow-y-auto");
|
||||
});
|
||||
|
||||
it("shows the selected language by its human label on the trigger", async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: PointerEventsCheckLevel.Never });
|
||||
render(
|
||||
<PromptCodeSnippets
|
||||
promptId="welcome"
|
||||
model="gpt-4o"
|
||||
promptVariables={{ name: "Ada" }}
|
||||
accessToken="token"
|
||||
version="2"
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: /get code/i }));
|
||||
|
||||
const trigger = await screen.findByRole("combobox", { name: "Language" });
|
||||
expect(trigger).toHaveTextContent("cURL");
|
||||
|
||||
await user.click(trigger);
|
||||
const python = await screen.findByRole("option", { name: "Python (OpenAI SDK)" });
|
||||
await user.click(python);
|
||||
|
||||
expect(screen.getByRole("combobox", { name: "Language" })).toHaveTextContent("Python (OpenAI SDK)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ import { toast } from "@/lib/toast";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
const LANGUAGE_ITEMS = [
|
||||
{ value: "curl", label: "cURL" },
|
||||
{ value: "python", label: "Python (OpenAI SDK)" },
|
||||
{ value: "javascript", label: "JavaScript (OpenAI SDK)" },
|
||||
] as const;
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
interface PromptCodeSnippetsProps {
|
||||
|
|
@ -252,6 +258,7 @@ main();`;
|
|||
Language
|
||||
</label>
|
||||
<Select
|
||||
items={LANGUAGE_ITEMS}
|
||||
value={selectedLanguage}
|
||||
onValueChange={(value) => setSelectedLanguage(value as "curl" | "python" | "javascript")}
|
||||
>
|
||||
|
|
@ -259,9 +266,11 @@ main();`;
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="curl">cURL</SelectItem>
|
||||
<SelectItem value="python">Python (OpenAI SDK)</SelectItem>
|
||||
<SelectItem value="javascript">JavaScript (OpenAI SDK)</SelectItem>
|
||||
{LANGUAGE_ITEMS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,4 +28,25 @@ describe("PromptEditorHeader", () => {
|
|||
expect(onBack).toHaveBeenCalledOnce();
|
||||
expect(onSave).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["development", "Development"],
|
||||
["staging", "Staging"],
|
||||
["production", "Production"],
|
||||
])("shows the %s environment by its human label", (environment, label) => {
|
||||
render(
|
||||
<PromptEditorHeader
|
||||
promptName="welcome"
|
||||
onNameChange={vi.fn()}
|
||||
onBack={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
isSaving={false}
|
||||
accessToken="token"
|
||||
environment={environment}
|
||||
onEnvironmentChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("combobox", { name: "Environment" })).toHaveTextContent(label);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
const ENVIRONMENT_ITEMS = [
|
||||
{ value: "development", label: "Development" },
|
||||
{ value: "staging", label: "Staging" },
|
||||
{ value: "production", label: "Production" },
|
||||
] as const;
|
||||
|
||||
interface PromptEditorHeaderProps {
|
||||
promptName: string;
|
||||
onNameChange: (name: string) => void;
|
||||
|
|
@ -57,14 +63,20 @@ const PromptEditorHeader: React.FC<PromptEditorHeaderProps> = ({
|
|||
style={{ width: "200px" }}
|
||||
/>
|
||||
{version && <Badge>{version}</Badge>}
|
||||
<Select value={environment} onValueChange={(value) => onEnvironmentChange(String(value))}>
|
||||
<Select
|
||||
items={ENVIRONMENT_ITEMS}
|
||||
value={environment}
|
||||
onValueChange={(value) => onEnvironmentChange(String(value))}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-[140px]" aria-label="Environment">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="development">Development</SelectItem>
|
||||
<SelectItem value="staging">Staging</SelectItem>
|
||||
<SelectItem value="production">Production</SelectItem>
|
||||
{ENVIRONMENT_ITEMS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Badge variant="secondary">Draft</Badge>
|
||||
|
|
|
|||
|
|
@ -22,4 +22,22 @@ describe("PromptMessagesCard", () => {
|
|||
fireEvent.click(screen.getByRole("button", { name: /add message/i }));
|
||||
expect(onAddMessage).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["user", "User"],
|
||||
["assistant", "Assistant"],
|
||||
["system", "System"],
|
||||
])("shows the %s role by its human label", (role, label) => {
|
||||
render(
|
||||
<PromptMessagesCard
|
||||
messages={[{ role, content: "Hello" }]}
|
||||
onAddMessage={vi.fn()}
|
||||
onUpdateMessage={vi.fn()}
|
||||
onRemoveMessage={vi.fn()}
|
||||
onMoveMessage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("combobox", { name: "Message 1 role" })).toHaveTextContent(label);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { Card } from "@/components/ui/card";
|
||||
import { Select as ShadcnSelect, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
const ROLE_ITEMS = [
|
||||
{ value: "user", label: "User" },
|
||||
{ value: "assistant", label: "Assistant" },
|
||||
{ value: "system", label: "System" },
|
||||
] as const;
|
||||
|
||||
interface PromptMessagesCardProps {
|
||||
messages: Message[];
|
||||
onAddMessage: () => void;
|
||||
|
|
@ -70,6 +76,7 @@ const PromptMessagesCard: React.FC<PromptMessagesCardProps> = ({
|
|||
>
|
||||
<div className="bg-muted px-2 py-1.5 border-b border-border flex items-center justify-between">
|
||||
<ShadcnSelect
|
||||
items={ROLE_ITEMS}
|
||||
value={message.role}
|
||||
onValueChange={(value) => onUpdateMessage(index, "role", String(value))}
|
||||
>
|
||||
|
|
@ -81,9 +88,11 @@ const PromptMessagesCard: React.FC<PromptMessagesCardProps> = ({
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="assistant">Assistant</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
{ROLE_ITEMS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</ShadcnSelect>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -85,6 +85,19 @@ describe("CreateVectorStore", () => {
|
|||
expect(screen.getByText(/Support for single or bulk upload/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the provider display name on the trigger rather than its wire value", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CreateVectorStore accessToken="test-token" />);
|
||||
|
||||
const providerSelect = screen.getByRole("combobox", { name: /Provider/ });
|
||||
expect(providerSelect).toHaveTextContent("Amazon Bedrock");
|
||||
|
||||
await user.click(providerSelect);
|
||||
await user.click(await screen.findByText("AWS S3 Vectors"));
|
||||
|
||||
expect(screen.getByRole("combobox", { name: /Provider/ })).toHaveTextContent("AWS S3 Vectors");
|
||||
});
|
||||
|
||||
it("should have provider selection dropdown", () => {
|
||||
render(<CreateVectorStore accessToken="test-token" />);
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@ const { Dragger } = Upload;
|
|||
|
||||
const RAG_INGEST_UNSUPPORTED_PROVIDERS = new Set(["valkey"]);
|
||||
|
||||
const providerItems = Object.entries(VectorStoreProviders)
|
||||
.filter(([providerEnum]) => !RAG_INGEST_UNSUPPORTED_PROVIDERS.has(vectorStoreProviderMap[providerEnum]))
|
||||
.map(([providerEnum, providerDisplayName]) => ({
|
||||
value: vectorStoreProviderMap[providerEnum],
|
||||
label: providerDisplayName,
|
||||
}));
|
||||
|
||||
const asText = (value: unknown): string => (typeof value === "string" ? value : "");
|
||||
|
||||
const labelWithHint = (label: string, hint: string): React.ReactNode => (
|
||||
|
|
@ -292,6 +299,7 @@ const CreateVectorStore: React.FC<CreateVectorStoreProps> = ({ accessToken, onSu
|
|||
{labelWithHint("Provider", "Select the provider for embedding and vector store operations")}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
items={providerItems}
|
||||
value={selectedProvider}
|
||||
onValueChange={(value: string | null) => value !== null && setSelectedProvider(value)}
|
||||
>
|
||||
|
|
@ -299,20 +307,12 @@ const CreateVectorStore: React.FC<CreateVectorStoreProps> = ({ accessToken, onSu
|
|||
<SelectValue placeholder="Select a provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
{Object.entries(VectorStoreProviders)
|
||||
.filter(
|
||||
([providerEnum]) => !RAG_INGEST_UNSUPPORTED_PROVIDERS.has(vectorStoreProviderMap[providerEnum]),
|
||||
)
|
||||
.map(([providerEnum, providerDisplayName]) => (
|
||||
<SelectItem key={providerEnum} value={vectorStoreProviderMap[providerEnum]}>
|
||||
<Logo
|
||||
src={vectorStoreProviderLogoMap[providerDisplayName]}
|
||||
label={providerDisplayName}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
<span>{providerDisplayName}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
{providerItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<Logo src={vectorStoreProviderLogoMap[item.label]} label={item.label} className="w-5 h-5" />
|
||||
<span>{item.label}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -78,4 +78,15 @@ describe("UserBannerSettings", () => {
|
|||
fireEvent.click(screen.getByRole("button", { name: "Save banner" }));
|
||||
expect(mutate).toHaveBeenCalledWith({ enabled: false, message: "", severity: "info" }, expect.anything());
|
||||
});
|
||||
|
||||
it.each([
|
||||
["info", "Info"],
|
||||
["warning", "Warning"],
|
||||
["error", "Error"],
|
||||
])("shows the %s severity by its human label", (severity, label) => {
|
||||
mockHooks({ ...publishedBanner, severity: severity as UserBanner["severity"] });
|
||||
renderWithProviders(<UserBannerSettings />);
|
||||
|
||||
expect(screen.getByRole("combobox", { name: "Banner severity" })).toHaveTextContent(label);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ const SEVERITY_LABELS: Record<UserBannerSeverity, string> = {
|
|||
error: "Error",
|
||||
};
|
||||
|
||||
const SEVERITY_ITEMS = (Object.keys(SEVERITY_LABELS) as UserBannerSeverity[]).map((severity) => ({
|
||||
value: severity,
|
||||
label: SEVERITY_LABELS[severity],
|
||||
}));
|
||||
|
||||
const EMPTY_BANNER: UserBanner = { enabled: false, message: "", severity: "info", revision: "" };
|
||||
|
||||
export default function UserBannerSettings() {
|
||||
|
|
@ -109,6 +114,7 @@ function UserBannerSettingsForm({ persisted, isLoading, isPending, saveBanner }:
|
|||
<div className="flex flex-col gap-2">
|
||||
<Label>Severity</Label>
|
||||
<Select
|
||||
items={SEVERITY_ITEMS}
|
||||
value={draft.severity}
|
||||
onValueChange={(value: string | null) =>
|
||||
setDraft({ ...draft, severity: (value ?? "info") as UserBannerSeverity })
|
||||
|
|
@ -118,9 +124,9 @@ function UserBannerSettingsForm({ persisted, isLoading, isPending, saveBanner }:
|
|||
<SelectValue placeholder="Severity" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(SEVERITY_LABELS) as UserBannerSeverity[]).map((severity) => (
|
||||
<SelectItem key={severity} value={severity}>
|
||||
{SEVERITY_LABELS[severity]}
|
||||
{SEVERITY_ITEMS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
|
|||
|
|
@ -142,6 +142,20 @@ describe("ToolPoliciesTable filters", () => {
|
|||
expect(screen.getByTestId("filter-chip-team_id")).toHaveTextContent("Team Name:");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["filter-input-policy", "All Input Policies"],
|
||||
["filter-output-policy", "All Output Policies"],
|
||||
["filter-team", "All Teams"],
|
||||
["filter-key-alias", "All Keys"],
|
||||
])("should show the human label on the %s trigger while unfiltered", async (testId, label) => {
|
||||
const user = userEvent.setup();
|
||||
renderTable();
|
||||
|
||||
await user.click(screen.getByTestId("datatable-filters-trigger"));
|
||||
|
||||
expect(await screen.findByTestId(testId)).toHaveTextContent(label);
|
||||
});
|
||||
|
||||
it("should offer only the teams and keys present in the loaded rows", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderTable();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,16 @@ import { getToolPoliciesTableColumns } from "./ToolPoliciesTableColumns";
|
|||
|
||||
const ALL_VALUE = "all";
|
||||
|
||||
const INPUT_POLICY_FILTER_ITEMS = [
|
||||
{ value: ALL_VALUE, label: "All Input Policies" },
|
||||
...INPUT_POLICY_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
|
||||
];
|
||||
|
||||
const OUTPUT_POLICY_FILTER_ITEMS = [
|
||||
{ value: ALL_VALUE, label: "All Output Policies" },
|
||||
...OUTPUT_POLICY_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
|
||||
];
|
||||
|
||||
const toFilterValue = (value: string | null): string | undefined =>
|
||||
value === null || value === ALL_VALUE ? undefined : value;
|
||||
|
||||
|
|
@ -77,6 +87,20 @@ export function ToolPoliciesTable({
|
|||
|
||||
const teamOptions = useMemo(() => uniqueValues(data, (row) => row.team_id), [data]);
|
||||
const keyAliasOptions = useMemo(() => uniqueValues(data, (row) => row.key_alias), [data]);
|
||||
const teamFilterItems = useMemo(
|
||||
() => [
|
||||
{ value: ALL_VALUE, label: "All Teams" },
|
||||
...teamOptions.map((option) => ({ value: option, label: option })),
|
||||
],
|
||||
[teamOptions],
|
||||
);
|
||||
const keyAliasFilterItems = useMemo(
|
||||
() => [
|
||||
{ value: ALL_VALUE, label: "All Keys" },
|
||||
...keyAliasOptions.map((option) => ({ value: option, label: option })),
|
||||
],
|
||||
[keyAliasOptions],
|
||||
);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
|
|
@ -119,6 +143,7 @@ export function ToolPoliciesTable({
|
|||
<>
|
||||
<DataTableFilterField label="Input Policy">
|
||||
<Select
|
||||
items={INPUT_POLICY_FILTER_ITEMS}
|
||||
value={(get("input_policy") as string) ?? ALL_VALUE}
|
||||
onValueChange={(value) => set("input_policy", toFilterValue(value))}
|
||||
>
|
||||
|
|
@ -137,6 +162,7 @@ export function ToolPoliciesTable({
|
|||
</DataTableFilterField>
|
||||
<DataTableFilterField label="Output Policy">
|
||||
<Select
|
||||
items={OUTPUT_POLICY_FILTER_ITEMS}
|
||||
value={(get("output_policy") as string) ?? ALL_VALUE}
|
||||
onValueChange={(value) => set("output_policy", toFilterValue(value))}
|
||||
>
|
||||
|
|
@ -155,6 +181,7 @@ export function ToolPoliciesTable({
|
|||
</DataTableFilterField>
|
||||
<DataTableFilterField label="Team Name">
|
||||
<Select
|
||||
items={teamFilterItems}
|
||||
value={(get("team_id") as string) ?? ALL_VALUE}
|
||||
onValueChange={(value) => set("team_id", toFilterValue(value))}
|
||||
>
|
||||
|
|
@ -173,6 +200,7 @@ export function ToolPoliciesTable({
|
|||
</DataTableFilterField>
|
||||
<DataTableFilterField label="Key Name">
|
||||
<Select
|
||||
items={keyAliasFilterItems}
|
||||
value={(get("key_alias") as string) ?? ALL_VALUE}
|
||||
onValueChange={(value) => set("key_alias", toFilterValue(value))}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { useState } from "react";
|
||||
import userEvent, { PointerEventsCheckLevel } from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import DurationSelect from "./DurationSelect";
|
||||
|
|
@ -50,4 +51,29 @@ describe("DurationSelect", () => {
|
|||
const select = screen.getByRole("combobox");
|
||||
expect(select).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["24h", "Daily"],
|
||||
["7d", "Weekly"],
|
||||
["30d", "Monthly"],
|
||||
])("shows the human label on the trigger for %s", (value, label) => {
|
||||
render(<DurationSelect value={value} />);
|
||||
|
||||
expect(screen.getByRole("combobox")).toHaveTextContent(label);
|
||||
});
|
||||
|
||||
it("shows the human label on the trigger after the user picks an option", async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: PointerEventsCheckLevel.Never });
|
||||
const Harness = () => {
|
||||
const [value, setValue] = useState("24h");
|
||||
return <DurationSelect value={value} onChange={setValue} />;
|
||||
};
|
||||
render(<Harness />);
|
||||
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
const monthly = screen.getByText("Monthly");
|
||||
await user.click(monthly.closest('[role="option"]') ?? monthly);
|
||||
|
||||
expect(screen.getByRole("combobox")).toHaveTextContent("Monthly");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const DURATION_OPTIONS = [
|
|||
export default function DurationSelect({ className, value, onChange }: DurationSelectProps) {
|
||||
return (
|
||||
<Select
|
||||
items={DURATION_OPTIONS}
|
||||
value={value}
|
||||
onValueChange={(nextValue) => {
|
||||
const selectedOption = DURATION_OPTIONS.find((option) => option.value === nextValue);
|
||||
|
|
|
|||
45
ui/litellm-dashboard/src/components/ui/select.test.tsx
Normal file
45
ui/litellm-dashboard/src/components/ui/select.test.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
const ENVIRONMENTS = [
|
||||
{ value: "development", label: "Development" },
|
||||
{ value: "staging", label: "Staging" },
|
||||
] as const;
|
||||
|
||||
function renderSelect(props: { value: string | null; items?: React.ComponentProps<typeof Select>["items"] }) {
|
||||
return render(
|
||||
<Select value={props.value} items={props.items}>
|
||||
<SelectTrigger data-testid="trigger">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ENVIRONMENTS.map((environment) => (
|
||||
<SelectItem key={environment.value} value={environment.value}>
|
||||
{environment.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("SelectValue label resolution", () => {
|
||||
it("renders the raw value when the root carries no items", () => {
|
||||
renderSelect({ value: "development" });
|
||||
|
||||
expect(screen.getByTestId("trigger")).toHaveTextContent("development");
|
||||
});
|
||||
|
||||
it("renders the human label when the root carries items", () => {
|
||||
renderSelect({ value: "development", items: ENVIRONMENTS });
|
||||
|
||||
expect(screen.getByTestId("trigger")).toHaveTextContent("Development");
|
||||
});
|
||||
|
||||
it("falls back to the raw value for a value absent from items", () => {
|
||||
renderSelect({ value: "production", items: ENVIRONMENTS });
|
||||
|
||||
expect(screen.getByTestId("trigger")).toHaveTextContent("production");
|
||||
});
|
||||
});
|
||||
|
|
@ -143,4 +143,31 @@ describe("AuditLogsTable", () => {
|
|||
const committed = typeof arg === "function" ? arg([]) : arg;
|
||||
expect(committed).toEqual([{ id: "object_id", value: "obj-9" }]);
|
||||
});
|
||||
|
||||
it("shows the human label on both filter triggers while unfiltered", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderTable();
|
||||
|
||||
await user.click(screen.getByTestId("datatable-filters-trigger"));
|
||||
const [action, table] = await screen.findAllByRole("combobox");
|
||||
|
||||
expect(action).toHaveTextContent("All Actions");
|
||||
expect(table).toHaveTextContent("All Tables");
|
||||
});
|
||||
|
||||
it("shows the human label on the filter triggers for an applied filter", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderTable({
|
||||
columnFilters: [
|
||||
{ id: "action", value: "created" },
|
||||
{ id: "table_name", value: "LiteLLM_TeamTable" },
|
||||
],
|
||||
});
|
||||
|
||||
await user.click(screen.getByTestId("datatable-filters-trigger"));
|
||||
const [action, table] = await screen.findAllByRole("combobox");
|
||||
|
||||
expect(action).toHaveTextContent("Created");
|
||||
expect(table).toHaveTextContent("Teams");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,6 +45,16 @@ const TABLE_OPTIONS = [
|
|||
{ label: "Models", value: "LiteLLM_ProxyModelTable" },
|
||||
] as const;
|
||||
|
||||
const ACTION_FILTER_ITEMS = [
|
||||
{ value: ALL_VALUE, label: "All Actions" },
|
||||
...ACTION_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
|
||||
];
|
||||
|
||||
const TABLE_FILTER_ITEMS = [
|
||||
{ value: ALL_VALUE, label: "All Tables" },
|
||||
...TABLE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
|
||||
];
|
||||
|
||||
const FILTER_LABELS: Record<string, string> = {
|
||||
object_id: "Object ID",
|
||||
changed_by: "Changed By",
|
||||
|
|
@ -164,6 +174,7 @@ export function AuditLogsTable({
|
|||
</DataTableFilterField>
|
||||
<DataTableFilterField label="Action">
|
||||
<Select
|
||||
items={ACTION_FILTER_ITEMS}
|
||||
value={(get("action") as string) ?? ALL_VALUE}
|
||||
onValueChange={(value) => set("action", value === ALL_VALUE ? undefined : value)}
|
||||
>
|
||||
|
|
@ -182,6 +193,7 @@ export function AuditLogsTable({
|
|||
</DataTableFilterField>
|
||||
<DataTableFilterField label="Table">
|
||||
<Select
|
||||
items={TABLE_FILTER_ITEMS}
|
||||
value={(get("table_name") as string) ?? ALL_VALUE}
|
||||
onValueChange={(value) => set("table_name", value === ALL_VALUE ? undefined : value)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -249,4 +249,14 @@ describe("RequestLogsFilters", () => {
|
|||
|
||||
await waitFor(() => expect(useInfiniteSpendLogEndUsers).toHaveBeenCalledWith(otherWindow, 50, undefined));
|
||||
});
|
||||
|
||||
it.each([
|
||||
["", "All Statuses"],
|
||||
["success", "Success"],
|
||||
["failure", "Failure"],
|
||||
])("shows the human label on the Status trigger for %s", async (status, label) => {
|
||||
renderFilters(status === "" ? {} : { [LOG_FILTER_IDS.STATUS]: status });
|
||||
|
||||
expect(await screen.findByText(label)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ import { ERROR_CODE_OPTIONS } from "./constants";
|
|||
import { LOG_FILTER_IDS, type LogsWindow } from "./log_filter_logic";
|
||||
|
||||
const ALL_VALUE = "all";
|
||||
|
||||
const STATUS_FILTER_ITEMS = [
|
||||
{ value: ALL_VALUE, label: "All Statuses" },
|
||||
{ value: "success", label: "Success" },
|
||||
{ value: "failure", label: "Failure" },
|
||||
] as const;
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const asString = (value: unknown): string => (typeof value === "string" ? value : "");
|
||||
|
|
@ -305,6 +311,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
|
|||
|
||||
<DataTableFilterField label="Status">
|
||||
<Select
|
||||
items={STATUS_FILTER_ITEMS}
|
||||
value={valueOf(LOG_FILTER_IDS.STATUS) === "" ? ALL_VALUE : valueOf(LOG_FILTER_IDS.STATUS)}
|
||||
onValueChange={(next) => set(LOG_FILTER_IDS.STATUS, next === null || next === ALL_VALUE ? undefined : next)}
|
||||
>
|
||||
|
|
@ -312,9 +319,11 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
|
|||
<SelectValue placeholder="All Statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_VALUE}>All Statuses</SelectItem>
|
||||
<SelectItem value="success">Success</SelectItem>
|
||||
<SelectItem value="failure">Failure</SelectItem>
|
||||
{STATUS_FILTER_ITEMS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</DataTableFilterField>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue