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",
|
"@anthropic-ai/sdk": "0.92.0",
|
||||||
"@headlessui/tailwindcss": "0.2.2",
|
"@headlessui/tailwindcss": "0.2.2",
|
||||||
"@heroicons/react": "1.0.6",
|
"@heroicons/react": "1.0.6",
|
||||||
"@remixicon/react": "4.9.0",
|
|
||||||
"@tanstack/react-pacer": "0.2.0",
|
"@tanstack/react-pacer": "0.2.0",
|
||||||
"@tanstack/react-query": "5.100.7",
|
"@tanstack/react-query": "5.100.7",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
@ -44,18 +43,15 @@
|
|||||||
"@testing-library/jest-dom": "6.9.1",
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
"@testing-library/react": "16.3.2",
|
"@testing-library/react": "16.3.2",
|
||||||
"@testing-library/user-event": "14.6.1",
|
"@testing-library/user-event": "14.6.1",
|
||||||
"@types/babel__traverse": "7.28.0",
|
|
||||||
"@types/lodash": "4.17.23",
|
"@types/lodash": "4.17.23",
|
||||||
"@types/node": "20.19.37",
|
"@types/node": "20.19.37",
|
||||||
"@types/react": "18.2.48",
|
"@types/react": "18.2.48",
|
||||||
"@types/react-copy-to-clipboard": "5.0.7",
|
"@types/react-copy-to-clipboard": "5.0.7",
|
||||||
"@types/react-dom": "18.3.7",
|
"@types/react-dom": "18.3.7",
|
||||||
"@types/react-syntax-highlighter": "15.5.13",
|
"@types/react-syntax-highlighter": "15.5.13",
|
||||||
"@types/uuid": "10.0.0",
|
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"@vitest/ui": "3.2.4",
|
"@vitest/ui": "3.2.4",
|
||||||
"autoprefixer": "10.4.24",
|
"autoprefixer": "10.4.24",
|
||||||
"dotenv": "17.2.3",
|
|
||||||
"eslint": "9.39.2",
|
"eslint": "9.39.2",
|
||||||
"eslint-config-next": "16.2.6",
|
"eslint-config-next": "16.2.6",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
@ -68,7 +64,6 @@
|
|||||||
"tailwindcss": "3.4.19",
|
"tailwindcss": "3.4.19",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.60.1",
|
"typescript-eslint": "8.60.1",
|
||||||
"vite": "7.3.2",
|
|
||||||
"vitest": "3.2.4"
|
"vitest": "3.2.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -2847,15 +2842,6 @@
|
|||||||
"npm": ">=9.5.0"
|
"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": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.60.3",
|
"version": "4.60.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
|
||||||
@ -3490,16 +3476,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
@ -3737,13 +3713,6 @@
|
|||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.60.1",
|
"version": "8.60.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
|
||||||
@ -5912,19 +5881,6 @@
|
|||||||
"csstype": "^3.0.2"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|||||||
@ -26,7 +26,6 @@
|
|||||||
"@anthropic-ai/sdk": "0.92.0",
|
"@anthropic-ai/sdk": "0.92.0",
|
||||||
"@headlessui/tailwindcss": "0.2.2",
|
"@headlessui/tailwindcss": "0.2.2",
|
||||||
"@heroicons/react": "1.0.6",
|
"@heroicons/react": "1.0.6",
|
||||||
"@remixicon/react": "4.9.0",
|
|
||||||
"@tanstack/react-pacer": "0.2.0",
|
"@tanstack/react-pacer": "0.2.0",
|
||||||
"@tanstack/react-query": "5.100.7",
|
"@tanstack/react-query": "5.100.7",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
@ -59,18 +58,15 @@
|
|||||||
"@testing-library/jest-dom": "6.9.1",
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
"@testing-library/react": "16.3.2",
|
"@testing-library/react": "16.3.2",
|
||||||
"@testing-library/user-event": "14.6.1",
|
"@testing-library/user-event": "14.6.1",
|
||||||
"@types/babel__traverse": "7.28.0",
|
|
||||||
"@types/lodash": "4.17.23",
|
"@types/lodash": "4.17.23",
|
||||||
"@types/node": "20.19.37",
|
"@types/node": "20.19.37",
|
||||||
"@types/react": "18.2.48",
|
"@types/react": "18.2.48",
|
||||||
"@types/react-copy-to-clipboard": "5.0.7",
|
"@types/react-copy-to-clipboard": "5.0.7",
|
||||||
"@types/react-dom": "18.3.7",
|
"@types/react-dom": "18.3.7",
|
||||||
"@types/react-syntax-highlighter": "15.5.13",
|
"@types/react-syntax-highlighter": "15.5.13",
|
||||||
"@types/uuid": "10.0.0",
|
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"@vitest/ui": "3.2.4",
|
"@vitest/ui": "3.2.4",
|
||||||
"autoprefixer": "10.4.24",
|
"autoprefixer": "10.4.24",
|
||||||
"dotenv": "17.2.3",
|
|
||||||
"eslint": "9.39.2",
|
"eslint": "9.39.2",
|
||||||
"eslint-config-next": "16.2.6",
|
"eslint-config-next": "16.2.6",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
@ -83,7 +79,6 @@
|
|||||||
"tailwindcss": "3.4.19",
|
"tailwindcss": "3.4.19",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.60.1",
|
"typescript-eslint": "8.60.1",
|
||||||
"vite": "7.3.2",
|
|
||||||
"vitest": "3.2.4"
|
"vitest": "3.2.4"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"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