From 84b0a4297a9ddcbb6b0bedeca165aa43fa91fd56 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 06:17:41 +0000 Subject: [PATCH] fix(ui): preserve credential actions across dashboard renders (#50845) Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../anthropicFederationCredential.spec.ts | 18 ++++++++++++++- .../model_add/CredentialsPanel.test.tsx | 23 +++++++++++++++++++ .../components/model_add/CredentialsPanel.tsx | 10 ++++---- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/tests/e2e/ui/tests/integrationCritical/anthropicFederationCredential.spec.ts b/tests/e2e/ui/tests/integrationCritical/anthropicFederationCredential.spec.ts index 82f088634be..01f7cf8de07 100644 --- a/tests/e2e/ui/tests/integrationCritical/anthropicFederationCredential.spec.ts +++ b/tests/e2e/ui/tests/integrationCritical/anthropicFederationCredential.spec.ts @@ -155,11 +155,13 @@ async function openAddCredentialDialog( async function openEditDialog( page: PlaywrightPage, name: string, + afterMenuOpen?: () => Promise, ): Promise { await expect(page.getByText(UPDATED_TOAST)).toHaveCount(0); const row = page.locator("tr", { hasText: name }); await expect(row).toBeVisible({ timeout: 15_000 }); await row.getByTestId(`credential-actions-${name}`).click(); + await afterMenuOpen?.(); await page.getByTestId("credential-action-edit").click(); const dialog = page.getByRole("dialog", { name: "Edit Credential" }); await expect(dialog).toBeVisible(); @@ -478,6 +480,15 @@ test("the Edit Credential dialog sends only the changed values and deletes the k }; const ruleId = `fdrl_${suffix}`; const tokenFile = `/var/run/secrets/anthropic/${suffix}/token`; + let releaseModels!: () => void; + const modelsReleased = new Promise((resolve) => { + releaseModels = resolve; + }); + await page.route("**/v2/model/info?*", async (route) => { + const response = await route.fetch(); + await modelsReleased; + await route.fulfill({ response }); + }); try { await createCredential(request, names.fromApiKey, { api_key: `sk-ant-${suffix}`, @@ -500,7 +511,11 @@ test("the Edit Credential dialog sends only the changed values and deletes the k await logInAsAdmin(page); await openModelsTab(page, "LLM Credentials"); - const keyDialog = await openEditDialog(page, names.fromApiKey); + const keyDialog = await openEditDialog(page, names.fromApiKey, async () => { + const response = page.waitForResponse((response) => response.url().includes("/v2/model/info?")); + releaseModels(); + await (await response).finished(); + }); await pickAuthMethod(page, keyDialog, FEDERATION_BADGE); await fillFields(keyDialog, { anthropic_federation_rule_id: ruleId, @@ -612,6 +627,7 @@ test("the Edit Credential dialog sends only the changed values and deletes the k page.locator("tr", { hasText: names.switchProvider }), ).not.toContainText(FEDERATION_BADGE); } finally { + releaseModels(); await deleteCredentials(request, Object.values(names)); } }); diff --git a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx index a9e98cc5430..146074fcb9e 100644 --- a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx +++ b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx @@ -142,6 +142,29 @@ describe("CredentialsPanel", () => { expect(screen.getByText("No credentials configured")).toBeInTheDocument(); }); + it("keeps an open credential action usable when the panel renders again", async () => { + const user = userEvent.setup(); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); + mockUseCredentials.mockReturnValue({ data: { credentials }, isLoading: false, refetch: vi.fn() }); + const client = createQueryClient(); + const { rerender } = render( + + + , + ); + + await user.click(screen.getByTestId("credential-actions-openai-key")); + expect(await screen.findByTestId("credential-action-edit")).toBeVisible(); + rerender( + + + , + ); + + await user.click(screen.getByTestId("credential-action-edit")); + expect(screen.getByTestId("credential-modal-edit-submit")).toBeVisible(); + }); + it("shows the loading skeleton instead of the empty state while credentials load", () => { mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); mockUseCredentials.mockReturnValue({ data: undefined, isLoading: true, refetch: vi.fn() }); diff --git a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx index e3738a12bd2..98a25caacff 100644 --- a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx +++ b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx @@ -2,7 +2,7 @@ import { useTerminology } from "@/contexts/TerminologyContext"; import { Plus } from "lucide-react"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; @@ -91,15 +91,15 @@ export default function CredentialsPanel() { } }; - const openEditModal = (credential: CredentialItem) => { + const openEditModal = useCallback((credential: CredentialItem) => { setSelectedCredential(credential); setIsUpdateModalOpen(true); - }; + }, []); - const openDeleteModal = (credential: CredentialItem) => { + const openDeleteModal = useCallback((credential: CredentialItem) => { setCredentialToDelete(credential); setIsDeleteModalOpen(true); - }; + }, []); const closeDeleteModal = () => { setCredentialToDelete(null);