diff --git a/frontend/src/components/GroupImportForm.tsx b/frontend/src/components/GroupImportForm.tsx new file mode 100644 index 0000000..169ccc7 --- /dev/null +++ b/frontend/src/components/GroupImportForm.tsx @@ -0,0 +1,284 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { apiPost } from '@/utils/fetchInterceptor'; + +interface GroupImportFormProps { + onSuccess: () => void; + onCancel: () => void; +} + +interface ImportGroupConfig { + name: string; + description?: string; + servers?: string[] | Array<{ name: string; tools?: string[] | 'all' }>; +} + +interface ImportJsonFormat { + groups: ImportGroupConfig[]; +} + +const GroupImportForm: React.FC = ({ onSuccess, onCancel }) => { + const { t } = useTranslation(); + const [jsonInput, setJsonInput] = useState(''); + const [error, setError] = useState(null); + const [isImporting, setIsImporting] = useState(false); + const [previewGroups, setPreviewGroups] = useState(null); + + const examplePlaceholder = `{ + "groups": [ + { + "name": "AI Assistants", + "servers": ["openai-server", "anthropic-server"] + }, + { + "name": "Development Tools", + "servers": [ + { + "name": "github-server", + "tools": ["create_issue", "list_repos"] + }, + { + "name": "gitlab-server", + "tools": "all" + } + ] + } + ] +} + +Supports: +- Simple server list: ["server1", "server2"] +- Advanced server config: [{"name": "server1", "tools": ["tool1", "tool2"]}] +- All groups will be imported in a single efficient batch operation.`; + + const parseAndValidateJson = (input: string): ImportJsonFormat | null => { + try { + const parsed = JSON.parse(input.trim()); + + // Validate structure + if (!parsed.groups || !Array.isArray(parsed.groups)) { + setError(t('groupImport.invalidFormat')); + return null; + } + + // Validate each group + for (const group of parsed.groups) { + if (!group.name || typeof group.name !== 'string') { + setError(t('groupImport.missingName')); + return null; + } + } + + return parsed as ImportJsonFormat; + } catch (e) { + setError(t('groupImport.parseError')); + return null; + } + }; + + const handlePreview = () => { + setError(null); + const parsed = parseAndValidateJson(jsonInput); + if (!parsed) return; + + setPreviewGroups(parsed.groups); + }; + + const handleImport = async () => { + if (!previewGroups) return; + + setIsImporting(true); + setError(null); + + try { + // Use batch import API for better performance + const result = await apiPost('/groups/batch', { + groups: previewGroups, + }); + + if (result.success) { + const { successCount, failureCount, results } = result; + + if (failureCount > 0) { + const errors = results + .filter((r: any) => !r.success) + .map((r: any) => `${r.name}: ${r.message || t('groupImport.addFailed')}`); + + setError( + t('groupImport.partialSuccess', { count: successCount, total: previewGroups.length }) + + '\n' + + errors.join('\n'), + ); + } + + if (successCount > 0) { + onSuccess(); + } + } else { + setError(result.message || t('groupImport.importFailed')); + } + } catch (err) { + console.error('Import error:', err); + setError(t('groupImport.importFailed')); + } finally { + setIsImporting(false); + } + }; + + const renderServerList = ( + servers?: string[] | Array<{ name: string; tools?: string[] | 'all' }>, + ) => { + if (!servers || servers.length === 0) { + return {t('groups.noServers')}; + } + + return ( +
+ {servers.map((server, idx) => { + if (typeof server === 'string') { + return ( +
+ • {server} +
+ ); + } else { + return ( +
+ • {server.name} + {server.tools && server.tools !== 'all' && ( + + ({Array.isArray(server.tools) ? server.tools.join(', ') : server.tools}) + + )} + {server.tools === 'all' && (all tools)} +
+ ); + } + })} +
+ ); + }; + + return ( +
+
+
+

{t('groupImport.title')}

+ +
+ + {error && ( +
+

{error}

+
+ )} + + {!previewGroups ? ( +
+
+ +