mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
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:
parent
de34bbda12
commit
8ae6fed101
4 changed files with 117 additions and 80 deletions
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue