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
This commit is contained in:
mateo-berri 2026-09-05 17:54:47 -07:00
parent 6ab64f0229
commit 40622e27d7
2 changed files with 42 additions and 24 deletions

View file

@ -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 () => {

View file

@ -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 (
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 overflow-y-auto p-2">
<div className="flex items-center justify-between gap-4">
<h2 className="text-2xl font-semibold text-foreground">Add Credential</h2>
<Button variant="ghost" onClick={() => setIsAddWizardOpen(false)}>
<ArrowLeft className="size-4" />
Back to credentials
</Button>
</div>
<AddCredentialWizard onClose={() => setIsAddWizardOpen(false)} />
</div>
);
}
return (
<div className="mx-auto flex w-full flex-auto flex-col gap-4 overflow-y-auto p-2">
<div className="flex items-center justify-between gap-4">
@ -117,16 +131,6 @@ export default function CredentialsPanel() {
isLoading={isLoading}
/>
{isAddWizardOpen && (
<Dialog open onOpenChange={(open) => !open && setIsAddWizardOpen(false)}>
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Add Credential</DialogTitle>
</DialogHeader>
<AddCredentialWizard onClose={() => setIsAddWizardOpen(false)} />
</DialogContent>
</Dialog>
)}
{isUpdateModalOpen && (
<CredentialModal
mode="edit"