diff --git a/frontend/src/components/monitor/log-center/DatasourceCard.tsx b/frontend/src/components/monitor/log-center/DatasourceCard.tsx
deleted file mode 100644
index 834316a..0000000
--- a/frontend/src/components/monitor/log-center/DatasourceCard.tsx
+++ /dev/null
@@ -1,245 +0,0 @@
-// DatasourceCard.tsx — 数据源卡片组件 // Logan.Li
-
-import { Check, X, Link, Pencil, Trash2, Loader2 } from 'lucide-react';
-import { ENGINE_CONFIG } from './logLibraryTypes';
-import type { LogDatasource } from './logLibraryTypes';
-import { StatusDot } from './StatusDot';
-
-interface DatasourceCardProps {
- ds: LogDatasource;
- onEdit: (ds: LogDatasource) => void;
- onDelete: (ds: LogDatasource) => void;
- onTest: (ds: LogDatasource) => void;
- testingId: number | null;
- testResult: { id: number; success: boolean; message: string } | null;
-}
-
-/** 单个数据源卡片:展示引擎、名称、URL、状态及操作按钮 */
-export function DatasourceCard({ ds, onEdit, onDelete, onTest, testingId, testResult }: DatasourceCardProps) {
- const cfg = ENGINE_CONFIG[ds.engine];
- const EngineIcon = cfg.icon;
- const isTesting = testingId === ds.id;
- const showResult = testResult?.id === ds.id;
-
- return (
-
{
- (e.currentTarget as HTMLDivElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
- }}
- onMouseLeave={(e) => {
- (e.currentTarget as HTMLDivElement).style.boxShadow = 'none';
- }}
- >
- {/* 引擎色条 */}
-
-
- {/* 头部:图标 + 名称 + 默认标记 */}
-
-
-
-
-
-
-
- {ds.name}
-
- {ds.isDefault && (
-
- 默认
-
- )}
-
-
{cfg.label}
-
-
-
- {/* URL */}
-
-
-
- {ds.url}
-
-
-
- {/* 描述 */}
- {ds.description && (
-
- {ds.description}
-
- )}
-
- {/* 状态行 + 操作按钮 */}
-
-
-
-
- {/* 测试连接 */}
-
-
- {/* 编辑 */}
-
-
- {/* 删除 */}
-
-
-
-
- {/* 测试结果提示 */}
- {showResult && (
-
- {testResult.success ? : }
- {testResult.message}
-
- )}
-
- );
-}
diff --git a/frontend/src/components/monitor/log-center/DatasourceDialog.tsx b/frontend/src/components/monitor/log-center/DatasourceDialog.tsx
deleted file mode 100644
index 30ec62f..0000000
--- a/frontend/src/components/monitor/log-center/DatasourceDialog.tsx
+++ /dev/null
@@ -1,394 +0,0 @@
-// DatasourceDialog.tsx — 添加/编辑数据源对话框 // Logan.Li
-
-import { useState, useEffect } from 'react';
-import { X, Check, ChevronDown, Star } from 'lucide-react';
-import { ENGINE_CONFIG } from './logLibraryTypes';
-import type { LogDatasource } from './logLibraryTypes';
-
-interface DatasourceDialogProps {
- open: boolean;
- editing: LogDatasource | null;
- onSave: (ds: LogDatasource) => void;
- onCancel: () => void;
-}
-
-/** 添加/编辑数据源对话框:引擎选择、名称、URL、描述、认证、默认设置 */
-export function DatasourceDialog({ open, editing, onSave, onCancel }: DatasourceDialogProps) {
- const [engine, setEngine] = useState<'loki' | 'elasticsearch'>('loki');
- const [name, setName] = useState('');
- const [url, setUrl] = useState('');
- const [description, setDescription] = useState('');
- const [isDefault, setIsDefault] = useState(false);
- const [useAuth, setUseAuth] = useState(false);
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [engineMenuOpen, setEngineMenuOpen] = useState(false);
-
- // 初始化表单 // Logan.Li
- useEffect(() => {
- if (editing) {
- setEngine(editing.engine);
- setName(editing.name);
- setUrl(editing.url);
- setDescription(editing.description || '');
- setIsDefault(editing.isDefault);
- const has = !!editing.auth;
- setUseAuth(has);
- setUsername(editing.auth?.username || '');
- setPassword(editing.auth?.password || '');
- } else {
- setEngine('loki');
- setName('');
- setUrl('');
- setDescription('');
- setIsDefault(false);
- setUseAuth(false);
- setUsername('');
- setPassword('');
- }
- setEngineMenuOpen(false);
- }, [editing, open]);
-
- const handleSave = () => {
- if (!name.trim() || !url.trim()) return;
- onSave({
- id: editing?.id || Date.now(),
- engine,
- name: name.trim(),
- url: url.trim(),
- description: description.trim() || undefined,
- status: editing?.status || 'untested',
- isDefault,
- auth: useAuth && username.trim() ? { username: username.trim(), password: password.trim() } : null,
- });
- };
-
- const isValid = name.trim() && url.trim() && (!useAuth || username.trim());
-
- if (!open) return null;
-
- const cfg = ENGINE_CONFIG[engine];
- const EngineIcon = cfg.icon;
-
- return (
-
-
e.stopPropagation()}
- >
- {/* 标题栏 */}
-
-
- {editing ? '编辑数据源' : '添加数据源'}
-
-
-
-
- {/* 表单内容 */}
-
- {/* 引擎类型选择 */}
-
-
-
-
- {engineMenuOpen && (
-
- {Object.entries(ENGINE_CONFIG).map(([key, c]) => {
- const Icon = c.icon;
- return (
-
- );
- })}
-
- )}
-
-
-
- {/* 数据源名称 */}
-
-
- setName(e.target.value)}
- placeholder="如:prod-loki"
- style={{ width: '100%', padding: '8px 12px', borderRadius: 6, fontSize: 13 }}
- />
-
-
- {/* URL */}
-
-
- setUrl(e.target.value)}
- placeholder={cfg.placeholder}
- style={{ width: '100%', padding: '8px 12px', borderRadius: 6, fontSize: 13 }}
- />
-
-
- {/* 描述 */}
-
-
- setDescription(e.target.value)}
- placeholder="可选,如:生产环境 Loki"
- style={{ width: '100%', padding: '8px 12px', borderRadius: 6, fontSize: 13 }}
- />
-
-
- {/* Basic Auth 开关 */}
-
setUseAuth(!useAuth)}
- />
-
- {/* 认证信息 */}
- {useAuth && (
-
- )}
-
- {/* 设为默认 */}
- setIsDefault(!isDefault)}
- icon={}
- />
-
-
- {/* 底部按钮 */}
-
-
-
-
-
-
- );
-}
-
-// ==================== 内联子组件:开关行 ==================== // Logan.Li
-
-function ToggleRow({
- label,
- enabled,
- onToggle,
- icon,
-}: {
- label: string;
- enabled: boolean;
- onToggle: () => void;
- icon?: React.ReactNode;
-}) {
- return (
-
-
- {icon}
- {label}
-
-
-
- );
-}
diff --git a/frontend/src/components/monitor/log-center/DeleteConfirmDialog.tsx b/frontend/src/components/monitor/log-center/DeleteConfirmDialog.tsx
deleted file mode 100644
index 8ed5378..0000000
--- a/frontend/src/components/monitor/log-center/DeleteConfirmDialog.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-// DeleteConfirmDialog.tsx — 删除确认对话框 // Logan.Li
-
-import { AlertTriangle } from 'lucide-react';
-import type { LogDatasource } from './logLibraryTypes';
-
-interface DeleteConfirmDialogProps {
- open: boolean;
- datasource: LogDatasource | null;
- hasAlerts: boolean;
- onConfirm: () => void;
- onCancel: () => void;
-}
-
-/** 删除数据源二次确认对话框 */
-export function DeleteConfirmDialog({
- open,
- datasource,
- hasAlerts,
- onConfirm,
- onCancel,
-}: DeleteConfirmDialogProps) {
- if (!open || !datasource) return null;
-
- return (
-
-
e.stopPropagation()}
- >
- {/* 标题 */}
-
-
- {/* 内容 */}
-
- 确定要删除数据源 {datasource.name} 吗?
- 此操作不可恢复。
-
-
- {hasAlerts && (
-
-
- 该数据源有关联的告警规则,请先删除告警后再删除数据源。
-
- )}
-
- {/* 按钮 */}
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/components/monitor/log-center/EsQueryPanel.tsx b/frontend/src/components/monitor/log-center/EsQueryPanel.tsx
deleted file mode 100644
index 097568c..0000000
--- a/frontend/src/components/monitor/log-center/EsQueryPanel.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-// EsQueryPanel — Elasticsearch 查询 // Logan.Li
-import { useState, useEffect, useCallback } from 'react';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { Play, Plus, RefreshCw, Loader2 } from 'lucide-react';
-import { useToast } from '@/hooks/use-toast';
-import { logCenterApi, type LogDataSource, type LogQueryEntry } from '@/api/monitor/log-center';
-import { LogDatasourceDialog } from './LogDatasourceDialog';
-import { LogResultsTable } from './LogResultsTable';
-import { useLogQueryContext } from './LogQueryContext';
-
-const TIME_PRESETS = [
- { label: '15 分钟', value: '15m' },
- { label: '1 小时', value: '1h' },
- { label: '6 小时', value: '6h' },
- { label: '24 小时', value: '24h' },
-];
-
-function presetToStart(preset: string): string {
- const m = preset.match(/^(\d+)(m|h)$/);
- if (!m) return new Date(Date.now() - 3600000).toISOString();
- const n = parseInt(m[1], 10);
- const ms = m[2] === 'h' ? n * 3600000 : n * 60000;
- return new Date(Date.now() - ms).toISOString();
-}
-
-export function EsQueryPanel() {
- const { toast } = useToast();
- const { setContext } = useLogQueryContext();
- const [sources, setSources] = useState([]);
- const [dsId, setDsId] = useState('');
- const [index, setIndex] = useState('logstash-*');
- const [query, setQuery] = useState('level:error AND message:*timeout*');
- const [timePreset, setTimePreset] = useState('1h');
- const [limit, setLimit] = useState(200);
- const [entries, setEntries] = useState([]);
- const [loading, setLoading] = useState(false);
- const [dsDialog, setDsDialog] = useState(false);
-
- const loadSources = useCallback(async () => {
- const list = await logCenterApi.listDatasources('elasticsearch').catch(() => []);
- const arr = Array.isArray(list) ? list : [];
- setSources(arr);
- if (!dsId && arr.length) {
- const def = arr.find(s => s.is_default) || arr[0];
- setDsId(String(def.id));
- if (def.default_index) setIndex(def.default_index);
- }
- }, [dsId]);
-
- useEffect(() => { loadSources(); }, [loadSources]);
-
- const syncContext = useCallback((q: string, id: string, idx: string) => {
- const ds = sources.find(s => String(s.id) === id);
- setContext({
- engine: 'elasticsearch',
- datasourceId: ds ? ds.id : undefined,
- datasourceName: ds?.name,
- query: q,
- index: idx,
- timeRange: timePreset,
- });
- }, [sources, timePreset, setContext]);
-
- useEffect(() => {
- syncContext(query, dsId, index);
- }, [query, dsId, index, syncContext]);
-
- const runQuery = async () => {
- if (!dsId) return;
- setLoading(true);
- try {
- const res = await logCenterApi.queryElasticsearch({
- datasource_id: Number(dsId),
- index: index.trim() || undefined,
- query: query.trim() || '*',
- size: limit,
- start: presetToStart(timePreset),
- end: new Date().toISOString(),
- });
- setEntries(res?.entries || []);
- toast({ title: '查询完成', description: `共 ${res?.total ?? 0} 条` });
- } catch (e: any) {
- toast({ title: '查询失败', description: e?.message || String(e), variant: 'destructive' });
- setEntries([]);
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- setIndex(e.target.value)} className="h-9 mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
-
-
-
-
-
-
-
-
-
-
-
setQuery(e.target.value)}
- className="mt-1 font-mono text-xs h-10 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]"
- placeholder='status:500 AND path:"/api/*"'
- />
-
支持 Lucene 语法;留空则匹配全部 · 单次最多 500 条,不落库
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/frontend/src/components/monitor/log-center/LogDatasourcePage.tsx b/frontend/src/components/monitor/log-center/LogDatasourcePage.tsx
index baaa0dc..5e7bf1c 100644
--- a/frontend/src/components/monitor/log-center/LogDatasourcePage.tsx
+++ b/frontend/src/components/monitor/log-center/LogDatasourcePage.tsx
@@ -12,16 +12,10 @@ import { logCenterApi, type LogDataSource } from '@/api/monitor/log-center';
import { LogDatasourceDialog } from './LogDatasourceDialog';
import type { LogEngine } from './log-center-types';
-// Demo 兜底数据源(API 无数据时展示) // Logan.Li
-const DEMO_FALLBACK: LogDataSource[] = [
- { id: -1, name: 'demo-loki', type: 'loki', url: '' },
- { id: -2, name: 'demo-es', type: 'elasticsearch', url: '' },
-];
-
export function LogDatasourcePage() {
const { toast } = useToast();
const confirm = useConfirm();
- const [sources, setSources] = useState(DEMO_FALLBACK);
+ const [sources, setSources] = useState([]);
const [loading, setLoading] = useState(true);
const [dialog, setDialog] = useState<{ open: boolean; engine: LogEngine; editing?: LogDataSource | null }>({
open: false, engine: 'loki', editing: null,
@@ -32,10 +26,9 @@ export function LogDatasourcePage() {
try {
const res = await logCenterApi.listDatasources();
const apiList: LogDataSource[] = Array.isArray(res) ? res : [];
- // 真实数据源在前 + demo 兜底在后 // Logan.Li
- setSources([...apiList, ...DEMO_FALLBACK]);
+ setSources(apiList);
} catch {
- setSources(DEMO_FALLBACK);
+ setSources([]);
} finally {
setLoading(false);
}
@@ -116,31 +109,28 @@ export function LogDatasourcePage() {
},
{
key: 'actions', header: '操作', align: 'right', hideable: false,
- render: (ds) => {
- const isDemo = ds.id < 0 || !ds.url;
- return (
+ render: (ds) => (
{!ds.is_default && (
)}
- );
- },
+ ),
},
];
diff --git a/frontend/src/components/monitor/log-center/LogLibraryPage.tsx b/frontend/src/components/monitor/log-center/LogLibraryPage.tsx
deleted file mode 100644
index de87a5e..0000000
--- a/frontend/src/components/monitor/log-center/LogLibraryPage.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-// LogLibraryPage.tsx — 日志库(数据源管理)主页面 // Logan.Li
-// 组装卡片列表、编辑对话框、删除确认对话框,管理全局状态
-
-import { useState, useEffect, useCallback } from 'react';
-import { Database, Flame, Plus, Check, Server } from 'lucide-react';
-import { loadDatasources, saveDatasources, ENGINE_CONFIG } from './logLibraryTypes';
-import type { LogDatasource } from './logLibraryTypes';
-import { DatasourceCard } from './DatasourceCard';
-import { DatasourceDialog } from './DatasourceDialog';
-import { DeleteConfirmDialog } from './DeleteConfirmDialog';
-
-export function LogLibraryPage() {
- const [datasources, setDatasources] = useState(loadDatasources);
- const [dialogOpen, setDialogOpen] = useState(false);
- const [editing, setEditing] = useState(null);
- const [deleteOpen, setDeleteOpen] = useState(false);
- const [deleting, setDeleting] = useState(null);
- const [testingId, setTestingId] = useState(null);
- const [testResult, setTestResult] = useState<{ id: number; success: boolean; message: string } | null>(null);
-
- // 数据变更时持久化到 localStorage // Logan.Li
- useEffect(() => {
- saveDatasources(datasources);
- }, [datasources]);
-
- // 统计数据 // Logan.Li
- const lokiCount = datasources.filter((d) => d.engine === 'loki').length;
- const esCount = datasources.filter((d) => d.engine === 'elasticsearch').length;
- const connectedCount = datasources.filter((d) => d.status === 'connected').length;
-
- // 打开添加对话框 // Logan.Li
- const openCreate = useCallback(() => {
- setEditing(null);
- setDialogOpen(true);
- }, []);
-
- // 打开编辑对话框 // Logan.Li
- const openEdit = useCallback((ds: LogDatasource) => {
- setEditing(ds);
- setDialogOpen(true);
- }, []);
-
- // 保存数据源(新增或更新) // Logan.Li
- const handleSave = useCallback((ds: LogDatasource) => {
- setDatasources((prev) => {
- // 若设为默认,取消同引擎其他数据源的默认标记 // Logan.Li
- let next = prev.map((item) =>
- ds.isDefault && item.engine === ds.engine && item.id !== ds.id
- ? { ...item, isDefault: false }
- : item
- );
- const exists = next.find((item) => item.id === ds.id);
- if (exists) {
- next = next.map((item) => (item.id === ds.id ? ds : item));
- } else {
- next = [...next, ds];
- }
- return next;
- });
- setDialogOpen(false);
- }, []);
-
- // 打开删除确认 // Logan.Li
- const openDelete = useCallback((ds: LogDatasource) => {
- setDeleting(ds);
- setDeleteOpen(true);
- }, []);
-
- // 确认删除 // Logan.Li
- const confirmDelete = useCallback(() => {
- if (!deleting) return;
- setDatasources((prev) => prev.filter((d) => d.id !== deleting.id));
- setDeleteOpen(false);
- setDeleting(null);
- }, [deleting]);
-
- // 测试连接 // Logan.Li
- const handleTest = useCallback((ds: LogDatasource) => {
- setTestingId(ds.id);
- setTestResult(null);
-
- // 模拟异步测试 // Logan.Li
- setTimeout(() => {
- const success = ds.status !== 'error';
- const streamCount = Math.floor(Math.random() * 50) + 10;
- setTestResult({
- id: ds.id,
- success,
- message: success
- ? `连接成功,${streamCount} 个日志流`
- : '连接失败:无法访问目标地址 (timeout)',
- });
- // 同步更新数据源状态 // Logan.Li
- setDatasources((prev) =>
- prev.map((d) =>
- d.id === ds.id ? { ...d, status: success ? 'connected' : 'error' } : d
- )
- );
- setTestingId(null);
- }, 1200);
- }, []);
-
- // 检查数据源是否有关联告警 // Logan.Li
- const hasRelatedAlerts = useCallback((ds: LogDatasource): boolean => {
- try {
- const alertsRaw = localStorage.getItem('log-center:alerts');
- if (alertsRaw) {
- const alerts = JSON.parse(alertsRaw);
- return alerts.some((a: { datasourceId?: number; datasourceName?: string }) =>
- a.datasourceId === ds.id || a.datasourceName === ds.name
- );
- }
- } catch { /* ignore */ }
- return false;
- }, []);
-
- return (
-
- {/* 页面标题 + 统计 */}
-
-
-
-
- 日志库
-
-
- 管理 Loki 和 Elasticsearch 数据源配置
-
-
-
- {/* 统计 + 添加按钮 */}
-
-
} label="Loki" value={lokiCount} valueColor={ENGINE_CONFIG.loki.color} />
-
} label="ES" value={esCount} valueColor={ENGINE_CONFIG.elasticsearch.color} />
-
} label="正常" value={connectedCount} valueColor="var(--dg-green)" />
-
-
-
-
-
- {/* 分隔线 */}
-
-
- {/* 数据源卡片网格 */}
- {datasources.length === 0 ? (
-
- ) : (
-
- {datasources.map((ds) => (
-
- ))}
-
- )}
-
- {/* 子对话框 */}
-
setDialogOpen(false)} />
- { setDeleteOpen(false); setDeleting(null); }}
- />
-
- );
-}
-
-// ==================== 内联子组件:统计徽章 ==================== // Logan.Li
-
-function StatBadge({
- icon,
- label,
- value,
- valueColor,
-}: {
- icon: React.ReactNode;
- label: string;
- value: number;
- valueColor: string;
-}) {
- return (
-
- {icon}
- {label}
- {value}
-
- );
-}
-
-// ==================== 内联子组件:空状态 ==================== // Logan.Li
-
-function EmptyState() {
- return (
-
-
-
暂无数据源
-
点击右上角「添加数据源」按钮创建
-
- );
-}
diff --git a/frontend/src/components/monitor/log-center/LogResultsTable.tsx b/frontend/src/components/monitor/log-center/LogResultsTable.tsx
deleted file mode 100644
index 9edf382..0000000
--- a/frontend/src/components/monitor/log-center/LogResultsTable.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-// LogResultsTable — 日志查询结果表格 // Logan.Li
-import { Badge } from '@/components/ui/badge';
-import type { LogQueryEntry } from '@/api/monitor/log-center';
-
-const levelColor: Record = {
- error: 'bg-red-500/15 text-red-400 border-red-500/30',
- warn: 'bg-amber-500/15 text-amber-400 border-amber-500/30',
- warning: 'bg-amber-500/15 text-amber-400 border-amber-500/30',
- info: 'bg-blue-500/15 text-blue-400 border-blue-500/30',
- debug: 'bg-slate-500/15 text-slate-400 border-slate-500/30',
-};
-
-export function LogResultsTable({ entries, loading }: { entries: LogQueryEntry[]; loading?: boolean }) {
- if (loading) {
- return 查询中...
;
- }
- if (entries.length === 0) {
- return 暂无日志,调整查询条件后重试
;
- }
- return (
-
-
- 时间
- 级别
- 内容
-
-
- {entries.map((e, i) => (
-
-
{e.timestamp}
-
- {e.level ? (
- {e.level}
- ) : '—'}
-
-
{e.message}
-
- ))}
-
-
- );
-}
\ No newline at end of file
diff --git a/frontend/src/components/monitor/log-center/LokiQueryPanel.tsx b/frontend/src/components/monitor/log-center/LokiQueryPanel.tsx
deleted file mode 100644
index 3a9aa93..0000000
--- a/frontend/src/components/monitor/log-center/LokiQueryPanel.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-// LokiQueryPanel — LogQL 查询 // Logan.Li
-import { useState, useEffect, useCallback } from 'react';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { Textarea } from '@/components/ui/textarea';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { Play, Plus, RefreshCw, Loader2 } from 'lucide-react';
-import { useToast } from '@/hooks/use-toast';
-import { logCenterApi, type LogDataSource, type LogQueryEntry } from '@/api/monitor/log-center';
-import { LogDatasourceDialog } from './LogDatasourceDialog';
-import { LogResultsTable } from './LogResultsTable';
-import { useLogQueryContext } from './LogQueryContext';
-
-const TIME_PRESETS = [
- { label: '15 分钟', value: '15m' },
- { label: '1 小时', value: '1h' },
- { label: '6 小时', value: '6h' },
- { label: '24 小时', value: '24h' },
-];
-
-function presetToStart(preset: string): string {
- const m = preset.match(/^(\d+)(m|h)$/);
- if (!m) return new Date(Date.now() - 3600000).toISOString();
- const n = parseInt(m[1], 10);
- const ms = m[2] === 'h' ? n * 3600000 : n * 60000;
- return new Date(Date.now() - ms).toISOString();
-}
-
-export function LokiQueryPanel() {
- const { toast } = useToast();
- const { setContext } = useLogQueryContext();
- const [sources, setSources] = useState([]);
- const [dsId, setDsId] = useState('');
- const [query, setQuery] = useState('{namespace="default"} |= "error"');
- const [timePreset, setTimePreset] = useState('1h');
- const [limit, setLimit] = useState(200);
- const [entries, setEntries] = useState([]);
- const [loading, setLoading] = useState(false);
- const [dsDialog, setDsDialog] = useState(false);
-
- const loadSources = useCallback(async () => {
- const list = await logCenterApi.listDatasources('loki').catch(() => []);
- const arr = Array.isArray(list) ? list : [];
- setSources(arr);
- if (!dsId && arr.length) {
- const def = arr.find(s => s.is_default) || arr[0];
- setDsId(String(def.id));
- }
- }, [dsId]);
-
- useEffect(() => { loadSources(); }, [loadSources]);
-
- const syncContext = useCallback((q: string, id: string) => {
- const ds = sources.find(s => String(s.id) === id);
- setContext({
- engine: 'loki',
- datasourceId: ds ? ds.id : undefined,
- datasourceName: ds?.name,
- query: q,
- timeRange: timePreset,
- });
- }, [sources, timePreset, setContext]);
-
- useEffect(() => {
- syncContext(query, dsId);
- }, [query, dsId, syncContext]);
-
- const runQuery = async () => {
- if (!dsId || !query.trim()) return;
- setLoading(true);
- try {
- const res = await logCenterApi.queryLoki({
- datasource_id: Number(dsId),
- query: query.trim(),
- limit,
- start: presetToStart(timePreset),
- end: new Date().toISOString(),
- });
- setEntries(res?.entries || []);
- toast({ title: '查询完成', description: `共 ${res?.total ?? 0} 条` });
- } catch (e: any) {
- toast({ title: '查询失败', description: e?.message || String(e), variant: 'destructive' });
- setEntries([]);
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- setLimit(Number(e.target.value) || 200)} className="h-9 mt-1 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
-
-
-
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/frontend/src/components/monitor/log-center/StatusDot.tsx b/frontend/src/components/monitor/log-center/StatusDot.tsx
deleted file mode 100644
index 0ff5722..0000000
--- a/frontend/src/components/monitor/log-center/StatusDot.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-// StatusDot.tsx — 状态指示器组件 // Logan.Li
-
-import { STATUS_CONFIG } from './logLibraryTypes';
-
-interface StatusDotProps {
- status: string;
-}
-
-/** 数据源状态指示器:彩色圆点 + 文字 */
-export function StatusDot({ status }: StatusDotProps) {
- const cfg = STATUS_CONFIG[status] || STATUS_CONFIG.untested;
- return (
-
-
- {cfg.label}
-
- );
-}
diff --git a/frontend/src/components/monitor/log-center/UnifiedQueryPanel.tsx b/frontend/src/components/monitor/log-center/UnifiedQueryPanel.tsx
index 3e879a4..cb3aa74 100644
--- a/frontend/src/components/monitor/log-center/UnifiedQueryPanel.tsx
+++ b/frontend/src/components/monitor/log-center/UnifiedQueryPanel.tsx
@@ -13,7 +13,7 @@ import { QueryHistoryPanel } from './QueryHistoryPanel';
import { LogAlertFromQueryDialog } from './LogAlertFromQueryDialog';
import { logCenterApi, type LogDataSource } from '@/api/monitor/log-center';
import type { LogEngine, TimeRange, ResultTab, ViewMode, LogEntry, StatTable, LogCluster, QueryHistoryItem } from './types-v3';
-import { ENGINE_META, generateMockEntries, generateMockStatTable, generateMockClusters, extractFields, generateHistogramData, historyStorage } from './types-v3';
+import { ENGINE_META, buildStatTableFromEntries, buildClustersFromEntries, extractFields, generateHistogramData, historyStorage } from './types-v3';
import { useToast } from '@/hooks/use-toast';
// 默认最近5分钟 // Logan.Li
@@ -24,12 +24,6 @@ const DEFAULT_TIME_RANGE: TimeRange = {
type: 'relative',
};
-// Demo 数据源(后端无数据源时的兜底)// Logan.Li
-const DEMO_SOURCES: Record<'loki' | 'elasticsearch', LogDataSource[]> = {
- loki: [{ id: -1, name: 'demo-loki', type: 'loki', url: '' }],
- elasticsearch: [{ id: -2, name: 'demo-es', type: 'elasticsearch', url: '' }],
-};
-
// 引擎默认查询语句 // Logan.Li
const DEFAULT_QUERIES: Record<'loki' | 'elasticsearch', string> = {
loki: '{app="cex-admin-api"} |= ""',
@@ -81,22 +75,34 @@ const ALL_TEMPLATES: Record<'loki' | 'elasticsearch', { label: string; query: st
// LogQL / Lucene 语法高亮 → HTML // Logan.Li
function highlightSyntax(query: string, engine: 'loki' | 'elasticsearch'): string {
if (!query) return '.';
+ // 先转义 HTML 特殊字符,避免查询语句中的 < > & 破坏标签结构 // Logan.Li
+ const esc = query.replace(/&/g, '&').replace(//g, '>');
+ const span = (color: string, text: string) => `${text}`;
if (engine === 'loki') {
- return query
- .replace(/([a-zA-Z_][\w-]*)(?==)/g, '$1')
- .replace(/("(?:[^"\\]|\\.)*")/g, '$1')
- .replace(/(\|=|\!=|=~|!~|=)/g, '$1')
- .replace(/(\|json|\|regexp|\|line_format|\|unwrap|\|pattern|\|label_format)/g, '$1')
- .replace(/\b(count_over_time|sum|rate|avg|max|min|count|by|over)\b/g, '$1')
- .replace(/(\{|\})/g, '$1')
- .replace(/.';
+ // 单正则 + 回调:一次匹配,避免链式 replace 对已生成颜色值的二次匹配 // Logan.Li
+ return esc.replace(
+ /("(?:[^"\\]|\\.)*")|([a-zA-Z_][\w-]*)(?==)|(\|json|\|regexp|\|line_format|\|unwrap|\|pattern|\|label_format)|\b(count_over_time|sum|rate|avg|max|min|count|by|over)\b|([{}])|(\|=|\!=|=~|!~|=)/g,
+ (m, str, ident, pipe, fn, brace, op) => {
+ if (str) return span('#22c55e', str);
+ if (ident) return span('#f59e0b', ident);
+ if (pipe) return span('#8b5cf6', pipe);
+ if (fn) return span('#06b6d4', fn);
+ if (brace) return span('#f59e0b', brace);
+ if (op) return span('#f43f5e', op);
+ return m;
+ },
+ ) + '.';
}
- return query
- .replace(/\b(AND|OR|NOT)\b/g, '$1')
- .replace(/([a-zA-Z_@][\w.-]*):/g, '$1:')
- .replace(/("(?:[^"\\]|\\.)*")/g, '$1')
- .replace(/(\*|\?|\~|\^|\[|\]|\(|\))/g, '$1')
- .replace(/.';
+ return esc.replace(
+ /("(?:[^"\\]|\\.)*")|\b(AND|OR|NOT)\b|([a-zA-Z_@][\w.-]*)(?=:)|([*?~^[\]()])/g,
+ (m, str, kw, field, punct) => {
+ if (str) return span('#22c55e', str);
+ if (kw) return span('#f43f5e', kw);
+ if (field) return span('#f59e0b', field);
+ if (punct) return span('#8b5cf6', punct);
+ return m;
+ },
+ ) + '.';
}
// 语法高亮输入框:底层高亮 + 上层透明输入 // Logan.Li
@@ -132,15 +138,13 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
const meta = ENGINE_META[effEngine];
const badge = ENGINE_BADGE[effEngine];
- // 从后端加载真实数据源,同时保留 demo 兜底 // Logan.Li
- const [sources, setSources] = useState(DEMO_SOURCES[effEngine]);
+ // 从后端加载真实数据源(无 demo 兜底,无源时引导用户前往日志库配置)// Logan.Li
+ const [sources, setSources] = useState([]);
useEffect(() => {
logCenterApi.listDatasources(effEngine).then((res: any) => {
const list: LogDataSource[] = Array.isArray(res) ? res : (res?.list || []);
- // 合并真实数据源 + demo 兜底(真实源在前) // Logan.Li
- if (list.length > 0) setSources([...list, ...DEMO_SOURCES[effEngine]]);
- else setSources(DEMO_SOURCES[effEngine]);
- }).catch(() => setSources(DEMO_SOURCES[effEngine]));
+ setSources(list);
+ }).catch(() => setSources([]));
}, [effEngine]);
const [query, setQuery] = useState(DEFAULT_QUERIES[effEngine]);
@@ -168,13 +172,15 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
const [saveMenuOpen, setSaveMenuOpen] = useState(false);
const [hasQueried, setHasQueried] = useState(false);
- // 数据源切换后更新 dsId // Logan.Li
+ // 数据源加载后选中默认或首个(无源时保持空)// Logan.Li
useEffect(() => {
- if (sources.length > 0) setDsId(String(sources[0].id));
+ if (sources.length > 0) {
+ const def = sources.find(s => s.is_default) || sources[0];
+ setDsId(String(def.id));
+ }
}, [sources]);
const currentSource = sources.find(s => String(s.id) === dsId);
- const isDemo = currentSource && (currentSource.id < 0 || !currentSource.url);
const fields = useMemo(() => extractFields(entries), [entries]);
const commonLabels = useMemo(() => extractCommonLabels(entries), [entries]);
@@ -182,26 +188,13 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
const totalPages = Math.max(1, Math.ceil(sortedEntries.length / perPage));
const pagedEntries = useMemo(() => { const s = (page - 1) * perPage; return sortedEntries.slice(s, s + perPage); }, [sortedEntries, page, perPage]);
- // 执行查询 — 接入真实 API(demo 源则使用 mock)// Logan.Li
+ // 执行查询 — 接入真实 API(无 mock)// Logan.Li
const handleRun = useCallback(async () => {
if (!currentSource) return;
const dsIdNum = currentSource.id;
setLoading(true);
const t0 = performance.now();
- // Demo 源:模拟数据
- if (isDemo) {
- setTimeout(() => {
- const mock = generateMockEntries(effEngine as LogEngine, 120);
- setEntries(mock); setHistogramData(generateHistogramData(mock)); setTotalCount(mock.length * 14);
- setQueryTime(Math.round(performance.now() - t0)); setStatTable(generateMockStatTable(effEngine as LogEngine));
- setClusters(generateMockClusters()); setPage(1); setLoading(false); setHasQueried(true);
- historyStorage.add({ engine: effEngine as LogEngine, datasourceName: currentSource.name, query, timeRange: timeRange.label, resultCount: mock.length });
- }, 300);
- return;
- }
-
- // 真实 API 查询
try {
const startISO = (timeRange.start + (timeRange.start.includes('T') ? '' : '.000Z')).replace(' ', 'T');
const endISO = (timeRange.end + (timeRange.end.includes('T') ? '' : '.000Z')).replace(' ', 'T');
@@ -241,8 +234,9 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
setHistogramData(generateHistogramData(mapped));
setTotalCount(data?.total || mapped.length);
setQueryTime(Math.round(performance.now() - t0));
- setStatTable(generateMockStatTable(effEngine as LogEngine));
- setClusters(generateMockClusters());
+ // 统计图表 / 聚类基于真实返回条目聚合(无 mock)// Logan.Li
+ setStatTable(buildStatTableFromEntries(mapped));
+ setClusters(buildClustersFromEntries(mapped));
setPage(1);
setHasQueried(true);
historyStorage.add({ engine: effEngine as LogEngine, datasourceName: currentSource.name, query, timeRange: timeRange.label, resultCount: mapped.length });
@@ -252,7 +246,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
} finally {
setLoading(false);
}
- }, [effEngine, query, currentSource, timeRange, isDemo, toast]);
+ }, [effEngine, query, currentSource, timeRange, toast]);
const toggleField = useCallback((n: string) => setSelectedFields(p => p.includes(n) ? p.filter(f => f !== n) : [...p, n]), []);
const handleSaveMenu = (a: 'query' | 'alert') => { setSaveMenuOpen(false); a === 'query' ? setSaveQueryOpen(true) : setSaveAlertOpen(true); };
@@ -280,10 +274,11 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
-
+ {/* 无数据源时引导用户前往日志库配置 // Logan.Li */}
+ {sources.length === 0 && (
+
+ 暂无{meta.name}数据源,请先前往「日志库」添加数据源。
+
+ )}
+
{/* 快捷查询助手 */}
diff --git a/frontend/src/components/monitor/log-center/logLibraryTypes.ts b/frontend/src/components/monitor/log-center/logLibraryTypes.ts
deleted file mode 100644
index 75ebb87..0000000
--- a/frontend/src/components/monitor/log-center/logLibraryTypes.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-// logLibraryTypes.ts — 日志库(数据源管理)类型定义与常量 // Logan.Li
-
-import type { LogEngine } from './types-v3';
-import { Flame, Database } from 'lucide-react';
-import type { LucideIcon } from 'lucide-react';
-
-/** 数据源认证信息 */
-export interface DatasourceAuth {
- username: string;
- password: string;
-}
-
-/** 日志数据源定义 */
-export interface LogDatasource {
- id: number;
- engine: 'loki' | 'elasticsearch';
- name: string;
- url: string;
- description?: string;
- status: 'connected' | 'error' | 'untested';
- isDefault: boolean;
- auth?: DatasourceAuth | null;
-}
-
-/** 引擎配置信息 */
-export interface EngineConfigItem {
- icon: LucideIcon;
- color: string;
- label: string;
- placeholder: string;
-}
-
-/** 状态显示配置 */
-export interface StatusConfigItem {
- color: string;
- label: string;
- dot: string;
-}
-
-/** localStorage 存储键 */
-export const STORAGE_KEY = 'log-center:datasources';
-
-/** 引擎配置映射 */
-export const ENGINE_CONFIG: Record = {
- loki: { icon: Flame, color: '#06b6d4', label: 'Loki', placeholder: 'http://loki.host:3100' },
- elasticsearch: { icon: Database, color: '#22c55e', label: 'Elasticsearch', placeholder: 'http://es.host:9200' },
-};
-
-/** 状态显示配置映射 */
-export const STATUS_CONFIG: Record = {
- connected: { color: 'var(--dg-green)', label: '正常', dot: '#22c55e' },
- error: { color: 'var(--dg-red)', label: '异常', dot: '#ef4444' },
- untested: { color: 'var(--dg-text-muted)', label: '未测试', dot: '#94a3b8' },
-};
-
-/** Mock 数据源(仅演示用 — demo-loki / demo-es 各一个) */
-export const MOCK_DATASOURCES: LogDatasource[] = [
- { id: 1, engine: 'loki', name: 'demo-loki', url: '', status: 'connected', description: 'Loki 模拟数据', isDefault: true, auth: null },
- { id: 2, engine: 'elasticsearch', name: 'demo-es', url: '', status: 'connected', description: 'ES 模拟数据', isDefault: true, auth: null },
-];
-
-/** 从 localStorage 加载数据源(迁移旧数据自动清理) */
-export function loadDatasources(): LogDatasource[] {
- try {
- const raw = localStorage.getItem(STORAGE_KEY);
- if (raw) {
- const list: LogDatasource[] = JSON.parse(raw);
- // 版本迁移:旧数据(含 prod-loki / dev-loki 等)强制重置为 demo
- const oldNames = list.map((d) => d.name);
- if (oldNames.includes('prod-loki') || oldNames.includes('dev-es') || oldNames.includes('prod-es') || oldNames.includes('dev-loki')) {
- localStorage.removeItem(STORAGE_KEY);
- return [...MOCK_DATASOURCES];
- }
- return list;
- }
- } catch { /* ignore */ }
- return [...MOCK_DATASOURCES];
-}
-
-/** 保存数据源到 localStorage */
-export function saveDatasources(list: LogDatasource[]) {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
-}
-
-/** 引擎图标映射(兼容 types-v3 的 LogEngine) */
-export const ENGINE_ICONS: Record = {
- loki: { Icon: Flame, color: '#06b6d4', name: 'Loki' },
- elasticsearch: { Icon: Database, color: '#22c55e', name: 'ES' },
- sls: { Icon: Database, color: '#f97316', name: 'SLS' },
-};
diff --git a/frontend/src/components/monitor/log-center/types-v3.ts b/frontend/src/components/monitor/log-center/types-v3.ts
index fdd5a6f..514ca89 100644
--- a/frontend/src/components/monitor/log-center/types-v3.ts
+++ b/frontend/src/components/monitor/log-center/types-v3.ts
@@ -136,83 +136,55 @@ export const ENGINE_META: Record = {
- app: APP,
- container: APP,
- detected_level: level.toLowerCase(),
- namespace: 'astralx',
- pod: 'cex-admin-api-66c5cc7d6c-wkngz',
- service_name: APP,
- stream: 'stdout',
- level: level.toLowerCase(),
- node_name: 'ip-172-36-156-240.ap-northeast-1.compute.internal',
- };
-
- const tsISO = ts.toISOString();
- const message = `${tsISO} ${level} 1 --- [${APP}] [${thread}] ${cls} : admin ${['getBalanceFlows', 'getBrokerUser', 'getOrderList', 'authenticate'][Math.floor(Math.random() * 4)]} ${level === 'ERROR' ? 'failed' : level === 'WARN' ? 'resolved' : 'response'}. orgId=${orgId}, brokerUserId=${brokerUserId}`;
-
- const raw = JSON.stringify({ ts: tsISO, level: level.toLowerCase(), logger: cls, thread, message: message.slice(0, 80), app: APP, orgId, brokerUserId });
-
- entries.push({
- timestamp: ts.toISOString().replace('T', ' ').substring(0, 19),
- level: level.toLowerCase(),
- message,
- labels,
- raw: engine === 'loki' ? message : raw,
- source: APP,
- });
- }
- return entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
-}
-
-// 生成统计表格 Mock // Logan.Li
-export function generateMockStatTable(engine: LogEngine): StatTable {
- const methods = ['GET', 'POST', 'PUT', 'DELETE'];
- const levels = ['info', 'warn', 'error'];
- if (engine === 'loki') {
- return {
- columns: [{ name: 'level', type: 'string' }, { name: 'count', type: 'number' }],
- rows: levels.map(l => ({ level: l, count: Math.floor(Math.random() * 5000 + 1000) })),
- };
- }
+// 真实统计聚合 — 基于查询返回的真实条目按级别计数(无 mock)// Logan.Li
+// level 由后端 flattenLokiStreams / flattenESEntries 提取;缺失归 unknown
+export function buildStatTableFromEntries(entries: LogEntry[]): StatTable {
+ const counter = new Map();
+ entries.forEach(e => {
+ const key = (e.level || e.labels?.level || 'unknown').toString();
+ counter.set(key, (counter.get(key) || 0) + 1);
+ });
+ const rows = Array.from(counter.entries())
+ .map(([level, count]) => ({ level, count }))
+ .sort((a, b) => b.count - a.count);
return {
- columns: [{ name: 'method', type: 'string' }, { name: 'count', type: 'number' }],
- rows: methods.map(m => ({ method: m, count: Math.floor(Math.random() * 2000 + 100) })),
+ columns: [
+ { name: 'level', type: 'string' },
+ { name: 'count', type: 'number' },
+ ],
+ rows,
};
}
-// 生成聚类 Mock // Logan.Li
-export function generateMockClusters(): LogCluster[] {
- return [
- { pattern: 'admin getBalanceFlows response. orgId=*, brokerUserId=*', count: 4523, sample: 'admin getBalanceFlows response. orgId=10001, brokerUserId=97164001', percentage: 37.7 },
- { pattern: 'admin getBrokerUser resolved. orgId=*, brokerUserId=*', count: 3281, sample: 'admin getBrokerUser resolved. orgId=10002, brokerUserId=97164234', percentage: 27.3 },
- { pattern: 'admin authenticate response. orgId=*, brokerUserId=*', count: 2156, sample: 'admin authenticate response. orgId=10003, brokerUserId=97164567', percentage: 18.0 },
- { pattern: 'admin getOrderList failed. orgId=*, brokerUserId=*', count: 1024, sample: 'admin getOrderList failed. orgId=10004, brokerUserId=97164890', percentage: 8.5 },
- { pattern: 'admin updateBalance response. orgId=*, brokerUserId=*', count: 890, sample: 'admin updateBalance response. orgId=10001, brokerUserId=97164123', percentage: 7.4 },
- { pattern: 'Other patterns', count: 126, sample: '...', percentage: 1.1 },
- ];
+// 真实日志聚类 — 将 message 中数字/十六进制/IPv4/UUID 泛化为 * 后分组计数 // Logan.Li
+export function buildClustersFromEntries(entries: LogEntry[]): LogCluster[] {
+ const buckets = new Map();
+ const generalize = (msg: string): string => {
+ if (!msg) return '';
+ return msg
+ .replace(/\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g, '*') // UUID(优先于一般十六进制)
+ .replace(/\b\d{1,3}(\.\d{1,3}){3}\b/g, '*') // IPv4
+ .replace(/[0-9a-fA-F]{8,}/g, '*') // 长十六进制串(traceId / objectId 等)
+ .replace(/\d+/g, '*'); // 连续数字
+ };
+ entries.forEach(e => {
+ const msg = e.message || '';
+ if (!msg) return;
+ const pattern = generalize(msg);
+ const hit = buckets.get(pattern);
+ if (hit) hit.count += 1;
+ else buckets.set(pattern, { count: 1, sample: msg });
+ });
+ const total = entries.length || 1;
+ return Array.from(buckets.entries())
+ .map(([pattern, v]) => ({
+ pattern,
+ count: v.count,
+ sample: v.sample,
+ percentage: Math.round((v.count / total) * 1000) / 10,
+ }))
+ .sort((a, b) => b.count - a.count)
+ .slice(0, 10);
}
export function extractFields(entries: LogEntry[]): LogField[] {
@@ -273,17 +245,6 @@ export const savedQueryStorage = {
},
remove(id: string) { localStorage.setItem(SAVED_QUERY_KEY, JSON.stringify(this.getAll().filter(q => q.id !== id))); },
};
-
-// Mock 告警规则 // Logan.Li
-export function getMockAlerts(): AlertRule[] {
- return [
- { id: 1, name: 'Nginx 5xx 错误告警', expression: '{job="nginx"} |= "error" | json | status_code>=500', engine: 'loki', severity: 'critical', enabled: true, description: 'Nginx 5xx 错误数 > 0 触发告警' },
- { id: 2, name: 'API 慢响应检测', expression: 'response_time:>2000 AND path:"/api/*"', engine: 'elasticsearch', severity: 'warning', enabled: true, description: 'API 响应时间超过 2 秒' },
- { id: 3, name: '错误日志频率', expression: "* | select count(*) as cnt where level = 'error' having cnt > 100", engine: 'sls', severity: 'warning', enabled: false, description: '1 小时内错误日志超过 100 条' },
- { id: 4, name: '服务离线检测', expression: '{job="agent"} |= "offline"', engine: 'loki', severity: 'critical', enabled: true, description: 'Agent 离线检测' },
- ];
-}
-
// 快捷查询模板 // Logan.Li
export const QUICK_TEMPLATES: Record = {
loki: [
diff --git a/frontend/src/components/monitor/log-center/useLogQuery.ts b/frontend/src/components/monitor/log-center/useLogQuery.ts
deleted file mode 100644
index 0cc4633..0000000
--- a/frontend/src/components/monitor/log-center/useLogQuery.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-// useLogQuery.ts — 日志查询通用 Hook // Logan.Li
-import { useState, useMemo, useCallback } from 'react';
-import type { LogEntry, ViewMode } from './types-v3';
-import { generateMockEntries, generateHistogramData, extractFields, historyStorage } from './types-v3';
-import type { LogEngine } from './types-v3';
-
-export interface UseLogQueryOptions {
- engine: LogEngine;
- defaultQuery: string;
-}
-
-export function useLogQuery({ engine, defaultQuery }: UseLogQueryOptions) {
- const [query, setQuery] = useState(defaultQuery);
- const [allEntries, setAllEntries] = useState([]);
- const [loading, setLoading] = useState(false);
- const [totalCount, setTotalCount] = useState(0);
- const [queryTime, setQueryTime] = useState(0);
- const [queryStatus, setQueryStatus] = useState<'precise' | 'partial' | 'failed'>('precise');
-
- // 视图状态 // Logan.Li
- const [viewMode, setViewMode] = useState('raw');
- const [wrapLines, setWrapLines] = useState(false);
- const [truncate, setTruncate] = useState(true);
- const [selectedFields, setSelectedFields] = useState(['timestamp', 'level', 'service', 'message']);
-
- // 分页 // Logan.Li
- const [page, setPage] = useState(1);
- const [perPage, setPerPage] = useState(20);
- const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
-
- // 对话框 // Logan.Li
- const [alertDialog, setAlertDialog] = useState(false);
- const [historyPanel, setHistoryPanel] = useState(false);
-
- // 排序后的 entries // Logan.Li
- const sortedEntries = useMemo(() => {
- const arr = [...allEntries];
- if (sortDirection === 'asc') {
- arr.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
- } else {
- arr.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
- }
- return arr;
- }, [allEntries, sortDirection]);
-
- const totalPages = Math.max(1, Math.ceil(sortedEntries.length / perPage));
-
- const pagedEntries = useMemo(() => {
- const start = (page - 1) * perPage;
- return sortedEntries.slice(start, start + perPage);
- }, [sortedEntries, page, perPage]);
-
- // 直方图数据 // Logan.Li
- const histogramData = useMemo(() => generateHistogramData(allEntries), [allEntries]);
-
- // 字段列表 // Logan.Li
- const fields = useMemo(() => extractFields(allEntries), [allEntries]);
-
- // 执行查询 // Logan.Li
- const runQuery = useCallback((datasourceName: string = 'default', timeRange: string = '1h') => {
- setLoading(true);
- setQueryStatus('precise');
- const t0 = performance.now();
-
- // 模拟异步查询 // Logan.Li
- setTimeout(() => {
- const entries = generateMockEntries(engine, 120);
- const elapsed = Math.round(performance.now() - t0);
- setAllEntries(entries);
- setTotalCount(entries.length * 14); // 模拟更多总数
- setQueryTime(elapsed);
- setPage(1);
- setLoading(false);
-
- // 保存到历史 // Logan.Li
- historyStorage.add({ engine, datasourceName, query, timeRange, resultCount: entries.length });
- }, 600 + Math.random() * 800);
- }, [engine, query]);
-
- const toggleField = useCallback((name: string) => {
- setSelectedFields(prev =>
- prev.includes(name) ? prev.filter(f => f !== name) : [...prev, name]
- );
- }, []);
-
- return {
- // 查询状态
- query, setQuery,
- allEntries, setAllEntries,
- loading, totalCount, queryTime, queryStatus,
- // 视图
- viewMode, setViewMode,
- wrapLines, setWrapLines,
- truncate, setTruncate,
- selectedFields, setSelectedFields, toggleField,
- // 分页
- page, setPage,
- perPage, setPerPage,
- sortDirection, setSortDirection,
- totalPages,
- pagedEntries,
- sortedEntries,
- // 派生数据
- histogramData,
- fields,
- // 对话框
- alertDialog, setAlertDialog,
- historyPanel, setHistoryPanel,
- // 操作
- runQuery,
- };
-}