chore(ui): remove dead dashboard files and unused dependencies (#30047)

* chore(ui): remove dead dashboard files and unused dependencies

knip flagged seven orphaned source/config files with no importers and
five declared dependencies that nothing in the tree uses. Removing them
shrinks the dashboard bundle's source surface and keeps the manifest
honest; vite stays installed transitively via vitest, so test tooling is
unaffected.

* fix(ci): restore serverRootPath.config.ts referenced by SERVER_ROOT_PATH workflow

The dead-code sweep removed e2e_tests/serverRootPath.config.ts, but its spec
(tests/login/serverRootPathRedirect.spec.ts) and the test_server_root_path.yml
workflow step still depend on it, so the redirect e2e job failed to load a
config that no longer existed.
This commit is contained in:
ryan-crabbe-berri 2026-06-09 17:54:38 -07:00 committed by GitHub
parent 248176112e
commit 9e0d92c129
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 0 additions and 1220 deletions

View File

@ -11,7 +11,6 @@
"@anthropic-ai/sdk": "0.92.0",
"@headlessui/tailwindcss": "0.2.2",
"@heroicons/react": "1.0.6",
"@remixicon/react": "4.9.0",
"@tanstack/react-pacer": "0.2.0",
"@tanstack/react-query": "5.100.7",
"@tanstack/react-table": "8.21.3",
@ -44,18 +43,15 @@
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
"@types/babel__traverse": "7.28.0",
"@types/lodash": "4.17.23",
"@types/node": "20.19.37",
"@types/react": "18.2.48",
"@types/react-copy-to-clipboard": "5.0.7",
"@types/react-dom": "18.3.7",
"@types/react-syntax-highlighter": "15.5.13",
"@types/uuid": "10.0.0",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"autoprefixer": "10.4.24",
"dotenv": "17.2.3",
"eslint": "9.39.2",
"eslint-config-next": "16.2.6",
"eslint-config-prettier": "10.1.8",
@ -68,7 +64,6 @@
"tailwindcss": "3.4.19",
"typescript": "5.9.3",
"typescript-eslint": "8.60.1",
"vite": "7.3.2",
"vitest": "3.2.4"
},
"engines": {
@ -2847,15 +2842,6 @@
"npm": ">=9.5.0"
}
},
"node_modules/@remixicon/react": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@remixicon/react/-/react-4.9.0.tgz",
"integrity": "sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q==",
"license": "Remix Icon License 1.0",
"peerDependencies": {
"react": ">=18.2.0"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
@ -3490,16 +3476,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__traverse": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@ -3737,13 +3713,6 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
@ -5912,19 +5881,6 @@
"csstype": "^3.0.2"
}
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@ -26,7 +26,6 @@
"@anthropic-ai/sdk": "0.92.0",
"@headlessui/tailwindcss": "0.2.2",
"@heroicons/react": "1.0.6",
"@remixicon/react": "4.9.0",
"@tanstack/react-pacer": "0.2.0",
"@tanstack/react-query": "5.100.7",
"@tanstack/react-table": "8.21.3",
@ -59,18 +58,15 @@
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
"@types/babel__traverse": "7.28.0",
"@types/lodash": "4.17.23",
"@types/node": "20.19.37",
"@types/react": "18.2.48",
"@types/react-copy-to-clipboard": "5.0.7",
"@types/react-dom": "18.3.7",
"@types/react-syntax-highlighter": "15.5.13",
"@types/uuid": "10.0.0",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"autoprefixer": "10.4.24",
"dotenv": "17.2.3",
"eslint": "9.39.2",
"eslint-config-next": "16.2.6",
"eslint-config-prettier": "10.1.8",
@ -83,7 +79,6 @@
"tailwindcss": "3.4.19",
"typescript": "5.9.3",
"typescript-eslint": "8.60.1",
"vite": "7.3.2",
"vitest": "3.2.4"
},
"overrides": {

View File

@ -1,140 +0,0 @@
import { SearchOutlined } from "@ant-design/icons";
import { Card, Tab, TabGroup, TabList, TabPanel, TabPanels, Text } from "@tremor/react";
import { Input } from "antd";
import React, { useEffect, useMemo, useState } from "react";
import { extractCategories, filterPluginsByCategory, filterPluginsBySearch } from "../claude_code_plugins/helpers";
import { MarketplaceResponse } from "../claude_code_plugins/types";
import { ModelDataTable } from "../model_dashboard/table";
import NotificationsManager from "../molecules/notifications_manager";
import { getClaudeCodeMarketplace } from "../networking";
import { getMarketplaceTableColumns } from "./marketplace_table_columns";
interface ClaudeCodeMarketplaceTabProps {
publicPage?: boolean;
}
const ClaudeCodeMarketplaceTab: React.FC<ClaudeCodeMarketplaceTabProps> = ({ publicPage = false }) => {
const [marketplaceData, setMarketplaceData] = useState<MarketplaceResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState(0);
useEffect(() => {
fetchMarketplace();
}, []);
const fetchMarketplace = async () => {
setIsLoading(true);
try {
const data: MarketplaceResponse = await getClaudeCodeMarketplace();
console.log("Claude Code marketplace:", data);
setMarketplaceData(data);
} catch (error) {
console.error("Error fetching marketplace:", error);
} finally {
setIsLoading(false);
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
NotificationsManager.success("Copied to clipboard!");
};
// Extract unique categories from plugins
const categories = useMemo(() => {
if (!marketplaceData) return ["All"];
return extractCategories(marketplaceData.plugins);
}, [marketplaceData]);
// Get selected category name
const selectedCategory = categories[selectedCategoryIndex] || "All";
// Filter plugins by search and category
const filteredPlugins = useMemo(() => {
if (!marketplaceData) return [];
let plugins = marketplaceData.plugins;
// Apply category filter
plugins = filterPluginsByCategory(plugins, selectedCategory);
// Apply search filter
plugins = filterPluginsBySearch(plugins, searchTerm);
return plugins;
}, [marketplaceData, selectedCategory, searchTerm]);
const columns = useMemo(() => getMarketplaceTableColumns(copyToClipboard, publicPage), [publicPage]);
if (!marketplaceData && !isLoading) {
return (
<Card>
<div className="text-center p-12">
<Text className="text-gray-500">Failed to load marketplace. Please try again later.</Text>
</div>
</Card>
);
}
return (
<div className="space-y-4">
{/* Search Bar */}
<div className="max-w-md">
<Input
placeholder="Search plugins by name, description, or keywords..."
prefix={<SearchOutlined className="text-gray-400" />}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
allowClear
size="large"
/>
</div>
{/* Category Tabs */}
<TabGroup index={selectedCategoryIndex} onIndexChange={setSelectedCategoryIndex}>
<TabList className="mb-4">
{categories.map((category) => {
// Count plugins in this category
const categoryPlugins = filterPluginsByCategory(marketplaceData?.plugins || [], category);
const count = filterPluginsBySearch(categoryPlugins, searchTerm).length;
return (
<Tab key={category}>
{category} {count > 0 && `(${count})`}
</Tab>
);
})}
</TabList>
<TabPanels>
{categories.map((category) => (
<TabPanel key={category}>
<Card>
{/* Plugin Table */}
<ModelDataTable
columns={columns}
data={filteredPlugins}
isLoading={isLoading}
defaultSorting={[{ id: "name", desc: false }]}
/>
</Card>
{/* Footer Info */}
<div className="mt-4 text-center space-y-2">
<Text className="text-sm text-gray-600">
Showing {filteredPlugins.length} of {marketplaceData?.plugins.length || 0} plugin
{marketplaceData?.plugins.length !== 1 ? "s" : ""}
{searchTerm && ` matching "${searchTerm}"`}
{selectedCategory !== "All" && ` in ${selectedCategory}`}
</Text>
</div>
</TabPanel>
))}
</TabPanels>
</TabGroup>
</div>
);
};
export default ClaudeCodeMarketplaceTab;

View File

@ -1,172 +0,0 @@
import { ColumnDef } from "@tanstack/react-table";
import { Button, Badge, Text } from "@tremor/react";
import { Tooltip } from "antd";
import { CopyOutlined } from "@ant-design/icons";
import { MarketplacePluginEntry } from "@/components/claude_code_plugins/types";
import {
formatInstallCommand,
getCategoryBadgeColor,
getSourceDisplayText,
} from "@/components/claude_code_plugins/helpers";
export const getMarketplaceTableColumns = (
copyToClipboard: (text: string) => void,
publicPage: boolean = false,
): ColumnDef<MarketplacePluginEntry>[] => {
const allColumns: ColumnDef<MarketplacePluginEntry>[] = [
{
header: "Plugin Name",
accessorKey: "name",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const plugin = row.original;
const installCommand = formatInstallCommand(plugin);
return (
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Text className="font-medium text-sm">{plugin.name}</Text>
<Tooltip title="Copy install command">
<CopyOutlined
onClick={() => copyToClipboard(installCommand)}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
</Tooltip>
</div>
{/* Show description on mobile */}
<div className="md:hidden">
<Text className="text-xs text-gray-600">{plugin.description || "No description"}</Text>
</div>
</div>
);
},
},
{
header: "Description",
accessorKey: "description",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const plugin = row.original;
return <Text className="text-xs line-clamp-2">{plugin.description || "-"}</Text>;
},
meta: {
className: "hidden md:table-cell",
},
},
{
header: "Version",
accessorKey: "version",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const plugin = row.original;
return plugin.version ? (
<Badge color="blue" size="sm">
v{plugin.version}
</Badge>
) : (
<Text className="text-xs text-gray-400">-</Text>
);
},
meta: {
className: "hidden lg:table-cell",
},
},
{
header: "Category",
accessorKey: "category",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const plugin = row.original;
const badgeColor = getCategoryBadgeColor(plugin.category);
return plugin.category ? (
<Badge color={badgeColor} size="sm">
{plugin.category}
</Badge>
) : (
<Badge color="gray" size="sm">
Uncategorized
</Badge>
);
},
meta: {
className: "hidden lg:table-cell",
},
},
{
header: "Source",
accessorKey: "source",
enableSorting: false,
cell: ({ row }) => {
const plugin = row.original;
const sourceText = getSourceDisplayText(plugin.source);
return <Text className="text-xs text-gray-600">{sourceText}</Text>;
},
meta: {
className: "hidden xl:table-cell",
},
},
{
header: "Keywords",
accessorKey: "keywords",
enableSorting: false,
cell: ({ row }) => {
const plugin = row.original;
const keywords = plugin.keywords?.slice(0, 3) || [];
const remaining = (plugin.keywords?.length || 0) - 3;
return (
<div className="flex flex-wrap gap-1">
{keywords.map((keyword, index) => (
<Badge key={index} color="gray" size="xs">
{keyword}
</Badge>
))}
{remaining > 0 && (
<Badge color="gray" size="xs">
+{remaining}
</Badge>
)}
</div>
);
},
meta: {
className: "hidden xl:table-cell",
},
},
{
header: "Install Command",
id: "install_command",
enableSorting: false,
cell: ({ row }) => {
const plugin = row.original;
const installCommand = formatInstallCommand(plugin);
return (
<div className="flex items-center space-x-2">
<code className="text-xs bg-gray-100 px-2 py-1 rounded font-mono truncate max-w-[200px]">
{installCommand}
</code>
<Tooltip title="Copy command">
<Button
size="xs"
variant="secondary"
icon={CopyOutlined}
onClick={() => copyToClipboard(installCommand)}
/>
</Tooltip>
</div>
);
},
},
];
return allColumns;
};

