diff --git a/frontend/src/pages/logs/components/SourcesManager.tsx b/frontend/src/pages/logs/components/SourcesManager.tsx new file mode 100644 index 0000000..b8b9bbc --- /dev/null +++ b/frontend/src/pages/logs/components/SourcesManager.tsx @@ -0,0 +1,288 @@ +import { memo, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' +import { Database, Globe, Loader2, Lock, Pencil, Plus, Server, Trash2, Zap } from 'lucide-react' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Checkbox } from '@/components/ui/checkbox' +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { EmptyState } from '@/components/EmptyState' +import { clusterApi } from '@/api' +import { logsApi, type LogSource } from '@/api/logs' +import type { Cluster } from '@/types' + +type SourceForm = Partial & { basicPass?: string; bearerToken?: string } +const emptyForm: SourceForm = { name: '', targetType: 'url', url: '', authType: 'none', access: [] } + +/** 数据源管理 — 行卡片列表 + 新建/编辑 Dialog(平台部署的托管源只读,手动源可编辑/删除) */ +export const SourcesManager = memo(function SourcesManager({ sources, onSaved }: { + sources: LogSource[] + onSaved: () => void +}) { + const { t } = useTranslation('logs') + const [clusters, setClusters] = useState([]) + const [form, setForm] = useState(null) + const [saving, setSaving] = useState(false) + const [testing, setTesting] = useState(false) + const [deleteTarget, setDeleteTarget] = useState(null) + + useEffect(() => { + clusterApi.list({ page: 1, pageSize: 100 }).then(result => setClusters(result.list)).catch(() => setClusters([])) + }, []) + + const addressOf = (source: Partial) => { + if (source.targetType === 'url') return source.url ?? '' + const clusterName = clusters.find(cluster => cluster.id === source.clusterID)?.name + return [clusterName, source.namespace, `${source.service || 'loki'}:${source.port || 3100}`].filter(Boolean).join(' / ') + } + const typeText = (source: Partial) => source.targetType === 'url' ? t('source.targetUrl') : t('source.targetClusterSvc') + const authText = (source: Partial) => + source.authType === 'basic' ? t('source.authBasic') + : source.authType === 'bearer' ? t('source.authBearer') + : source.authType === 'headers' ? t('source.authHeaders') + : t('source.authNone') + + function test(data: Partial & { basicPass?: string; bearerToken?: string }) { + setTesting(true) + logsApi.testSource(data) + .then(result => { if (result.success) toast.info(result.message); else toast.error(result.message) }) + .catch(err => toast.error(err instanceof Error ? err.message : t('source.test'))) + .finally(() => setTesting(false)) + } + + function save() { + if (!form || !form.name?.trim()) return + setSaving(true) + const action = form.id ? logsApi.updateSource(form.id, form) : logsApi.createSource(form) + action + .then(() => { setForm(null); onSaved() }) + .catch(err => toast.error(err instanceof Error ? err.message : t('source.saveFailed'))) + .finally(() => setSaving(false)) + } + + function remove() { + if (!deleteTarget) return + setSaving(true) + logsApi.deleteSource(deleteTarget.id) + .then(() => { setDeleteTarget(null); if (form?.id === deleteTarget.id) setForm(null); onSaved() }) + .catch(err => toast.error(err instanceof Error ? err.message : t('source.saveFailed'))) + .finally(() => setSaving(false)) + } + + const labelClass = 'text-sm font-medium' + + return
+

{t('source.hint')}

+ + +
+ + + {t('source.heading')} + {t('source.countLabel', { count: sources.length })} + + +
+
+ + {sources.length === 0 ? ( + + ) : ( +
+ {sources.map(source => { + const managed = source.managed + const Icon = source.targetType === 'url' ? Globe : Server + return ( +
+
+
+ +
+
+
+ {source.name} + {source.isDefault && {t('source.defaultBadge')}} + {managed && ( + + + {t('source.managedBadge')} + + )} +
+

+ {typeText(source)} · {addressOf(source)} · {authText(source)} +

+
+
+
+ + {!managed && ( + + )} + {!managed && ( + + )} +
+
+ ) + })} +
+ )} +
+
+ + {/* 新建 / 编辑弹窗 */} + { if (!open && !saving) setForm(null) }}> + + + {form?.id ? `${t('source.editTitle')} · ${form.name}` : t('source.createTitle')} + + {form && ( +
+ +
+ {t('source.typeLabel')} +
+ + +
+
+ {form.targetType === 'url' ? ( + + ) : ( + <> + + + + + + )} + + {form.authType === 'basic' && ( + <> + + + + )} + {form.authType === 'bearer' && ( + + )} + {form.authType === 'headers' && ( +