feat(ui): migrate SearchToolView + SSOSettings small components to shadcn

- SearchToolView (SearchTools section): @tremor Button/Card/Grid/Text/
  Title + antd Button + @heroicons ArrowLeftIcon \u2192 shadcn Button/
  Card + plain Tailwind grid + lucide ArrowLeft. Replace 'left-2 z-10'
  positioning hack with simple inline copy buttons.
- RedactableField (SSOSettings): antd Button \u2192 shadcn Button. Raw
  text-gray-* tokens replaced with text-muted-foreground.
- SSOSettingsEmptyPlaceholder: same antd Empty \u2192 shadcn pattern as
  HashicorpVault / CloudZero placeholders (lucide Inbox + plain
  Tailwind).

Test updates:
- SearchToolView.test.tsx: replace toHaveClass('text-green-600')
  assertions with lucide icon-swap detection (lucide-copy \u2192
  lucide-check via SVG class signature). Visual confirmation is now
  conveyed by icon change, not raw color class.

Gates: TS \u2713 | Lint \u2713 | Vitest 19/19 SearchToolView + 101/101 SSO \u2713
| Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 08:12:19 +00:00
parent de34bbda12
commit 8ae6fed101
No known key found for this signature in database
4 changed files with 117 additions and 80 deletions

View file

