From 40622e27d7241e3551236935d1aeba2c4820d246 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:54:47 -0700 Subject: [PATCH] feat(ui): show the credential wizard inline in place of the credential list A dialog is cramped for a six step flow with a model table, and an outside click mid-flow throws the wizard state away. Add Credential now swaps the list for the wizard, and both the Back to credentials button and the wizard's Close return to the list --- .../model_add/CredentialsPanel.test.tsx | 38 +++++++++++++------ .../components/model_add/CredentialsPanel.tsx | 28 ++++++++------ 2 files changed, 42 insertions(+), 24 deletions(-) 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 587b6ae34e8..0d90bdc7b9c 100644 --- a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx +++ b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { render, screen, waitFor, within } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -133,33 +133,47 @@ describe("CredentialsPanel", () => { expect(screen.queryByText("No credentials configured")).not.toBeInTheDocument(); }); - it("opens the Add Credential wizard, not the credential form, when the add button is clicked", async () => { + it("swaps the credential list for the wizard, not the credential form, when the add button is clicked", async () => { const user = userEvent.setup(); mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); - mockUseCredentials.mockReturnValue({ data: { credentials: [] }, isLoading: false, refetch: vi.fn() }); + mockUseCredentials.mockReturnValue({ data: { credentials }, isLoading: false, refetch: vi.fn() }); renderPanel(); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + expect(screen.queryByTestId("add-credential-wizard-close")).not.toBeInTheDocument(); await user.click(screen.getByRole("button", { name: /add credential/i })); - const dialog = await screen.findByRole("dialog", { name: "Add Credential" }); - expect(within(dialog).getByTestId("add-credential-wizard-close")).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: "Add Credential" })).toBeInTheDocument(); + expect(screen.getByTestId("add-credential-wizard-close")).toBeInTheDocument(); + expect(screen.queryByText("openai-key")).not.toBeInTheDocument(); expect(screen.queryByTestId("credential-modal-add-submit")).not.toBeInTheDocument(); }); - it("closes the wizard dialog when the wizard finishes", async () => { + it("returns to the credential list when the wizard finishes", async () => { const user = userEvent.setup(); mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); - mockUseCredentials.mockReturnValue({ data: { credentials: [] }, isLoading: false, refetch: vi.fn() }); + mockUseCredentials.mockReturnValue({ data: { credentials }, isLoading: false, refetch: vi.fn() }); renderPanel(); await user.click(screen.getByRole("button", { name: /add credential/i })); - await user.click(await screen.findByTestId("add-credential-wizard-close")); + await user.click(screen.getByTestId("add-credential-wizard-close")); - await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); + expect(screen.getByText("openai-key")).toBeInTheDocument(); + expect(screen.queryByTestId("add-credential-wizard-close")).not.toBeInTheDocument(); + }); + + it("returns to the credential list from the Back to credentials button", async () => { + const user = userEvent.setup(); + mockUseAuthorized.mockReturnValue({ accessToken: "test-token", userRole: "Admin" }); + mockUseCredentials.mockReturnValue({ data: { credentials }, isLoading: false, refetch: vi.fn() }); + + renderPanel(); + + await user.click(screen.getByRole("button", { name: /add credential/i })); + await user.click(screen.getByRole("button", { name: /back to credentials/i })); + + expect(screen.getByText("openai-key")).toBeInTheDocument(); + expect(screen.queryByTestId("add-credential-wizard-close")).not.toBeInTheDocument(); }); it("drops the masked api key from the update payload while keeping the edited api base", async () => { diff --git a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx index 45d1ce1479d..7a17a175c2e 100644 --- a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx +++ b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { Plus } from "lucide-react"; +import { ArrowLeft, Plus } from "lucide-react"; import { useState } from "react"; import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; @@ -8,7 +8,6 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import AddCredentialWizard from "./add_credential_wizard/AddCredentialWizard"; import { credentialDeleteCall, CredentialItem, credentialUpdateCall } from "@/components/networking"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { stripMaskedSecrets } from "@/utils/maskedSecretUtils"; import { isProxyAdminRole } from "@/utils/roles"; @@ -95,6 +94,21 @@ export default function CredentialsPanel() { setIsDeleteModalOpen(false); }; + if (isAddWizardOpen) { + return ( +