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>
This commit is contained in:
devin-ai-integration[bot] 2026-11-10 06:17:41 +00:00 • committed by GitHub
parent 64d84e557e
commit 84b0a4297a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 45 additions and 6 deletions

View file

@ -155,11 +155,13 @@ async function openAddCredentialDialog(
async function openEditDialog(
page: PlaywrightPage,
name: string,
afterMenuOpen?: () => Promise<void>,
): Promise<Locator> {
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<void>((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));
}
});

View file

@ -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(
<QueryClientProvider client={client}>
<CredentialsPanel />
</QueryClientProvider>,
);
await user.click(screen.getByTestId("credential-actions-openai-key"));
expect(await screen.findByTestId("credential-action-edit")).toBeVisible();
rerender(
<QueryClientProvider client={client}>
<CredentialsPanel />
</QueryClientProvider>,
);
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() });

View file

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