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:
Yuneng Jiang 2026-08-18 15:29:35 -07:00
parent 0b82b087fd
commit 4cbceb565b
No known key found for this signature in database
27 changed files with 452 additions and 41 deletions

View file

@ -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);

View file

@ -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}

View file

@ -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)");
});
});

View file

@ -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)

View file

@ -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");
});
});

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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)");
});
});

View file

@ -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>

View file

@ -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);
});
});

View file

@ -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>

View file

@ -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);
});
});

View file

@ -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">

View file

@ -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" />);

View file

@ -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>

View file

@ -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);
});
});

View file

@ -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>

View file

@ -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();

View file

@ -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))}
>

View file

@ -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");
});
});

View file

@ -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);

View 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");
});
});

View file

@ -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");
});
});

View file

@ -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)}
>

View file

@ -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();
});
});

View file

@ -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>