diff --git a/ui/litellm-dashboard/src/components/logging_credentials/EditLoggingCredentialModal.tsx b/ui/litellm-dashboard/src/components/logging_credentials/EditLoggingCredentialModal.tsx index 30f6a910af7..9e168567bc0 100644 --- a/ui/litellm-dashboard/src/components/logging_credentials/EditLoggingCredentialModal.tsx +++ b/ui/litellm-dashboard/src/components/logging_credentials/EditLoggingCredentialModal.tsx @@ -33,8 +33,6 @@ const EditLoggingCredentialModal: React.FC = ({ onClose, onSaved, }) => { - // destroyOnClose remounts the Form each open, so initialValues re-seeds from the - // current destination -- no effect syncing prop into state. const [form] = Form.useForm(); const handleSave = async () => { @@ -65,7 +63,7 @@ const EditLoggingCredentialModal: React.FC = ({ onCancel={onClose} onOk={handleSave} okText="Save" - destroyOnClose + destroyOnHidden > form={form} layout="vertical" preserve={false} initialValues={{ access: access ?? {} }}> diff --git a/ui/litellm-dashboard/src/components/settings.test.tsx b/ui/litellm-dashboard/src/components/settings.test.tsx index e4a20bedd66..709211662df 100644 --- a/ui/litellm-dashboard/src/components/settings.test.tsx +++ b/ui/litellm-dashboard/src/components/settings.test.tsx @@ -258,6 +258,50 @@ describe("Settings", () => { expect(getByText("well-formed")).toBeInTheDocument(); }); + // Regression: the Edit scope dialog is driven by a Form store that Form.useForm() owns. + // initialValues only seeds that store on first mount, so reopening the dialog for a second + // destination left the first one's scope in the fields while only the title changed. Save + // sends the whole access object, so pressing it wrote the stale scope -- silently turning a + // team-scoped destination global and leaking every tenant's traces to it. The parent keys + // the modal per destination so the store is rebuilt each time. + it("should show each destination's own scope when Edit scope is reopened for another one", async () => { + const user = userEvent.setup(); + credentialsFixture = { + credentials: [ + { + credential_name: "global-dest", + credential_info: { credential_type: "logging", description: "generic", access: { global: true } }, + }, + { + credential_name: "team-dest", + credential_info: { credential_type: "logging", description: "generic", access: { teams: ["team-1"] } }, + }, + ], + }; + + const { findByText } = renderSettings(defaultProps); + await findByText("Active Logging Callbacks"); + + const openEditScope = async (name: string) => { + await user.click(await screen.findByTestId(`callback-actions-${name}-success`)); + await user.click(await screen.findByTestId("destination-action-edit-access")); + return await screen.findByText(`Edit scope — ${name}`); + }; + const globalSwitch = () => document.querySelector(".ant-modal .ant-switch"); + const closeDialog = async () => { + await user.click(screen.getByRole("button", { name: "Cancel" })); + await waitFor(() => expect(document.querySelector(".ant-modal-title")).not.toBeInTheDocument()); + }; + + await openEditScope("global-dest"); + expect(globalSwitch()).toHaveAttribute("aria-checked", "true"); + await closeDialog(); + + await openEditScope("team-dest"); + // Before the fix this read "true", carried over from global-dest. + expect(globalSwitch()).toHaveAttribute("aria-checked", "false"); + }); + it("should hold the callbacks table in loading state until the fetch settles", async () => { let resolveCallbacks: (value: { callbacks: never[]; diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index dacba0524bc..972825fec93 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -590,6 +590,15 @@ const Settings: React.FC = ({ accessToken, userRole, userID, /> {accessToken && (