feat(ui): migrate small top-level components to shadcn

- query_param_input: antd Button/Space + @ant-design/icons \u2192 shadcn
  Button/Input + lucide MinusCircle/Plus, dashed-outline add button.
- object_permissions_view: @tremor Text \u2192 plain <p> with semantic tokens.
- route_preview: antd Card/Typography + @ant-design/icons ChevronRight/
  InfoCircle \u2192 shadcn Card + lucide ChevronRight/Info.
- skill_hub_table_columns: @tremor Badge/Text + antd Tooltip + @ant-design/
  icons CopyOutlined/LinkOutlined \u2192 shadcn Badge/Tooltip + lucide
  Copy/Link. Added to overrides for the categorical blue/emerald badge
  palette.
- claude_code_plugins: @tremor Button + antd Modal \u2192 shadcn Button +
  AlertDialog (destructive variant for the Delete action).

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 10:29:50 +00:00
parent 38175a15a5
commit 32392518f6
No known key found for this signature in database
6 changed files with 223 additions and 128 deletions

View file

@ -43,7 +43,8 @@
"src/components/mcp_tools/ByokCredentialModal.tsx", "src/components/mcp_tools/ByokCredentialModal.tsx",
"src/components/shared/usage_date_picker.tsx", "src/components/shared/usage_date_picker.tsx",
"src/components/shared/advanced_date_picker.tsx", "src/components/shared/advanced_date_picker.tsx",
"src/components/activity_metrics.tsx" "src/components/activity_metrics.tsx",
"src/components/skill_hub_table_columns.tsx"
], ],
"rules": { "rules": {
"litellm-ui/no-banned-ui-imports": "off", "litellm-ui/no-banned-ui-imports": "off",

View file

@ -1,6 +1,15 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Button } from "@tremor/react"; import { Button } from "@/components/ui/button";
import { Modal } from "antd"; import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { import {
getClaudeCodePluginsList, getClaudeCodePluginsList,
deleteClaudeCodePlugin, deleteClaudeCodePlugin,
@ -40,7 +49,7 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({
try { try {
const response: ListPluginsResponse = await getClaudeCodePluginsList( const response: ListPluginsResponse = await getClaudeCodePluginsList(
accessToken, accessToken,
false false,
); );
setPluginsList(response.plugins); setPluginsList(response.plugins);
} catch (error) { } catch (error) {
@ -52,6 +61,7 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({
useEffect(() => { useEffect(() => {
fetchPlugins(); fetchPlugins();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accessToken]); }, [accessToken]);
const handleDeleteClick = (pluginName: string, displayName: string) => { const handleDeleteClick = (pluginName: string, displayName: string) => {
@ -64,7 +74,9 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({
setIsDeleting(true); setIsDeleting(true);
try { try {
await deleteClaudeCodePlugin(accessToken, pluginToDelete.name); await deleteClaudeCodePlugin(accessToken, pluginToDelete.name);
NotificationsManager.success(`Skill "${pluginToDelete.displayName}" deleted successfully`); NotificationsManager.success(
`Skill "${pluginToDelete.displayName}" deleted successfully`,
);
fetchPlugins(); fetchPlugins();
} catch (error) { } catch (error) {
console.error("Error deleting skill:", error); console.error("Error deleting skill:", error);
@ -89,13 +101,19 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({
<> <>
<div className="flex flex-col gap-2 mb-4"> <div className="flex flex-col gap-2 mb-4">
<h1 className="text-2xl font-bold">Skills</h1> <h1 className="text-2xl font-bold">Skills</h1>
<p className="text-sm text-gray-600"> <p className="text-sm text-muted-foreground">
Register Claude Code skills. Published skills appear in the Skill Hub for all users and Register Claude Code skills. Published skills appear in the Skill
are served via{" "} Hub for all users and are served via{" "}
<code className="bg-gray-100 px-1 rounded">/claude-code/marketplace.json</code>. <code className="bg-muted px-1 rounded">
/claude-code/marketplace.json
</code>
.
</p> </p>
<div className="mt-2 flex gap-2"> <div className="mt-2 flex gap-2">
<Button onClick={() => setIsAddModalVisible(true)} disabled={!accessToken || !isAdmin}> <Button
onClick={() => setIsAddModalVisible(true)}
disabled={!accessToken || !isAdmin}
>
+ Add Skill + Add Skill
</Button> </Button>
</div> </div>
@ -122,23 +140,34 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({
onSuccess={fetchPlugins} onSuccess={fetchPlugins}
/> />
{pluginToDelete && ( <AlertDialog
<Modal open={pluginToDelete !== null}
title="Delete Skill" onOpenChange={(o) => (!o ? setPluginToDelete(null) : undefined)}
open={pluginToDelete !== null} >
onOk={handleDeleteConfirm} <AlertDialogContent>
onCancel={() => setPluginToDelete(null)} <AlertDialogHeader>
confirmLoading={isDeleting} <AlertDialogTitle>Delete Skill</AlertDialogTitle>
okText="Delete" <AlertDialogDescription>
okButtonProps={{ danger: true }} Are you sure you want to delete skill:{" "}
> <strong>{pluginToDelete?.displayName}</strong>? This action cannot
<p> be undone.
Are you sure you want to delete skill:{" "} </AlertDialogDescription>
<strong>{pluginToDelete.displayName}</strong>? </AlertDialogHeader>
</p> <AlertDialogFooter>
<p>This action cannot be undone.</p> <AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
</Modal> <AlertDialogAction
)} disabled={isDeleting}
onClick={(e) => {
e.preventDefault();
handleDeleteConfirm();
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? "Deleting…" : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
); );
}; };

View file

@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import { Text } from "@tremor/react";
import VectorStorePermissions from "./permissions/VectorStorePermissions"; import VectorStorePermissions from "./permissions/VectorStorePermissions";
import MCPServerPermissions from "./permissions/MCPServerPermissions"; import MCPServerPermissions from "./permissions/MCPServerPermissions";
import AgentPermissions from "./permissions/AgentPermissions"; import AgentPermissions from "./permissions/AgentPermissions";
@ -56,11 +55,15 @@ export function ObjectPermissionsView({
if (variant === "card") { if (variant === "card") {
return ( return (
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}> <div
className={`bg-background border border-border rounded-lg p-6 ${className}`}
>
<div className="flex items-center gap-2 mb-6"> <div className="flex items-center gap-2 mb-6">
<div> <div>
<Text className="font-semibold text-gray-900">Object Permissions</Text> <p className="font-semibold text-foreground">Object Permissions</p>
<Text className="text-xs text-gray-500">Access control for Vector Stores and MCP Servers</Text> <p className="text-xs text-muted-foreground">
Access control for Vector Stores and MCP Servers
</p>
</div> </div>
</div> </div>
{content} {content}
@ -70,7 +73,7 @@ export function ObjectPermissionsView({
return ( return (
<div className={`${className}`}> <div className={`${className}`}>
<Text className="font-medium text-gray-900 mb-3">Object Permissions</Text> <p className="font-medium text-foreground mb-3">Object Permissions</p>
{content} {content}
</div> </div>
); );

View file

@ -1,15 +1,20 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, Space } from "antd"; import { Button } from "@/components/ui/button";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; import { Input } from "@/components/ui/input";
import { TextInput } from "@tremor/react"; import { MinusCircle, Plus } from "lucide-react";
interface QueryParamInputProps { interface QueryParamInputProps {
value?: Record<string, string>; value?: Record<string, string>;
onChange?: (value: Record<string, string>) => void; onChange?: (value: Record<string, string>) => void;
} }
const QueryParamInput: React.FC<QueryParamInputProps> = ({ value = {}, onChange }) => { const QueryParamInput: React.FC<QueryParamInputProps> = ({
const [pairs, setPairs] = useState<[string, string][]>(Object.entries(value)); value = {},
onChange,
}) => {
const [pairs, setPairs] = useState<[string, string][]>(
Object.entries(value),
);
const handleAdd = () => { const handleAdd = () => {
setPairs([...pairs, ["", ""]]); setPairs([...pairs, ["", ""]]);
@ -29,25 +34,36 @@ const QueryParamInput: React.FC<QueryParamInputProps> = ({ value = {}, onChange
}; };
return ( return (
<div> <div className="space-y-2">
{pairs.map(([key, val], index) => ( {pairs.map(([key, val], index) => (
<Space key={index} style={{ display: "flex", marginBottom: 8 }} align="center"> <div key={index} className="flex items-center gap-2">
<TextInput <Input
placeholder="Parameter Name (e.g., version)" placeholder="Parameter Name (e.g., version)"
value={key} value={key}
onChange={(e) => handleChange(index, e.target.value, val)} onChange={(e) => handleChange(index, e.target.value, val)}
/> />
<TextInput <Input
placeholder="Parameter Value (e.g., v1)" placeholder="Parameter Value (e.g., v1)"
value={val} value={val}
onChange={(e) => handleChange(index, key, e.target.value)} onChange={(e) => handleChange(index, key, e.target.value)}
/> />
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%" }}> <button
<MinusCircleOutlined onClick={() => handleRemove(index)} style={{ cursor: "pointer" }} /> type="button"
</div> onClick={() => handleRemove(index)}
</Space> className="shrink-0 text-muted-foreground hover:text-destructive"
aria-label={`Remove query parameter ${index + 1}`}
>
<MinusCircle className="h-4 w-4" />
</button>
</div>
))} ))}
<Button type="dashed" onClick={handleAdd} icon={<PlusOutlined />}> <Button
type="button"
variant="outline"
onClick={handleAdd}
className="border-dashed"
>
<Plus className="h-4 w-4" />
Add Query Parameter Add Query Parameter
</Button> </Button>
</div> </div>

View file

@ -1,8 +1,6 @@
import React from "react"; import React from "react";
import { Card, Typography } from "antd"; import { Card } from "@/components/ui/card";
import { RightOutlined, InfoCircleOutlined } from "@ant-design/icons"; import { ChevronRight, Info } from "lucide-react";
const { Title, Text } = Typography;
import { getProxyBaseUrl } from "./networking"; import { getProxyBaseUrl } from "./networking";
interface RoutePreviewProps { interface RoutePreviewProps {
@ -11,94 +9,114 @@ interface RoutePreviewProps {
includeSubpath: boolean; includeSubpath: boolean;
} }
const RoutePreview: React.FC<RoutePreviewProps> = ({ pathValue, targetValue, includeSubpath }) => { const RoutePreview: React.FC<RoutePreviewProps> = ({
pathValue,
targetValue,
includeSubpath,
}) => {
const proxyBaseUrl = getProxyBaseUrl(); const proxyBaseUrl = getProxyBaseUrl();
const getLiteLLMProxyUrl = () => { const getLiteLLMProxyUrl = () =>
return pathValue ? `${proxyBaseUrl}${pathValue}` : ""; pathValue ? `${proxyBaseUrl}${pathValue}` : "";
};
// Only show if both path and target are provided
if (!pathValue || !targetValue) { if (!pathValue || !targetValue) {
return null; return null;
} }
return ( return (
<Card className="p-5"> <Card className="p-5">
<Title level={5} className="text-lg font-semibold text-gray-900 mb-2">Route Preview</Title> <h5 className="text-lg font-semibold text-foreground mb-2">
<Text type="secondary" className="text-gray-600 mb-5" style={{ display: "block" }}>How your requests will be routed</Text> Route Preview
</h5>
<p className="text-muted-foreground mb-5 text-sm">
How your requests will be routed
</p>
<div className="space-y-5"> <div className="space-y-5">
{/* Basic routing */}
<div> <div>
<div className="text-base font-semibold text-gray-900 mb-3">Basic routing:</div> <div className="text-base font-semibold text-foreground mb-3">
Basic routing:
</div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* Your endpoint */} <div className="flex-1 bg-muted border border-border rounded-lg p-3">
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-lg p-3"> <div className="text-sm text-muted-foreground mb-2">
<div className="text-sm text-gray-600 mb-2">Your endpoint</div> Your endpoint
<code className="font-mono text-sm text-gray-900">{getLiteLLMProxyUrl()}</code> </div>
<code className="font-mono text-sm text-foreground">
{getLiteLLMProxyUrl()}
</code>
</div> </div>
{/* Arrow */} <div className="text-muted-foreground">
<div className="text-gray-400"> <ChevronRight className="h-5 w-5" />
<RightOutlined className="text-lg" />
</div> </div>
{/* Forwards to */} <div className="flex-1 bg-muted border border-border rounded-lg p-3">
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-lg p-3"> <div className="text-sm text-muted-foreground mb-2">
<div className="text-sm text-gray-600 mb-2">Forwards to</div> Forwards to
<code className="font-mono text-sm text-gray-900">{targetValue}</code> </div>
<code className="font-mono text-sm text-foreground">
{targetValue}
</code>
</div> </div>
</div> </div>
</div> </div>
{includeSubpath && ( {includeSubpath && (
<> <div>
{/* With subpaths */} <div className="text-base font-semibold text-foreground mb-3">
<div> With subpaths:
<div className="text-base font-semibold text-gray-900 mb-3">With subpaths:</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* Your endpoint + subpath */} <div className="flex-1 bg-muted border border-border rounded-lg p-3">
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-lg p-3"> <div className="text-sm text-muted-foreground mb-2">
<div className="text-sm text-gray-600 mb-2">Your endpoint + subpath</div> Your endpoint + subpath
<code className="font-mono text-sm text-gray-900">
{pathValue && `${proxyBaseUrl}${pathValue}`}
<span className="text-blue-600">/v1/text-to-image/base/model</span>
</code>
</div>
{/* Arrow */}
<div className="text-gray-400">
<RightOutlined className="text-lg" />
</div>
{/* Forwards to with subpath */}
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-lg p-3">
<div className="text-sm text-gray-600 mb-2">Forwards to</div>
<code className="font-mono text-sm text-gray-900">
{targetValue}
<span className="text-blue-600">/v1/text-to-image/base/model</span>
</code>
</div> </div>
<code className="font-mono text-sm text-foreground">
{pathValue && `${proxyBaseUrl}${pathValue}`}
<span className="text-primary">
/v1/text-to-image/base/model
</span>
</code>
</div> </div>
{/* Note */} <div className="text-muted-foreground">
<div className="mt-3 text-sm text-gray-600"> <ChevronRight className="h-5 w-5" />
Any path after {pathValue} will be appended to the target URL </div>
<div className="flex-1 bg-muted border border-border rounded-lg p-3">
<div className="text-sm text-muted-foreground mb-2">
Forwards to
</div>
<code className="font-mono text-sm text-foreground">
{targetValue}
<span className="text-primary">
/v1/text-to-image/base/model
</span>
</code>
</div> </div>
</div> </div>
</>
<div className="mt-3 text-sm text-muted-foreground">
Any path after {pathValue} will be appended to the target URL
</div>
</div>
)} )}
{!includeSubpath && ( {!includeSubpath && (
<div className="mt-4 p-3 bg-blue-50 rounded-md border border-blue-200"> <div className="mt-4 p-3 bg-primary/5 rounded-md border border-primary/20">
<div className="flex items-start"> <div className="flex items-start">
<InfoCircleOutlined className="text-blue-500 mt-0.5 mr-2 flex-shrink-0" /> <Info className="text-primary mt-0.5 mr-2 h-4 w-4 flex-shrink-0" />
<div className="text-sm text-blue-700"> <div className="text-sm text-primary">
<span className="font-medium">Not seeing the routing you wanted?</span> Try enabling - Include Subpaths <span className="font-medium">
- above - this allows subroutes like{" "} Not seeing the routing you wanted?
<code className="bg-blue-100 px-1 py-0.5 rounded font-mono text-xs">/api/v1/models</code> to be </span>{" "}
forwarded automatically. Try enabling - Include Subpaths - above - this allows subroutes
like{" "}
<code className="bg-primary/10 px-1 py-0.5 rounded font-mono text-xs">
/api/v1/models
</code>{" "}
to be forwarded automatically.
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,12 +1,18 @@
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { Badge, Text } from "@tremor/react"; import { Badge } from "@/components/ui/badge";
import { Tooltip } from "antd"; import {
import { CopyOutlined, LinkOutlined } from "@ant-design/icons"; Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Copy, Link as LinkIcon } from "lucide-react";
import { Plugin } from "./claude_code_plugins/types"; import { Plugin } from "./claude_code_plugins/types";
export const skillHubColumns = ( export const skillHubColumns = (
showModal: (skill: Plugin) => void, showModal: (skill: Plugin) => void,
copyToClipboard: (text: string) => void, copyToClipboard: (text: string) => void,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
publicPage: boolean = false, publicPage: boolean = false,
): ColumnDef<Plugin>[] => [ ): ColumnDef<Plugin>[] => [
{ {
@ -21,22 +27,31 @@ export const skillHubColumns = (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
type="button" type="button"
className="font-medium text-sm cursor-pointer text-blue-600 hover:underline bg-transparent border-none p-0" className="font-medium text-sm cursor-pointer text-primary hover:underline bg-transparent border-none p-0"
onClick={() => showModal(skill)} onClick={() => showModal(skill)}
> >
{skill.name} {skill.name}
</button> </button>
<Tooltip title="Copy skill name"> <TooltipProvider>
<CopyOutlined <Tooltip>
onClick={() => copyToClipboard(skill.name)} <TooltipTrigger asChild>
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs" <button
/> type="button"
</Tooltip> onClick={() => copyToClipboard(skill.name)}
className="cursor-pointer text-muted-foreground hover:text-primary"
aria-label="Copy skill name"
>
<Copy className="h-3 w-3" />
</button>
</TooltipTrigger>
<TooltipContent>Copy skill name</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
{skill.description && ( {skill.description && (
<Text className="text-xs text-gray-500 line-clamp-1 md:hidden"> <p className="text-xs text-muted-foreground line-clamp-1 md:hidden">
{skill.description} {skill.description}
</Text> </p>
)} )}
</div> </div>
); );
@ -47,7 +62,9 @@ export const skillHubColumns = (
accessorKey: "description", accessorKey: "description",
enableSorting: false, enableSorting: false,
cell: ({ row }) => ( cell: ({ row }) => (
<Text className="text-xs line-clamp-2">{row.original.description || "-"}</Text> <p className="text-xs line-clamp-2">
{row.original.description || "-"}
</p>
), ),
}, },
{ {
@ -56,8 +73,12 @@ export const skillHubColumns = (
enableSorting: true, enableSorting: true,
cell: ({ row }) => { cell: ({ row }) => {
const cat = row.original.category; const cat = row.original.category;
if (!cat) return <Text className="text-xs text-gray-400">-</Text>; if (!cat) return <span className="text-xs text-muted-foreground">-</span>;
return <Badge color="blue" size="xs">{cat}</Badge>; return (
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 text-xs">
{cat}
</Badge>
);
}, },
}, },
{ {
@ -65,7 +86,7 @@ export const skillHubColumns = (
accessorKey: "domain", accessorKey: "domain",
enableSorting: true, enableSorting: true,
cell: ({ row }) => ( cell: ({ row }) => (
<Text className="text-xs">{row.original.domain || "-"}</Text> <span className="text-xs">{row.original.domain || "-"}</span>
), ),
}, },
{ {
@ -86,17 +107,18 @@ export const skillHubColumns = (
url = src.url; url = src.url;
label = src.url.replace(/^https?:\/\//, ""); label = src.url.replace(/^https?:\/\//, "");
} }
if (!url) return <Text className="text-xs text-gray-400">-</Text>; if (!url)
return <span className="text-xs text-muted-foreground">-</span>;
return ( return (
<a <a
href={url} href={url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-blue-600 hover:underline truncate max-w-[180px]" className="flex items-center gap-1 text-xs text-primary hover:underline truncate max-w-[180px]"
title={label} title={label}
> >
<span className="truncate">{label}</span> <span className="truncate">{label}</span>
<LinkOutlined className="shrink-0" style={{ fontSize: 10 }} /> <LinkIcon className="h-2.5 w-2.5 shrink-0" />
</a> </a>
); );
}, },
@ -106,7 +128,13 @@ export const skillHubColumns = (
accessorKey: "enabled", accessorKey: "enabled",
enableSorting: true, enableSorting: true,
cell: ({ row }) => ( cell: ({ row }) => (
<Badge color={row.original.enabled ? "green" : "gray"} size="xs"> <Badge
className={
row.original.enabled
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300 text-xs"
: "bg-muted text-muted-foreground text-xs"
}
>
{row.original.enabled ? "Public" : "Draft"} {row.original.enabled ? "Public" : "Draft"}
</Badge> </Badge>
), ),