@ -226,13 +226,18 @@ describe("SearchToolView", () => {
const initialSvg = nameCopyButton!.querySelector("svg");
expect(initialSvg).toBeInTheDocument();
// Pre-click: button has a copy icon (lucide CopyIcon).
const initialIconClass = initialSvg!.getAttribute("class") || "";
await user.click(nameCopyButton!);
// Post-click: button should swap to the check icon. Detect by waiting for
// the SVG class signature to change (lucide-copy \u2192 lucide-check).
await waitFor(() => {
const updatedSvg = nameCopyButton!.querySelector("svg");
expect(updatedSvg).toBeInTheDocument();
expect(nameCopyButton).toHaveClass("text-green-600");
const updatedIconClass = updatedSvg!.getAttribute("class") || "";
expect(updatedIconClass).not.toBe(initialIconClass);
});
});
@ -257,7 +262,11 @@ describe("SearchToolView", () => {
expect(copyToClipboard).toHaveBeenCalledWith("Test Search Tool");
}, { timeout: 3000 });
expect(nameCopyButton).not.toHaveClass("text-green-600");
// Post phase-1 the visual confirmation is conveyed via icon swap,
// not a text-green-600 class. Failed copy: icon does not swap.
const svg = nameCopyButton!.querySelector("svg");
const cls = svg?.getAttribute("class") || "";
expect(cls).toContain("lucide-copy");
});
it("should render SearchToolTester when accessToken is provided", () => {

View file

@ -1,8 +1,7 @@
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import { Button, Card, Grid, Text, Title } from "@tremor/react";
import { Button as AntdButton } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ArrowLeft, CheckIcon, CopyIcon } from "lucide-react";
import React, { useState } from "react";
import { SearchToolTester } from "./SearchToolTester";
import { AvailableSearchProvider, SearchTool } from "./types";
@ -18,13 +17,15 @@ interface SearchToolViewProps {
export const SearchToolView: React.FC<SearchToolViewProps> = ({
searchTool,
onBack,
isEditing,
accessToken,
availableProviders,
}) => {
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
const copyToClipboard = async (text: string | null | undefined, key: string) => {
const copyToClipboard = async (
text: string | null | undefined,
key: string,
) => {
const success = await utilCopyToClipboard(text);
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }));
@ -35,7 +36,9 @@ export const SearchToolView: React.FC<SearchToolViewProps> = ({
};
const getProviderDisplayName = (providerName: string) => {
const provider = availableProviders.find(p => p.provider_name === providerName);
const provider = availableProviders.find(
(p) => p.provider_name === providerName,
);
return provider?.ui_friendly_name || providerName;
};
@ -43,73 +46,98 @@ export const SearchToolView: React.FC<SearchToolViewProps> = ({
<div className="p-4 max-w-full">
<div className="flex justify-between items-center mb-6">
<div>
<Button icon={ArrowLeftIcon} variant="light" className="mb-4" onClick={onBack}>
<Button variant="ghost" size="sm" className="mb-4" onClick={onBack}>
<ArrowLeft className="h-4 w-4" />
Back to All Search Tools
</Button>
<div className="flex items-center cursor-pointer">
<Title>{searchTool.search_tool_name}</Title>
<AntdButton
type="text"
size="small"
icon={copiedStates["search-tool-name"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(searchTool.search_tool_name, "search-tool-name")}
className={`left-2 z-10 transition-all duration-200 ${copiedStates["search-tool-name"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
<div className="flex items-center gap-1">
<h2 className="text-2xl font-semibold">
{searchTool.search_tool_name}
</h2>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() =>
copyToClipboard(
searchTool.search_tool_name,
"search-tool-name",
)
}
aria-label="Copy search tool name"
>
{copiedStates["search-tool-name"] ? (
<CheckIcon size={12} />
) : (
<CopyIcon size={12} />
)}
</Button>
</div>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">{searchTool.search_tool_id}</Text>
<AntdButton
type="text"
size="small"
icon={copiedStates["search-tool-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(searchTool.search_tool_id, "search-tool-id")}
className={`left-2 z-10 transition-all duration-200 ${copiedStates["search-tool-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
<div className="flex items-center gap-1">
<span className="text-muted-foreground font-mono">
{searchTool.search_tool_id}
</span>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() =>
copyToClipboard(searchTool.search_tool_id, "search-tool-id")
}
aria-label="Copy search tool id"
>
{copiedStates["search-tool-id"] ? (
<CheckIcon size={12} />
) : (
<CopyIcon size={12} />
)}
</Button>
</div>
</div>
</div>
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
<Card>
<Text>Provider</Text>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<Card className="p-4">
<p className="text-sm">Provider</p>
<div className="mt-2">
<Title>{getProviderDisplayName(searchTool.litellm_params.search_provider)}</Title>
<h3 className="text-lg font-semibold">
{getProviderDisplayName(
searchTool.litellm_params.search_provider,
)}
</h3>
</div>
</Card>
<Card>
<Text>API Key</Text>
<Card className="p-4">
<p className="text-sm">API Key</p>
<div className="mt-2">
<Text>{searchTool.litellm_params.api_key ? "****" : "Not set"}</Text>
<p className="text-sm">
{searchTool.litellm_params.api_key ? "****" : "Not set"}
</p>
</div>
</Card>
<Card>
<Text>Created At</Text>
<Card className="p-4">
<p className="text-sm">Created At</p>
<div className="mt-2">
<Text>
{searchTool.created_at ? new Date(searchTool.created_at).toLocaleString() : "Unknown"}
</Text>
<p className="text-sm">
{searchTool.created_at
? new Date(searchTool.created_at).toLocaleString()
: "Unknown"}
</p>
</div>
</Card>
</Grid>
</div>
{searchTool.search_tool_info?.description && (
<Card className="mt-6">
<Text>Description</Text>
<Card className="mt-6 p-4">
<p className="text-sm">Description</p>
<div className="mt-2">
<Text>{searchTool.search_tool_info.description}</Text>
<p className="text-sm">{searchTool.search_tool_info.description}</p>
</div>
</Card>
)}
{/* Search Tool Tester */}
<div className="mt-6">
{accessToken && (
<SearchToolTester
@ -121,5 +149,3 @@ export const SearchToolView: React.FC<SearchToolViewProps> = ({
</div>
);
};

View file

@ -1,5 +1,5 @@
import { useState } from "react";
import { Button } from "antd";
import { Button } from "@/components/ui/button";
import { Eye, EyeOff } from "lucide-react";
export default function RedactableField({
@ -13,7 +13,7 @@ export default function RedactableField({
return (
<div className="flex items-center gap-2">
<span className="font-mono text-gray-600 flex-1">
<span className="font-mono text-muted-foreground flex-1">
{value ? (
isHidden ? (
"•".repeat(value.length)
@ -21,17 +21,23 @@ export default function RedactableField({
value
)
) : (
<span className="text-gray-400 italic">Not configured</span>
<span className="text-muted-foreground italic">Not configured</span>
)}
</span>
{value && (
<Button
type="text"
size="small"
icon={isHidden ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => setIsHidden(!isHidden)}
className="text-gray-400 hover:text-gray-600"
/>
aria-label={isHidden ? "Show value" : "Hide value"}
>
{isHidden ? (
<Eye className="w-4 h-4" />
) : (
<EyeOff className="w-4 h-4" />
)}
</Button>
)}
</div>
);

View file

@ -1,30 +1,26 @@
import { Empty, Typography, Button } from "antd";
const { Title, Paragraph } = Typography;
import { Button } from "@/components/ui/button";
import { Inbox } from "lucide-react";
interface SSOSettingsEmptyPlaceholderProps {
onAdd: () => void;
}
export default function SSOSettingsEmptyPlaceholder({ onAdd }: SSOSettingsEmptyPlaceholderProps) {
export default function SSOSettingsEmptyPlaceholder({
onAdd,
}: SSOSettingsEmptyPlaceholderProps) {
return (
<div className="bg-white p-12 rounded-lg border border-dashed border-gray-300 text-center w-full">
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<div className="space-y-2">
<Title level={4}>No SSO Configuration Found</Title>
<Paragraph type="secondary" className="max-w-md mx-auto">
Configure Single Sign-On (SSO) to enable seamless authentication for your team members using your identity
provider.
</Paragraph>
</div>
}
>
<Button type="primary" size="large" onClick={onAdd} className="flex items-center gap-2 mx-auto mt-4">
<div className="bg-background p-12 rounded-lg border border-dashed border-border text-center w-full">
<div className="flex flex-col items-center gap-3">
<Inbox className="h-12 w-12 text-muted-foreground" />
<h4 className="text-lg font-semibold">No SSO Configuration Found</h4>
<p className="text-sm text-muted-foreground max-w-md mx-auto">
Configure Single Sign-On (SSO) to enable seamless authentication for
your team members using your identity provider.
</p>
<Button onClick={onAdd} className="mt-4">
Configure SSO
</Button>
</Empty>
</div>
</div>
);
}