// LogDatasourceDialog — 添加/编辑 Loki / ES 数据源 // Logan.Li import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Loader2, Plug, RefreshCw } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; import { logCenterApi, type LogDataSource, type TestConnectionResult } from '@/api'; import type { LogEngine } from './log-center-types'; export function LogDatasourceDialog({ open, onOpenChange, engine, editing, onSaved, }: { open: boolean; onOpenChange: (v: boolean) => void; engine: LogEngine; editing?: LogDataSource | null; onSaved: () => void; }) { const { toast } = useToast(); const [name, setName] = useState(''); const [url, setUrl] = useState(''); const [defaultIndex, setDefaultIndex] = useState('logstash-*'); const [authType, setAuthType] = useState('none'); const [user, setUser] = useState(''); const [pass, setPass] = useState(''); const [token, setToken] = useState(''); const [headers, setHeaders] = useState<{ name: string; value: string }[]>([]); const [skipTLS, setSkipTLS] = useState(false); const [isDefault, setIsDefault] = useState(false); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); // 索引下拉: ES 索引列表 + 加载状态 // Logan.Li const [indices, setIndices] = useState([]); const [loadingIndices, setLoadingIndices] = useState(false); useEffect(() => { if (!open) return; if (editing) { setName(editing.name); setUrl(editing.url); setDefaultIndex(editing.default_index || 'logstash-*'); // 兼容旧版:未设置 auth_type 但 basic_auth=true 时回退为 basic // Logan.Li const initialAuth = editing.auth_type || (editing.basic_auth ? 'basic' : 'none'); setAuthType(initialAuth); setUser(editing.basic_auth_user || ''); setPass(''); setToken(editing.token || ''); try { setHeaders(editing.headers ? JSON.parse(editing.headers) : []); } catch { setHeaders([]); } setSkipTLS(!!editing.skip_tls_verify); setIsDefault(!!editing.is_default); // 编辑 ES 源时预加载索引列表 if (editing.type === 'elasticsearch' && editing.url) { fetchIndices(editing); } } else { const suffix = Math.floor(Math.random() * 1000).toString().padStart(3, '0'); setName(engine === 'loki' ? `Loki-${suffix}` : `Elasticsearch-${suffix}`); setUrl(engine === 'loki' ? 'http://localhost:3100' : 'http://localhost:9200'); setDefaultIndex('logstash-*'); setAuthType('none'); setUser(''); setPass(''); setToken(''); setHeaders([]); setSkipTLS(false); setIsDefault(false); setIndices([]); } }, [open, editing, engine]); const fetchIndices = async (ds: LogDataSource) => { if (!ds.url || ds.type !== 'elasticsearch') return; setLoadingIndices(true); try { const result = await logCenterApi.listIndices({ url: ds.url, auth_type: ds.auth_type || 'none', basic_auth: ds.auth_type === 'basic' ? 'true' : 'false', basic_auth_user: ds.basic_auth_user || '', basic_auth_pass: ds.basic_auth_pass || '', token: ds.token || '', headers: ds.headers || '', skip_tls_verify: ds.skip_tls_verify ? 'true' : 'false', }); setIndices(Array.isArray(result) ? result : []); } catch { // 静默失败,仍可手动输入 setIndices([]); } finally { setLoadingIndices(false); } }; const payload = () => ({ name: name.trim(), type: engine, url: url.trim(), auth_type: authType, basic_auth: authType === 'basic', basic_auth_user: authType === 'basic' ? user : '', basic_auth_pass: authType === 'basic' ? pass : '', token: authType === 'bearer' ? token : '', headers: authType === 'custom' ? JSON.stringify(headers.filter(h => h.name)) : '[]', skip_tls_verify: skipTLS, default_index: engine === 'elasticsearch' ? defaultIndex : undefined, is_default: isDefault, }); const handleTest = async () => { setTesting(true); try { // 编辑已保存数据源时通过 datasource_id 测试,避免密码在前端丢失。// Logan.Li const data = editing ? { datasource_id: editing.id } : payload(); const result: TestConnectionResult = await logCenterApi.testDatasource(data); if (result.status === 'ok') { toast({ title: '连接成功' }); } else { toast({ title: '连接失败', description: result.error || '未知错误', variant: 'destructive' }); } } catch (e: any) { toast({ title: '连接失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setTesting(false); } }; const handleSave = async () => { if (!name.trim() || !url.trim()) return; setSaving(true); try { if (editing) { await logCenterApi.updateDatasource(editing.id, payload()); } else { await logCenterApi.createDatasource(payload()); } toast({ title: '已保存' }); onOpenChange(false); onSaved(); } catch (e: any) { toast({ title: '保存失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setSaving(false); } }; return ( {editing ? '编辑' : '添加'}{engine === 'loki' ? ' Loki' : ' Elasticsearch'} 数据源 仅保存连接信息,日志本体存储在您的 Loki / ES 集群
setName(e.target.value)} className="mt-1 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
setUrl(e.target.value)} placeholder="http://host:port" className="mt-1 h-9 font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
{engine === 'elasticsearch' && (
setDefaultIndex(e.target.value)} placeholder="索引名或通配符,如 .monitoring-es-7-*" className="mt-1 h-9 font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" /> {/* 已获取索引快捷选择(支持通配) // Logan.Li */} {indices.length > 0 && (
{indices.slice(0, 30).map((idx) => ( ))}
)} {/* 常见通配模式 // Logan.Li */}
{['.monitoring-es-7-*', 'logstash-*', 'filebeat-*', 'metrics-*', '*-2026.06.*'].map((pattern) => ( ))}
)}
{authType === 'basic' && (
setUser(e.target.value)} placeholder="用户名" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" /> setPass(e.target.value)} placeholder="密码" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
)} {authType === 'bearer' && (
setToken(e.target.value)} placeholder="Bearer Token" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
)} {authType === 'custom' && (
{headers.map((h, i) => (
{ const next = [...headers]; next[i].name = e.target.value; setHeaders(next); }} placeholder="Header 名,如 X-Scope-OrgID" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" /> { const next = [...headers]; next[i].value = e.target.value; setHeaders(next); }} placeholder="Header 值" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
))}
)}
); }