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:
parent
248176112e
commit
9e0d92c129
44
ui/litellm-dashboard/package-lock.json
generated
44
ui/litellm-dashboard/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
];
|
||||
Loading…
Reference in New Issue
Block a user