From c696fdfb05c2b11d9f7f4d06b23f4e783c85ef54 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 19 Aug 2026 00:00:33 -0700 Subject: [PATCH] fix(ui): gate the pass-through guardrail field inputs when the section is disabled (#37435) PassThroughGuardrailsSection threads its `disabled` prop to the guardrail selector and to all three quick-add buttons, but not to the two `TagsInput` controls for request and response field targeting. Every other prop crossed, so the section greys out while a user can still type field names straight into both tag inputs. Latent rather than live: neither call site passes `disabled` today, so the prop is currently dead across the tree. This arms the gate for whoever passes it first. `TagsInput` already honours `disabled` end to end, measured rather than read: with it set, the input carries a real disabled attribute and all three commit paths, token separator, Enter and blur, are inert. So the fix belongs at the call sites and the primitive needs no change. The new test drives the control rather than asserting an attribute, since a Base UI control can paint a disabled state while still accepting input. The enabled cases are the liveness gate: they type the same text through the same gesture and assert it commits, so the disabled cases cannot pass vacuously. --- .../PassThroughGuardrailsSection.test.tsx | 61 +++++++++++++++++++ .../PassThroughGuardrailsSection.tsx | 2 + 2 files changed, 63 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.test.tsx diff --git a/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.test.tsx b/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.test.tsx new file mode 100644 index 00000000000..0cc95033dde --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.test.tsx @@ -0,0 +1,61 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import PassThroughGuardrailsSection from "./PassThroughGuardrailsSection"; + +vi.mock("../networking", async (importOriginal) => ({ + ...(await importOriginal()), + getGuardrailsList: vi.fn(async () => ({ guardrails: [{ guardrail_name: "pii-guard" }] })), +})); + +const FIELDS = [ + { label: "Request Fields", matcher: /Request Fields/, payloadKey: "request_fields", typed: "query" }, + { label: "Response Fields", matcher: /Response Fields/, payloadKey: "response_fields", typed: "choices.content" }, +] as const; + +const renderSection = (disabled: boolean) => { + const onChange = vi.fn(); + render( + , + ); + return onChange; +}; + +const fieldInput = (matcher: RegExp) => screen.getByLabelText(matcher) as HTMLInputElement; + +describe("PassThroughGuardrailsSection field targeting", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it.each(FIELDS)( + "commits a typed $label entry while the section is enabled", + async ({ matcher, payloadKey, typed }) => { + const user = userEvent.setup(); + const onChange = renderSection(false); + + await user.type(fieldInput(matcher), `${typed},`); + + expect(onChange.mock.calls.at(-1)?.[0]).toStrictEqual({ "pii-guard": { [payloadKey]: [typed] } }); + }, + ); + + it.each(FIELDS)("refuses typed $label input while the section is disabled", async ({ matcher, typed }) => { + const user = userEvent.setup(); + const onChange = renderSection(true); + const input = fieldInput(matcher); + + await user.type(input, `${typed},`); + await user.type(input, "sneaked-in{Enter}"); + await user.tab(); + + expect(input.value).toBe(""); + expect(onChange).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.tsx b/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.tsx index 2c6856cb8b8..f51629b7ee1 100644 --- a/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.tsx +++ b/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.tsx @@ -174,6 +174,7 @@ const PassThroughGuardrailsSection: React.FC value={value[guardrailName]?.request_fields ?? []} onValueChange={(fields) => handleFieldChange(guardrailName, "request_fields", fields)} tokenSeparators={[","]} + disabled={disabled} /> @@ -212,6 +213,7 @@ const PassThroughGuardrailsSection: React.FC value={value[guardrailName]?.response_fields ?? []} onValueChange={(fields) => handleFieldChange(guardrailName, "response_fields", fields)} tokenSeparators={[","]} + disabled={disabled} />