feat: Enhance group management with server tool configuration (#250)

This commit is contained in:
samanhappy
2025-07-29 17:31:05 +08:00
committed by GitHub
parent 5bb2715094
commit a6cea2ad3f
14 changed files with 827 additions and 152 deletions

View File

@@ -2,8 +2,8 @@ import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useGroupData } from '@/hooks/useGroupData'
import { useServerData } from '@/hooks/useServerData'
import { GroupFormData, Server } from '@/types'
import { ToggleGroup } from './ui/ToggleGroup'
import { GroupFormData, Server, IGroupServerConfig } from '@/types'
import { ServerToolConfig } from './ServerToolConfig'
interface AddGroupFormProps {
onAdd: () => void
@@ -21,7 +21,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
const [formData, setFormData] = useState<GroupFormData>({
name: '',
description: '',
servers: []
servers: [] as IGroupServerConfig[]
})
useEffect(() => {
@@ -66,64 +66,68 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
<div className="p-6">
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
<div className="p-6 flex-shrink-0">
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.addNew')}</h2>
{error && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md border border-gray-200">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
{t('groups.name')} *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
placeholder={t('groups.namePlaceholder')}
required
/>
</div>
<ToggleGroup
className="mb-6"
label={t('groups.servers')}
noOptionsText={t('groups.noServerOptions')}
values={formData.servers}
options={availableServers.map(server => ({
value: server.name,
label: server.name
}))}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
/>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-600 hover:text-gray-800 btn-secondary"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 btn-primary"
disabled={isSubmitting}
>
{isSubmitting ? t('common.submitting') : t('common.create')}
</button>
</div>
</form>
</div>
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
<div className="flex-1 overflow-y-auto px-6">
<div className="space-y-4">
<div>
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
{t('groups.name')} *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder={t('groups.namePlaceholder')}
required
/>
</div>
<div>
<label className="block text-gray-700 text-sm font-bold mb-2">
{t('groups.configureTools')}
</label>
<ServerToolConfig
servers={availableServers}
value={formData.servers as IGroupServerConfig[]}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
className="border border-gray-200 rounded-lg p-4 bg-gray-50"
/>
</div>
</div>
</div>
<div className="flex justify-end space-x-3 p-6 pt-4 border-t border-gray-200 flex-shrink-0">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 transition-colors"
disabled={isSubmitting}
>
{isSubmitting ? t('common.submitting') : t('common.create')}
</button>
</div>
</form>
</div>
</div>
)

View File

@@ -1,9 +1,9 @@
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Group, GroupFormData, Server } from '@/types'
import { Group, GroupFormData, Server, IGroupServerConfig } from '@/types'
import { useGroupData } from '@/hooks/useGroupData'
import { useServerData } from '@/hooks/useServerData'
import { ToggleGroup } from './ui/ToggleGroup'
import { ServerToolConfig } from './ServerToolConfig'
interface EditGroupFormProps {
group: Group
@@ -71,64 +71,68 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
<div className="p-6">
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
<div className="p-6 flex-shrink-0">
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.edit')}</h2>
{error && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md border border-gray-200">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
{t('groups.name')} *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
placeholder={t('groups.namePlaceholder')}
required
/>
</div>
<ToggleGroup
className="mb-6"
label={t('groups.servers')}
noOptionsText={t('groups.noServerOptions')}
values={formData.servers}
options={availableServers.map(server => ({
value: server.name,
label: server.name
}))}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
/>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-600 hover:text-gray-800 btn-secondary"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 btn-primary"
disabled={isSubmitting}
>
{isSubmitting ? t('common.submitting') : t('common.save')}
</button>
</div>
</form>
</div>
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
<div className="flex-1 overflow-y-auto px-6">
<div className="space-y-4">
<div>
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
{t('groups.name')} *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder={t('groups.namePlaceholder')}
required
/>
</div>
<div>
<label className="block text-gray-700 text-sm font-bold mb-2">
{t('groups.configureTools')}
</label>
<ServerToolConfig
servers={availableServers}
value={formData.servers as IGroupServerConfig[]}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
className="border border-gray-200 rounded-lg p-4 bg-gray-50"
/>
</div>
</div>
</div>
<div className="flex justify-end space-x-3 p-6 pt-4 border-t border-gray-200 flex-shrink-0">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 transition-colors"
disabled={isSubmitting}
>
{isSubmitting ? t('common.submitting') : t('common.save')}
</button>
</div>
</form>
</div>
</div>
)

View File

@@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Group, Server } from '@/types'
import { Edit, Trash, Copy, Check, Link, FileCode, DropdownIcon } from '@/components/icons/LucideIcons'
import { Group, Server, IGroupServerConfig } from '@/types'
import { Edit, Trash, Copy, Check, Link, FileCode, DropdownIcon, Wrench } from '@/components/icons/LucideIcons'
import DeleteDialog from '@/components/ui/DeleteDialog'
import { useToast } from '@/contexts/ToastContext'
import { useSettingsData } from '@/hooks/useSettingsData'
@@ -25,6 +25,7 @@ const GroupCard = ({
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [copied, setCopied] = useState(false)
const [showCopyDropdown, setShowCopyDropdown] = useState(false)
const [expandedServer, setExpandedServer] = useState<string | null>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
// Close dropdown when clicking outside
@@ -108,8 +109,25 @@ const GroupCard = ({
copyToClipboard(JSON.stringify(jsonConfig, null, 2))
}
// Helper function to normalize group servers to get server names
const getServerNames = (servers: string[] | IGroupServerConfig[]): string[] => {
return servers.map(server => typeof server === 'string' ? server : server.name);
};
// Helper function to get server configuration
const getServerConfig = (serverName: string): IGroupServerConfig | undefined => {
const server = group.servers.find(s =>
typeof s === 'string' ? s === serverName : s.name === serverName
);
if (typeof server === 'string') {
return { name: server, tools: 'all' };
}
return server;
};
// Get servers that belong to this group
const groupServers = servers.filter(server => group.servers.includes(server.name))
const serverNames = getServerNames(group.servers);
const groupServers = servers.filter(server => serverNames.includes(server.name));
return (
<div className="bg-white shadow rounded-lg p-6 ">
@@ -186,18 +204,68 @@ const GroupCard = ({
{groupServers.length === 0 ? (
<p className="text-gray-500 italic">{t('groups.noServers')}</p>
) : (
<div className="flex flex-wrap gap-2 mt-2">
{groupServers.map(server => (
<div
key={server.name}
className="inline-flex items-center px-3 py-1 bg-gray-50 rounded"
>
<span className="font-medium text-gray-700 text-sm">{server.name}</span>
<span className={`ml-2 inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
}`}></span>
</div>
))}
<div className="flex flex-wrap gap-2">
{groupServers.map(server => {
const serverConfig = getServerConfig(server.name);
const hasToolRestrictions = serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools);
const toolCount = hasToolRestrictions && Array.isArray(serverConfig?.tools)
? serverConfig.tools.length
: (server.tools?.length || 0); // Show total tool count when all tools are selected
const isExpanded = expandedServer === server.name;
// Get tools list for display
const getToolsList = () => {
if (hasToolRestrictions && Array.isArray(serverConfig?.tools)) {
return serverConfig.tools;
} else if (server.tools && server.tools.length > 0) {
return server.tools.map(tool => tool.name);
}
return [];
};
const handleServerClick = () => {
setExpandedServer(isExpanded ? null : server.name);
};
return (
<div key={server.name} className="relative">
<div
className="flex items-center space-x-2 bg-gray-50 rounded-lg px-3 py-2 cursor-pointer hover:bg-gray-100 transition-colors"
onClick={handleServerClick}
>
<span className="font-medium text-gray-700 text-sm">{server.name}</span>
<span className={`inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
}`}></span>
{toolCount > 0 && (
<span className="text-xs text-blue-600 bg-blue-100 px-2 py-0.5 rounded flex items-center gap-1">
<Wrench size={12} />
{toolCount}
</span>
)}
</div>
{isExpanded && (
<div className="absolute top-full left-0 mt-1 bg-white shadow-lg rounded-md border border-gray-200 p-3 z-10 min-w-[300px] max-w-[400px]">
<div className="text-gray-600 text-xs mb-2">
{hasToolRestrictions ? t('groups.selectedTools') : t('groups.allTools')}:
</div>
<div className="flex flex-wrap gap-1">
{getToolsList().map((toolName, index) => (
<span
key={index}
className="inline-block bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs"
>
{toolName}
</span>
))}
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>

View File

@@ -0,0 +1,317 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IGroupServerConfig, Server, Tool } from '@/types';
import { cn } from '@/utils/cn';
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 [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}-`, '')) || [];
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}-`, '');
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>
);
};

View File

@@ -16,7 +16,8 @@ import {
AlertCircle,
Link,
FileCode,
ChevronDown as DropdownIcon
ChevronDown as DropdownIcon,
Wrench
} from 'lucide-react'
export {
@@ -37,7 +38,8 @@ export {
AlertCircle,
Link,
FileCode,
DropdownIcon
DropdownIcon,
Wrench
}
const LucideIcons = {

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Group, ApiResponse } from '@/types';
import { Group, ApiResponse, IGroupServerConfig } from '@/types';
import { getApiUrl } from '../utils/runtime';
export const useGroupData = () => {
@@ -49,7 +49,11 @@ export const useGroupData = () => {
}, []);
// Create a new group with server associations
const createGroup = async (name: string, description?: string, servers: string[] = []) => {
const createGroup = async (
name: string,
description?: string,
servers: string[] | IGroupServerConfig[] = [],
) => {
try {
const token = localStorage.getItem('mcphub_token');
const response = await fetch(getApiUrl('/groups'), {
@@ -79,7 +83,7 @@ export const useGroupData = () => {
// Update an existing group with server associations
const updateGroup = async (
id: string,
data: { name?: string; description?: string; servers?: string[] },
data: { name?: string; description?: string; servers?: string[] | IGroupServerConfig[] },
) => {
try {
const token = localStorage.getItem('mcphub_token');
@@ -108,7 +112,7 @@ export const useGroupData = () => {
};
// Update servers in a group (for batch updates)
const updateGroupServers = async (groupId: string, servers: string[]) => {
const updateGroupServers = async (groupId: string, servers: string[] | IGroupServerConfig[]) => {
try {
const token = localStorage.getItem('mcphub_token');
const response = await fetch(getApiUrl(`/groups/${groupId}/servers/batch`), {

View File

@@ -271,7 +271,14 @@
"noGroups": "No groups available. Create a new group to get started.",
"noServers": "No servers in this group.",
"noServerOptions": "No servers available",
"serverCount": "{{count}} Servers"
"serverCount": "{{count}} Servers",
"toolSelection": "Tool Selection",
"toolsSelected": "Selected",
"allTools": "All",
"selectedTools": "Selected tools",
"selectAll": "Select All",
"selectNone": "Select None",
"configureTools": "Configure Tools"
},
"market": {
"title": "Server Market",

View File

@@ -272,7 +272,14 @@
"noGroups": "暂无可用分组。创建一个新分组以开始使用。",
"noServers": "此分组中没有服务器。",
"noServerOptions": "没有可用的服务器",
"serverCount": "{{count}} 台服务器"
"serverCount": "{{count}} 台服务器",
"toolSelection": "工具选择",
"toolsSelected": "选择",
"allTools": "全部",
"selectedTools": "选中的工具",
"selectAll": "全选",
"selectNone": "全不选",
"configureTools": "配置工具"
},
"market": {
"title": "服务器市场",

View File

@@ -137,11 +137,17 @@ export interface Server {
}
// Group types
// Group server configuration - supports tool selection
export interface IGroupServerConfig {
name: string; // Server name
tools?: string[] | 'all'; // Array of specific tool names to include, or 'all' for all tools (default: 'all')
}
export interface Group {
id: string;
name: string;
description?: string;
servers: string[];
servers: string[] | IGroupServerConfig[]; // Supports both old and new format
}
// Environment variable types
@@ -196,7 +202,7 @@ export interface ServerFormData {
export interface GroupFormData {
name: string;
description: string;
servers: string[]; // Added servers array to include in form data
servers: string[] | IGroupServerConfig[]; // Updated to support new format
}
// API response types