import { useState, useCallback, useEffect } from 'react'; import { X, Loader2, Plus, Trash2 } from 'lucide-react'; import { instanceApi } from '@/api/instance'; import { workflowApi } from '@/api/workflow'; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ interface InstanceData { id: number; name: string; db_type: string; host: string; port: number; username: string; database: string; env: string; access_mode: string; workflow_id?: number | null; workflow_name?: string | null; excluded_databases?: string[] | null; mask_fields?: { table: string; field: string; rule: string }[] | null; group_id?: number | null; } interface WorkflowItem { id: number; name: string; is_active?: boolean; } interface Props { instance: InstanceData; open: boolean; onClose: () => void; onSaved: () => void; } /* ------------------------------------------------------------------ */ /* Reusable Edit Instance Dialog */ /* ------------------------------------------------------------------ */ export default function EditInstanceDialog({ instance, open, onClose, onSaved }: Props) { const [name, setName] = useState(instance.name); const [host, setHost] = useState(instance.host); const [port, setPort] = useState(String(instance.port)); const [username, setUsername] = useState(instance.username); const [password, setPassword] = useState(''); const [database, setDatabase] = useState(instance.database || ''); const [env, setEnv] = useState(instance.env || 'development'); const [accessMode, setAccessMode] = useState(instance.access_mode || 'readwrite'); const [workflowId, setWorkflowId] = useState(instance.workflow_id || null); const [workflows, setWorkflows] = useState([]); // excluded_databases: comma-separated input const [excludedDbs, setExcludedDbs] = useState((instance.excluded_databases || []).join(', ')); // mask_fields const [maskFields, setMaskFields] = useState<{ table: string; field: string; rule: string }[]>( instance.mask_fields && instance.mask_fields.length > 0 ? instance.mask_fields : [] ); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); /* Sync when instance changes */ useEffect(() => { setName(instance.name); setHost(instance.host); setPort(String(instance.port)); setUsername(instance.username); setPassword(''); setDatabase(instance.database || ''); setEnv(instance.env || 'development'); setAccessMode(instance.access_mode || 'readwrite'); setWorkflowId(instance.workflow_id || null); setExcludedDbs((instance.excluded_databases || []).join(', ')); setMaskFields(instance.mask_fields && instance.mask_fields.length > 0 ? instance.mask_fields : []); setError(''); }, [instance]); /* Load workflows when modal opens */ useEffect(() => { if (open) { workflowApi.list().then((data: any) => { const items = Array.isArray(data) ? data : (data?.items || []); setWorkflows(items.filter((w: WorkflowItem) => w.is_active !== false)); }).catch(() => setWorkflows([])); } }, [open]); const addMaskField = () => setMaskFields([...maskFields, { table: '', field: '', rule: 'mask' }]); const removeMaskField = (idx: number) => setMaskFields(maskFields.filter((_, i) => i !== idx)); const updateMaskField = (idx: number, key: string, value: string) => { const next = [...maskFields]; next[idx] = { ...next[idx], [key]: value }; setMaskFields(next); }; const handleSubmit = useCallback(async () => { if (!name.trim()) { setError('请填写名称'); return; } if (!host.trim()) { setError('请填写地址'); return; } setSubmitting(true); setError(''); try { const parsedExcluded = excludedDbs.trim() ? excludedDbs.split(',').map(s => s.trim()).filter(Boolean) : undefined; const parsedMask = maskFields.some(m => m.table && m.field) ? maskFields.filter(m => m.table && m.field) : undefined; await instanceApi.update(instance.id, { name: name.trim(), host: host.trim(), port: Number(port), username: username.trim() || undefined, password: password || undefined, database: database.trim() || undefined, env, access_mode: accessMode, workflow_id: workflowId, excluded_databases: parsedExcluded, mask_fields: parsedMask, }); onSaved(); } catch (e: unknown) { const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '保存失败'; setError(msg); } finally { setSubmitting(false); } }, [name, host, port, username, password, database, env, accessMode, workflowId, excludedDbs, maskFields, instance.id, onSaved]); if (!open) return null; const inputCls = 'w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none transition-colors'; const labelCls = 'block text-xs font-medium text-[var(--dg-text-secondary)] mb-1'; return (
e.stopPropagation()} > {/* Header */}

编辑数据源

{/* Body */}
{error &&
{error}
}
setName(e.target.value)} placeholder="实例名称" />
setHost(e.target.value)} placeholder="127.0.0.1" />
setPort(e.target.value)} placeholder="3306" />
setUsername(e.target.value)} />
setPassword(e.target.value)} placeholder="••••••" />
setDatabase(e.target.value)} placeholder="可选" />
{/* 审批流程 */}
{workflows.length === 0 &&

暂无可用审批流程

}
{/* 排除数据库 */}
setExcludedDbs(e.target.value)} placeholder="sys, information_schema, mysql" />
{/* 脱敏字段 */}
{maskFields.length === 0 && (
未配置脱敏规则
)}
{maskFields.map((mf, idx) => (
updateMaskField(idx, 'table', e.target.value)} placeholder="表名" /> updateMaskField(idx, 'field', e.target.value)} placeholder="字段名" />
))}
{/* Footer */}
); }