mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-30 21:49:13 -05:00
320 lines
13 KiB
TypeScript
320 lines
13 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { IGroupServerConfig, Server, Tool } from '@/types';
|
|
import { cn } from '@/utils/cn';
|
|
import { useSettingsData } from '@/hooks/useSettingsData';
|
|
|
|
interface ServerToolConfigProps {
|
|
servers: Server[];
|
|
value: string[] | IGroupServerConfig[];
|
|
onChange: (value: IGroupServerConfig[]) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export const ServerToolConfig: React.FC<ServerToolConfigProps> = ({
|
|
servers,
|
|
value,
|
|
onChange,
|
|
className
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const { nameSeparator } = useSettingsData();
|
|
const [expandedServers, setExpandedServers] = useState<Set<string>>(new Set());
|
|
|
|
// Normalize current value to IGroupServerConfig[] format
|
|
const normalizedValue: IGroupServerConfig[] = React.useMemo(() => {
|
|
return value.map(item => {
|
|
if (typeof item === 'string') {
|
|
return { name: item, tools: 'all' as const };
|
|
}
|
|
return { ...item, tools: item.tools || 'all' as const };
|
|
});
|
|
}, [value]);
|
|
|
|
// Get available servers (enabled only)
|
|
const availableServers = React.useMemo(() =>
|
|
servers.filter(server => server.enabled !== false),
|
|
[servers]
|
|
);
|
|
|
|
// Clean up expanded servers when servers are removed from configuration
|
|
// But keep servers that were explicitly expanded even if they have no configuration
|
|
React.useEffect(() => {
|
|
const configuredServerNames = new Set(normalizedValue.map(config => config.name));
|
|
const availableServerNames = new Set(availableServers.map(server => server.name));
|
|
|
|
setExpandedServers(prev => {
|
|
const newSet = new Set<string>();
|
|
prev.forEach(serverName => {
|
|
// Keep expanded if server is configured OR if server exists and user manually expanded it
|
|
if (configuredServerNames.has(serverName) || availableServerNames.has(serverName)) {
|
|
newSet.add(serverName);
|
|
}
|
|
});
|
|
return newSet;
|
|
});
|
|
}, [normalizedValue, availableServers]);
|
|
|
|
const toggleServer = (serverName: string) => {
|
|
const existingIndex = normalizedValue.findIndex(config => config.name === serverName);
|
|
|
|
if (existingIndex >= 0) {
|
|
// Remove server - this will also remove all its tools
|
|
const newValue = normalizedValue.filter(config => config.name !== serverName);
|
|
onChange(newValue);
|
|
// Don't auto-collapse the server when it's unchecked - let user control expansion manually
|
|
} else {
|
|
// Add server with all tools by default
|
|
const newValue = [...normalizedValue, { name: serverName, tools: 'all' as const }];
|
|
onChange(newValue);
|
|
// Don't auto-expand the server when it's checked - let user control expansion manually
|
|
}
|
|
};
|
|
|
|
const toggleServerExpanded = (serverName: string) => {
|
|
setExpandedServers(prev => {
|
|
const newSet = new Set(prev);
|
|
if (newSet.has(serverName)) {
|
|
newSet.delete(serverName);
|
|
} else {
|
|
newSet.add(serverName);
|
|
}
|
|
return newSet;
|
|
});
|
|
};
|
|
|
|
const updateServerTools = (serverName: string, tools: string[] | 'all', keepExpanded = false) => {
|
|
if (Array.isArray(tools) && tools.length === 0) {
|
|
// If no tools are selected, remove the server entirely
|
|
const newValue = normalizedValue.filter(config => config.name !== serverName);
|
|
onChange(newValue);
|
|
// Only collapse the server if not explicitly asked to keep it expanded
|
|
if (!keepExpanded) {
|
|
setExpandedServers(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(serverName);
|
|
return newSet;
|
|
});
|
|
}
|
|
} else {
|
|
// Update server tools or add server if it doesn't exist
|
|
const existingServerIndex = normalizedValue.findIndex(config => config.name === serverName);
|
|
|
|
if (existingServerIndex >= 0) {
|
|
// Update existing server
|
|
const newValue = normalizedValue.map(config =>
|
|
config.name === serverName ? { ...config, tools } : config
|
|
);
|
|
onChange(newValue);
|
|
} else {
|
|
// Add new server with specified tools
|
|
const newValue = [...normalizedValue, { name: serverName, tools }];
|
|
onChange(newValue);
|
|
}
|
|
}
|
|
};
|
|
|
|
const toggleTool = (serverName: string, toolName: string) => {
|
|
const server = availableServers.find(s => s.name === serverName);
|
|
if (!server) return;
|
|
|
|
const allToolNames = server.tools?.map(tool => tool.name.replace(`${serverName}${nameSeparator}`, '')) || [];
|
|
const serverConfig = normalizedValue.find(config => config.name === serverName);
|
|
|
|
if (!serverConfig) {
|
|
// Server not selected yet, add it with only this tool
|
|
const newValue = [...normalizedValue, { name: serverName, tools: [toolName] }];
|
|
onChange(newValue);
|
|
// Don't auto-expand - let user control expansion manually
|
|
return;
|
|
}
|
|
|
|
if (serverConfig.tools === 'all') {
|
|
// Switch from 'all' to specific tools, excluding the toggled tool
|
|
const newTools = allToolNames.filter(name => name !== toolName);
|
|
updateServerTools(serverName, newTools);
|
|
// If all tools are deselected, the server will be removed and collapsed in updateServerTools
|
|
} else if (Array.isArray(serverConfig.tools)) {
|
|
const currentTools = serverConfig.tools;
|
|
if (currentTools.includes(toolName)) {
|
|
// Remove tool
|
|
const newTools = currentTools.filter(name => name !== toolName);
|
|
updateServerTools(serverName, newTools);
|
|
// If all tools are deselected, the server will be removed and collapsed in updateServerTools
|
|
} else {
|
|
// Add tool
|
|
const newTools = [...currentTools, toolName];
|
|
|
|
// If all tools are selected, switch to 'all'
|
|
if (newTools.length === allToolNames.length) {
|
|
updateServerTools(serverName, 'all');
|
|
} else {
|
|
updateServerTools(serverName, newTools);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const isServerSelected = (serverName: string) => {
|
|
const serverConfig = normalizedValue.find(config => config.name === serverName);
|
|
if (!serverConfig) return false;
|
|
|
|
// Server is considered "fully selected" if tools is 'all'
|
|
return serverConfig.tools === 'all';
|
|
};
|
|
|
|
const isServerPartiallySelected = (serverName: string) => {
|
|
const serverConfig = normalizedValue.find(config => config.name === serverName);
|
|
if (!serverConfig) return false;
|
|
|
|
// Server is partially selected if it has specific tools selected (not 'all' and not empty)
|
|
return Array.isArray(serverConfig.tools) && serverConfig.tools.length > 0;
|
|
};
|
|
|
|
const isToolSelected = (serverName: string, toolName: string) => {
|
|
const serverConfig = normalizedValue.find(config => config.name === serverName);
|
|
if (!serverConfig) return false;
|
|
|
|
if (serverConfig.tools === 'all') return true;
|
|
if (Array.isArray(serverConfig.tools)) {
|
|
return serverConfig.tools.includes(toolName);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const getServerTools = (serverName: string): Tool[] => {
|
|
const server = availableServers.find(s => s.name === serverName);
|
|
return server?.tools || [];
|
|
};
|
|
|
|
return (
|
|
<div className={cn("space-y-4", className)}>
|
|
<div className="space-y-3">
|
|
{availableServers.map(server => {
|
|
const isSelected = isServerSelected(server.name);
|
|
const isPartiallySelected = isServerPartiallySelected(server.name);
|
|
const isExpanded = expandedServers.has(server.name);
|
|
const serverTools = getServerTools(server.name);
|
|
const serverConfig = normalizedValue.find(config => config.name === server.name);
|
|
|
|
return (
|
|
<div key={server.name} className="border border-gray-200 rounded-lg hover:border-gray-300 hover:bg-gray-50 transition-colors">
|
|
<div
|
|
className="flex items-center justify-between p-3 cursor-pointer rounded-lg transition-colors"
|
|
onClick={() => toggleServerExpanded(server.name)}
|
|
>
|
|
<div
|
|
className="flex items-center space-x-3"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
toggleServer(server.name);
|
|
}}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected || isPartiallySelected}
|
|
onChange={() => toggleServer(server.name)}
|
|
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
|
/>
|
|
<span className="font-medium text-gray-900 cursor-pointer select-none">
|
|
{server.name}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
{serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools) && (
|
|
<span className="text-sm text-green-600">
|
|
({t('groups.toolsSelected')} {serverConfig.tools.length}/{serverTools.length})
|
|
</span>
|
|
)}
|
|
{serverConfig && serverConfig.tools === 'all' && (
|
|
<span className="text-sm text-green-600">
|
|
({t('groups.allTools')} {serverTools.length}/{serverTools.length})
|
|
</span>
|
|
)}
|
|
|
|
{serverTools.length > 0 && (
|
|
<button
|
|
type="button"
|
|
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
<svg
|
|
className={cn("w-5 h-5 transition-transform", isExpanded && "rotate-180")}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{isExpanded && serverTools.length > 0 && (
|
|
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="text-sm font-medium text-gray-700">
|
|
{t('groups.toolSelection')}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const isAllSelected = serverConfig?.tools === 'all';
|
|
if (isAllSelected || (Array.isArray(serverConfig?.tools) && serverConfig.tools.length === serverTools.length)) {
|
|
// If all tools are selected, deselect all (remove server) but keep expanded
|
|
updateServerTools(server.name, [], true);
|
|
} else {
|
|
// Select all tools (add server if not present)
|
|
updateServerTools(server.name, 'all');
|
|
// Don't auto-expand - let user control expansion manually
|
|
}
|
|
}}
|
|
className="text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
|
>
|
|
{(serverConfig?.tools === 'all' ||
|
|
(Array.isArray(serverConfig?.tools) && serverConfig.tools.length === serverTools.length))
|
|
? t('groups.selectNone')
|
|
: t('groups.selectAll')}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-2 max-h-32 overflow-y-auto">
|
|
{serverTools.map(tool => {
|
|
const toolName = tool.name.replace(`${server.name}${nameSeparator}`, '');
|
|
const isToolChecked = isToolSelected(server.name, toolName);
|
|
|
|
return (
|
|
<label key={tool.name} className="flex items-center space-x-2 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
checked={isToolChecked}
|
|
onChange={() => toggleTool(server.name, toolName)}
|
|
className="w-3 h-3 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
|
/>
|
|
<span className="text-gray-700">
|
|
{toolName}
|
|
</span>
|
|
{tool.description && (
|
|
<span className="text-gray-400 text-xs truncate">
|
|
{tool.description}
|
|
</span>
|
|
)}
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{availableServers.length === 0 && (
|
|
<p className="text-gray-500 text-sm">{t('groups.noServerOptions')}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|