View File

@ -1,14 +0,0 @@
export interface Project {
id: string;
name: string;
description: string;
teamId: string;
teamAlias: string;
models: string[];
status: "active" | "blocked";
spend: number;
createdAt: string;
createdBy: string;
updatedAt: string;
updatedBy: string;
}

View File

@ -1,211 +0,0 @@
import React, { useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Button } from "@tremor/react";
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, TrashIcon } from "@heroicons/react/outline";
import { Tooltip } from "antd";
import { CopyOutlined } from "@ant-design/icons";
import { Agent } from "./types";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
interface AgentTableProps {
agentsList: Agent[];
isLoading: boolean;
onDeleteClick: (agentId: string, agentName: string) => void;
accessToken: string | null;
onAgentUpdated: () => void;
isAdmin: boolean;
onAgentClick: (agentId: string) => void;
}
const AgentTable: React.FC<AgentTableProps> = ({
agentsList,
isLoading,
onDeleteClick,
accessToken,
onAgentUpdated,
isAdmin,
onAgentClick,
}) => {
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const columns: ColumnDef<Agent>[] = [
{
header: "Agent Name",
accessorKey: "agent_name",
cell: ({ row }) => {
const agent = row.original;
const name = agent.agent_name || "";
return (
<div className="flex items-center gap-2">
<Tooltip title={name}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate min-w-[200px] justify-start"
onClick={() => onAgentClick(agent.agent_id)}
>
{name}
</Button>
</Tooltip>
<Tooltip title="Copy Agent ID">
<CopyOutlined
onClick={(e) => {
e.stopPropagation();
copyToClipboard(agent.agent_id);
}}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
</Tooltip>
</div>
);
},
},
{
header: "Description",
accessorKey: "agent_card_params.description",
cell: ({ row }) => {
const description = row.original.agent_card_params?.description || "No description";
return <span className="text-xs text-gray-600 block max-w-[300px] truncate">{description}</span>;
},
},
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => {
const agent = row.original;
return (
<Tooltip title={agent.created_at}>
<span className="text-xs">{formatDate(agent.created_at)}</span>
</Tooltip>
);
},
},
...(isAdmin
? [
{
header: "Actions",
id: "actions",
enableSorting: false,
cell: ({ row }: any) => {
const agent = row.original;
return (
<div className="flex items-center gap-1">
<Tooltip title="Delete agent">
<Button
size="xs"
variant="light"
color="red"
onClick={(e) => {
e.stopPropagation();
onDeleteClick(agent.agent_id, agent.agent_name);
}}
icon={TrashIcon}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
/>
</Tooltip>
</div>
);
},
},
]
: []),
];
const table = useReactTable({
data: agentsList,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
});
return (
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className="py-1 h-8"
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</div>
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
</div>
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>Loading...</p>
</div>
</TableCell>
</TableRow>
) : agentsList && agentsList.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No agents found. Create one to get started.</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
};
export default AgentTable;

View File

@ -1,313 +0,0 @@
import { CopyOutlined } from "@ant-design/icons";
import { ArrowLeftIcon, ExternalLinkIcon } from "@heroicons/react/outline";
import { Badge, Button, Card, Grid, Text, Title } from "@tremor/react";
import { Spin, Switch, Tooltip } from "antd";
import React, { useEffect, useState } from "react";
import NotificationsManager from "../molecules/notifications_manager";
import { disableClaudeCodePlugin, enableClaudeCodePlugin, getClaudeCodePluginDetails } from "../networking";
import {
formatDateString,
formatInstallCommand,
getCategoryBadgeColor,
getSourceDisplayText,
getSourceLink,
} from "./helpers";
import { Plugin } from "./types";
interface PluginInfoViewProps {
pluginId: string;
onClose: () => void;
accessToken: string | null;
isAdmin: boolean;
onPluginUpdated: () => void;
}
const PluginInfoView: React.FC<PluginInfoViewProps> = ({
pluginId,
onClose,
accessToken,
isAdmin,
onPluginUpdated,
}) => {
const [plugin, setPlugin] = useState<Plugin | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isToggling, setIsToggling] = useState(false);
useEffect(() => {
fetchPluginInfo();
}, [pluginId, accessToken]);
const fetchPluginInfo = async () => {
if (!accessToken) return;
setIsLoading(true);
try {
// The backend expects plugin name, not ID
// We'll need to find the plugin by ID from the list
// For now, assume pluginId is actually the plugin name
const data = await getClaudeCodePluginDetails(accessToken, pluginId as string);
setPlugin(data.plugin);
} catch (error) {
console.error("Error fetching plugin info:", error);
NotificationsManager.error("Failed to load plugin information");
} finally {
setIsLoading(false);
}
};
const handleToggleEnabled = async () => {
if (!accessToken || !plugin) return;
setIsToggling(true);
try {
if (plugin.enabled) {
await disableClaudeCodePlugin(accessToken, plugin.name);
NotificationsManager.success(`Plugin "${plugin.name}" disabled`);
} else {
await enableClaudeCodePlugin(accessToken, plugin.name);
NotificationsManager.success(`Plugin "${plugin.name}" enabled`);
}
onPluginUpdated();
fetchPluginInfo();
} catch (error) {
NotificationsManager.error("Failed to toggle plugin status");
} finally {
setIsToggling(false);
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
NotificationsManager.success("Copied to clipboard!");
};
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
<Spin size="large" />
</div>
);
}
if (!plugin) {
return (
<div className="p-8 text-center text-gray-500">
<p>Plugin not found</p>
<Button className="mt-4" onClick={onClose}>
Go Back
</Button>
</div>
);
}
const installCommand = formatInstallCommand(plugin);
const sourceLink = getSourceLink(plugin.source);
const categoryBadgeColor = getCategoryBadgeColor(plugin.category);
return (
<div className="space-y-4">
{/* Header with Back Button */}
<div className="flex items-center gap-3 mb-6">
<ArrowLeftIcon className="h-5 w-5 cursor-pointer text-gray-500 hover:text-gray-700" onClick={onClose} />
<h2 className="text-2xl font-bold">{plugin.name}</h2>
{plugin.version && (
<Badge color="blue" size="xs">
v{plugin.version}
</Badge>
)}
{plugin.category && (
<Badge color={categoryBadgeColor} size="xs">
{plugin.category}
</Badge>
)}
<Badge color={plugin.enabled ? "green" : "gray"} size="xs">
{plugin.enabled ? "Enabled" : "Disabled"}
</Badge>
</div>
{/* Install Command */}
<Card>
<div className="flex items-center justify-between">
<div className="flex-1">
<Text className="text-gray-600 text-xs mb-2">Install Command</Text>
<div className="font-mono bg-gray-100 px-3 py-2 rounded text-sm">{installCommand}</div>
</div>
<Tooltip title="Copy install command">
<Button
size="xs"
variant="secondary"
icon={CopyOutlined}
onClick={() => copyToClipboard(installCommand)}
className="ml-4"
>
Copy
</Button>
</Tooltip>
</div>
</Card>
{/* Plugin Details */}
<Card>
<Title>Plugin Details</Title>
<Grid className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-4">
{/* Plugin ID */}
<div>
<Text className="text-gray-600 text-xs">Plugin ID</Text>
<div className="flex items-center gap-2 mt-1">
<Text className="font-mono text-xs">{plugin.id}</Text>
<CopyOutlined
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
onClick={() => copyToClipboard(plugin.id)}
/>
</div>
</div>
{/* Name */}
<div>
<Text className="text-gray-600 text-xs">Name</Text>
<Text className="font-semibold mt-1">{plugin.name}</Text>
</div>
{/* Version */}
<div>
<Text className="text-gray-600 text-xs">Version</Text>
<Text className="font-semibold mt-1">{plugin.version || "N/A"}</Text>
</div>
{/* Source */}
<div className="col-span-2">
<Text className="text-gray-600 text-xs">Source</Text>
<div className="flex items-center gap-2 mt-1">
<Text className="font-semibold">{getSourceDisplayText(plugin.source)}</Text>
{sourceLink && (
<a
href={sourceLink}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700"
>
<ExternalLinkIcon className="h-4 w-4" />
</a>
)}
</div>
</div>
{/* Category */}
<div>
<Text className="text-gray-600 text-xs">Category</Text>
<div className="mt-1">
{plugin.category ? (
<Badge color={categoryBadgeColor} size="xs">
{plugin.category}
</Badge>
) : (
<Text className="text-gray-400">Uncategorized</Text>
)}
</div>
</div>
{/* Enabled Status */}
{isAdmin && (
<div className="col-span-3">
<Text className="text-gray-600 text-xs">Status</Text>
<div className="flex items-center gap-3 mt-2">
<Switch checked={plugin.enabled} loading={isToggling} onChange={handleToggleEnabled} />
<Text className="text-sm">
{plugin.enabled
? "Plugin is enabled and visible in marketplace"
: "Plugin is disabled and hidden from marketplace"}
</Text>
</div>
</div>
)}
</Grid>
</Card>
{/* Description */}
{plugin.description && (
<Card>
<Title>Description</Title>
<Text className="mt-2">{plugin.description}</Text>
</Card>
)}
{/* Keywords */}
{plugin.keywords && plugin.keywords.length > 0 && (
<Card>
<Title>Keywords</Title>
<div className="flex flex-wrap gap-2 mt-2">
{plugin.keywords.map((keyword, index) => (
<Badge key={index} color="gray" size="xs">
{keyword}
</Badge>
))}
</div>
</Card>
)}
{/* Author Information */}
{plugin.author && (
<Card>
<Title>Author Information</Title>
<Grid className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
{plugin.author.name && (
<div>
<Text className="text-gray-600 text-xs">Name</Text>
<Text className="font-semibold mt-1">{plugin.author.name}</Text>
</div>
)}
{plugin.author.email && (
<div>
<Text className="text-gray-600 text-xs">Email</Text>
<Text className="font-semibold mt-1">
<a href={`mailto:${plugin.author.email}`} className="text-blue-500 hover:text-blue-700">
{plugin.author.email}
</a>
</Text>
</div>
)}
</Grid>
</Card>
)}
{/* Additional Links */}
{plugin.homepage && (
<Card>
<Title>Homepage</Title>
<a
href={plugin.homepage}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700 flex items-center gap-2 mt-2"
>
{plugin.homepage}
<ExternalLinkIcon className="h-4 w-4" />
</a>
</Card>
)}
{/* Timestamps */}
<Card>
<Title>Metadata</Title>
<Grid className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<div>
<Text className="text-gray-600 text-xs">Created At</Text>
<Text className="font-semibold mt-1">{formatDateString(plugin.created_at)}</Text>
</div>
<div>
<Text className="text-gray-600 text-xs">Updated At</Text>
<Text className="font-semibold mt-1">{formatDateString(plugin.updated_at)}</Text>
</div>
{plugin.created_by && (
<div className="col-span-2">
<Text className="text-gray-600 text-xs">Created By</Text>
<Text className="font-semibold mt-1">{plugin.created_by}</Text>
</div>
)}
</Grid>
</Card>
</div>
);
};
export default PluginInfoView;

View File

@ -1,321 +0,0 @@
import { useState } from "react";
import { ColumnDef } from "@tanstack/react-table";
import { MCPServer } from "./types";
import { Icon } from "@tremor/react";
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
import { getMaskedAndFullUrl } from "./utils";
import { Tooltip } from "antd";
import { CheckOutlined } from "@ant-design/icons";
const HealthStatusBadge: React.FC<{
server: MCPServer;
isLoadingHealth?: boolean;
isRechecking?: boolean;
onRecheck?: (serverId: string) => void;
}> = ({ server, isLoadingHealth, isRechecking, onRecheck }) => {
const [isHovered, setIsHovered] = useState(false);
const status = server.status || "unknown";
const lastCheck = server.last_health_check;
const error = server.health_check_error;
if (isLoadingHealth || isRechecking) {
return (
<span className="inline-flex items-center gap-1.5 text-xs text-gray-400 px-2 py-0.5 rounded-full bg-gray-50 border border-gray-100">
<span className="h-1.5 w-1.5 rounded-full bg-gray-300 animate-pulse"></span>
Checking
</span>
);
}
const getStatusColor = (status: string) => {
switch (status) {
case "healthy":
return "text-green-700 bg-green-50 border border-green-200";
case "unhealthy":
return "text-red-700 bg-red-50 border border-red-200";
default:
return "text-gray-600 bg-gray-50 border border-gray-200";
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "healthy":
return "✓";
case "unhealthy":
return "✗";
default:
return "?";
}
};
const isClickable = !!onRecheck;
const tooltipContent = (
<div className="max-w-xs">
<div className="font-semibold mb-1">Health Status: {status}</div>
{lastCheck && <div className="text-xs mb-1">Last Check: {new Date(lastCheck).toLocaleString()}</div>}
{error && (
<div className="text-xs">
<div className="font-medium text-red-400 mb-1">Error:</div>
<div className="break-words">{error}</div>
</div>
)}
{!lastCheck && !error && <div className="text-xs text-gray-400">No health check data available</div>}
{isClickable && <div className="text-xs text-gray-400 mt-1">Click to recheck</div>}
</div>
);
return (
<Tooltip title={tooltipContent} placement="top">
<span
className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${getStatusColor(status)} ${isClickable ? "cursor-pointer hover:opacity-80" : "cursor-default"}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={isClickable ? () => onRecheck(server.server_id) : undefined}
>
<span>{isHovered && isClickable ? "↻" : getStatusIcon(status)}</span>
{isHovered && isClickable ? "Recheck" : status.charAt(0).toUpperCase() + status.slice(1)}
</span>
</Tooltip>
);
};
export const mcpServerColumns = (
userRole: string,
onView: (serverId: string) => void,
onEdit: (serverId: string) => void,
onDelete: (serverId: string) => void,
isLoadingHealth?: boolean,
onByokConnect?: (server: MCPServer) => void,
onRecheckHealth?: (serverId: string) => void,
recheckingServerIds?: Set<string>,
): ColumnDef<MCPServer>[] => [
{
accessorKey: "server_id",
header: "Server ID",
enableSorting: true,
cell: ({ row }) => (
<button
onClick={() => onView(row.original.server_id)}
className="font-mono text-blue-600 bg-blue-50 hover:bg-blue-100 text-xs font-medium px-2 py-0.5 rounded-md border border-blue-200 text-left truncate whitespace-nowrap cursor-pointer max-w-[15ch] transition-colors"
>
{row.original.server_id.slice(0, 7)}...
</button>
),
},
{
accessorKey: "server_name",
header: "Name",
enableSorting: true,
cell: ({ row }) => {
const logoUrl = row.original.mcp_info?.logo_url;
const name = row.original.server_name;
return (
<div className="flex items-center gap-2">
{logoUrl ? (
<img
src={logoUrl}
alt={`${name ?? "MCP"} logo`}
className="h-5 w-5 rounded object-contain flex-shrink-0"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
) : null}
<span>{name}</span>
</div>
);
},
},
{
accessorKey: "alias",
header: "Alias",
enableSorting: true,
},
{
id: "url",
header: "URL",
cell: ({ row }) => {
const url = row.original.url;
if (!url) {
return <span className="text-gray-400"></span>;
}
const { maskedUrl } = getMaskedAndFullUrl(url);
return <span className="font-mono text-sm">{maskedUrl}</span>;
},
},
{
accessorKey: "transport",
header: "Transport",
enableSorting: true,
cell: ({ row }) => {
const transport = row.original.transport || "http";
const specPath = row.original.spec_path;
const displayTransport = specPath && transport !== "stdio" ? "OPENAPI" : transport;
const label = displayTransport.toUpperCase();
return (
<span className="inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200">
{label}
</span>
);
},
},
{
accessorKey: "auth_type",
header: "Auth Type",
enableSorting: true,
cell: ({ getValue }) => {
const authType = (getValue() as string) || "none";
return (
<span className="inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200">
{authType}
</span>
);
},
},
{
id: "health_status",
header: "Health Status",
cell: ({ row }) => (
<HealthStatusBadge
server={row.original}
isLoadingHealth={isLoadingHealth}
isRechecking={recheckingServerIds?.has(row.original.server_id)}
onRecheck={onRecheckHealth}
/>
),
},
{
id: "mcp_access_groups",
header: "Access Groups",
cell: ({ row }) => {
const groups = row.original.mcp_access_groups;
if (Array.isArray(groups) && groups.length > 0) {
if (typeof groups[0] === "string") {
const joined = groups.join(", ");
return (
<Tooltip title={joined}>
<div className="flex items-center gap-1 max-w-[200px]">
<span className="inline-flex items-center text-xs font-medium px-1.5 py-0.5 rounded bg-gray-100 text-gray-700 border border-gray-200 truncate max-w-[140px]">
{groups[0]}
</span>
{groups.length > 1 && <span className="text-xs text-gray-400 font-medium">+{groups.length - 1}</span>}
</div>
</Tooltip>
);
}
}
return <span className="text-xs text-gray-400"></span>;
},
},
{
id: "available_on_public_internet",
header: "Network Access",
cell: ({ row }) => {
const isPublic = row.original.available_on_public_internet;
return isPublic ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium">
<span className="h-1.5 w-1.5 rounded-full bg-green-500"></span>
Public
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-orange-50 text-orange-700 rounded-full border border-orange-200 text-xs font-medium">
<span className="h-1.5 w-1.5 rounded-full bg-orange-500"></span>
Internal
</span>
);
},
},
{
header: "Created",
accessorKey: "created_at",
enableSorting: true,
sortingFn: "datetime",
cell: ({ row }) => {
const server = row.original;
if (!server.created_at) return <span className="text-xs text-gray-400"></span>;
const date = new Date(server.created_at);
return (
<Tooltip title={date.toLocaleString()}>
<span className="text-xs text-gray-600">{date.toLocaleDateString()}</span>
</Tooltip>
);
},
},
{
header: "Updated",
accessorKey: "updated_at",
enableSorting: true,
sortingFn: "datetime",
cell: ({ row }) => {
const server = row.original;
if (!server.updated_at) return <span className="text-xs text-gray-400"></span>;
const date = new Date(server.updated_at);
return (
<Tooltip title={date.toLocaleString()}>
<span className="text-xs text-gray-600">{date.toLocaleDateString()}</span>
</Tooltip>
);
},
},
{
id: "byok_credential",
header: "Credential",
cell: ({ row }) => {
const server = row.original;
if (!server.is_byok) {
return <span className="text-gray-300 text-xs"></span>;
}
if (server.has_user_credential) {
return (
<div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-green-50 text-green-700 border border-green-200">
<CheckOutlined style={{ fontSize: 10 }} /> Connected
</span>
{onByokConnect && (
<button
className="text-xs text-gray-400 hover:text-blue-600 transition-colors"
onClick={() => onByokConnect(server)}
>
Update
</button>
)}
</div>
);
}
return onByokConnect ? (
<button
className="text-xs bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md font-medium transition-colors shadow-sm"
onClick={() => onByokConnect(server)}
>
Connect
</button>
) : null;
},
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => (
<div className="flex items-center gap-1">
<Tooltip title="Edit">
<button
onClick={() => onEdit(row.original.server_id)}
className="p-1.5 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors"
>
<Icon icon={PencilAltIcon} size="sm" />
</button>
</Tooltip>
<Tooltip title="Delete">
<button
onClick={() => onDelete(row.original.server_id)}
className="p-1.5 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors"
>
<Icon icon={TrashIcon} size="sm" />
</button>
</Tooltip>
</div>
),
},
];