feat(ux): U2 统一 DataTable 组件 — 封装排序/搜索/分页/导出/选择/列可见性通用表格 + 迁移 RegionManager/LoginLogsTab/LogDatasourcePage 3 个页面
This commit is contained in:
1 parent
bdf3c2daf4
commit
ba8a4b5f19
9 files changed
+1268
-504
No files matched your search
@@ -1,13 +1,10 @@
|
||||
// LogDatasourcePage — 日志库数据源管理(Loki / Elasticsearch CRUD)// Logan.Li
|
||||
// LogDatasourcePage — 日志库数据源管理(Loki / Elasticsearch CRUD)
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { DataTable, type DataTableColumn } from '@/components/ui/data-table';
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Plus, Pencil, Trash2, RefreshCw, Loader2, Star, Plug,
|
||||
Plus, Pencil, Trash2, RefreshCw, Star, Plug,
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
@@ -58,7 +55,7 @@ export function LogDatasourcePage() {
|
||||
await logCenterApi.deleteDatasource(ds.id);
|
||||
toast({ title: '已删除' });
|
||||
load();
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
toast({ title: '删除失败', description: e?.message || String(e), variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
@@ -74,7 +71,7 @@ export function LogDatasourcePage() {
|
||||
skip_tls_verify: ds.skip_tls_verify,
|
||||
});
|
||||
toast({ title: '连接成功', description: `${ds.name} 可达` });
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
toast({ title: '连接失败', description: e?.message || String(e), variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
@@ -84,7 +81,7 @@ export function LogDatasourcePage() {
|
||||
await logCenterApi.setDefaultDatasource(ds.id);
|
||||
toast({ title: '已设为默认' });
|
||||
load();
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
toast({ title: '操作失败', description: e?.message || String(e), variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
@@ -94,100 +91,80 @@ export function LogDatasourcePage() {
|
||||
? 'bg-orange-500/15 text-orange-400 border-orange-500/25'
|
||||
: 'bg-cyan-500/15 text-cyan-400 border-cyan-500/25';
|
||||
|
||||
// DataTable 列定义
|
||||
const columns: DataTableColumn<LogDataSource>[] = [
|
||||
{ key: 'name', header: '名称', sortable: true, render: (ds) => <span className="font-medium text-[var(--dg-text-primary)]">{ds.name}</span> },
|
||||
{
|
||||
key: 'type', header: '类型', sortable: true, width: 120,
|
||||
render: (ds) => <Badge className={engineBadge(ds.type)}>{engineLabel(ds.type)}</Badge>,
|
||||
},
|
||||
{
|
||||
key: 'url', header: '地址',
|
||||
render: (ds) => <span className="text-xs font-mono text-[var(--dg-text-muted)]">{ds.url}</span>,
|
||||
},
|
||||
{
|
||||
key: 'is_default', header: '默认', width: 60, align: 'center', sortable: true,
|
||||
render: (ds) => ds.is_default ? <Star className="h-4 w-4 text-amber-400 mx-auto" /> : <span className="text-xs text-[var(--dg-text-muted)]">—</span>,
|
||||
},
|
||||
{
|
||||
key: 'actions', header: '操作', align: 'right', hideable: false,
|
||||
render: (ds) => (
|
||||
<div className="flex items-center justify-end gap-0.5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||
title="测试连接" onClick={() => handleTest(ds)}>
|
||||
<Plug className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{!ds.is_default && (
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-amber-400"
|
||||
title="设为默认" onClick={() => handleSetDefault(ds)}>
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||
title="编辑" onClick={() => handleEdit(ds)}>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-red-400"
|
||||
title="删除" onClick={() => handleDelete(ds)}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">
|
||||
日志查询依赖数据源连接,平台不存储海量日志本体
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="h-9 border-[var(--dg-border)]" onClick={load}>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />刷新
|
||||
</Button>
|
||||
<Button size="sm" className="h-9" onClick={() => handleAdd('loki')}>
|
||||
<Plus className="h-4 w-4 mr-1" />Loki
|
||||
</Button>
|
||||
<Button size="sm" className="h-9" onClick={() => handleAdd('elasticsearch')}>
|
||||
<Plus className="h-4 w-4 mr-1" />Elasticsearch
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Description */}
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">
|
||||
日志查询依赖数据源连接,平台不存储海量日志本体
|
||||
</p>
|
||||
|
||||
{/* Table */}
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">类型</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">地址</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">默认</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-[var(--dg-text-muted)] py-12">
|
||||
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : sources.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-[var(--dg-text-muted)] py-12">
|
||||
暂无数据源,点击上方按钮添加
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
sources.map((ds) => (
|
||||
<TableRow key={ds.id} className="border-[var(--dg-border)]">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">
|
||||
{ds.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={engineBadge(ds.type)}>{engineLabel(ds.type)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">
|
||||
{ds.url}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{ds.is_default
|
||||
? <Star className="h-4 w-4 text-amber-400" />
|
||||
: <span className="text-xs text-[var(--dg-text-muted)]">-</span>}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||
title="测试连接" onClick={() => handleTest(ds)}>
|
||||
<Plug className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{!ds.is_default && (
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-amber-400"
|
||||
title="设为默认" onClick={() => handleSetDefault(ds)}>
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||
title="编辑" onClick={() => handleEdit(ds)}>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-red-400"
|
||||
title="删除" onClick={() => handleDelete(ds)}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* DataTable */}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={sources}
|
||||
rowKey={(ds) => ds.id}
|
||||
loading={loading}
|
||||
searchable
|
||||
searchPlaceholder="搜索数据源名称或地址..."
|
||||
searchKeys={['name', 'url']}
|
||||
sortable
|
||||
emptyText="暂无数据源,点击上方按钮添加"
|
||||
toolbarExtra={
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="h-8 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)]" onClick={load}>
|
||||
<RefreshCw className="h-3.5 w-3.5 mr-1" />刷新
|
||||
</Button>
|
||||
<Button size="sm" className="h-8" onClick={() => handleAdd('loki')}>
|
||||
<Plus className="h-4 w-4 mr-1" />Loki
|
||||
</Button>
|
||||
<Button size="sm" className="h-8" onClick={() => handleAdd('elasticsearch')}>
|
||||
<Plus className="h-4 w-4 mr-1" />Elasticsearch
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Add/Edit Dialog */}
|
||||
<LogDatasourceDialog
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
// TimeRangePicker.tsx — SLS风格时间选择器 // Logan.Li
|
||||
// TimeRangePicker.tsx — SLS风格时间选择器(相对/绝对双模式)// Logan.Li
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { ChevronDown, ChevronUp, Calendar } from 'lucide-react';
|
||||
import type { TimeRange } from './types-v3';
|
||||
|
||||
const PRESETS = [
|
||||
{ label: '1分钟', value: '1m' },
|
||||
{ label: '5分钟', value: '5m' },
|
||||
{ label: '15分钟', value: '15m' },
|
||||
{ label: '1小时', value: '1h' },
|
||||
{ label: '4小时', value: '4h' },
|
||||
{ label: '1天', value: '24h' },
|
||||
{ label: '今天', value: 'today' },
|
||||
{ label: '昨天', value: 'yesterday' },
|
||||
{ label: '前天', value: 'day_before_yesterday' },
|
||||
{ label: '1周', value: '7d' },
|
||||
{ label: '本周', value: 'this_week' },
|
||||
{ label: '上周', value: 'last_week' },
|
||||
{ label: '30天', value: '30d' },
|
||||
{ label: '本月', value: 'this_month' },
|
||||
{ label: '上月', value: 'last_month' },
|
||||
// 相对时间预设:最近N... 每次点击时计算 start = now - N // Logan.Li
|
||||
const RELATIVE_PRESETS = [
|
||||
{ label: '最近1分钟', value: '1m' },
|
||||
{ label: '最近5分钟', value: '5m' },
|
||||
{ label: '最近15分钟', value: '15m' },
|
||||
{ label: '最近1小时', value: '1h' },
|
||||
{ label: '最近4小时', value: '4h' },
|
||||
{ label: '最近1天', value: '24h' },
|
||||
{ label: '最近1周', value: '7d' },
|
||||
{ label: '最近30天', value: '30d' },
|
||||
];
|
||||
|
||||
// 绝对时间预设:固定的自然日/周/月/季/年 // Logan.Li
|
||||
const ABSOLUTE_PRESETS = [
|
||||
{ label: '今天', value: 'today' },
|
||||
{ label: '昨天', value: 'yesterday' },
|
||||
{ label: '前天', value: 'day_before_yesterday' },
|
||||
{ label: '本周', value: 'this_week' },
|
||||
{ label: '上周', value: 'last_week' },
|
||||
{ label: '本月', value: 'this_month' },
|
||||
{ label: '上月', value: 'last_month' },
|
||||
{ label: '本季度', value: 'this_quarter' },
|
||||
{ label: '本年度', value: 'this_year' },
|
||||
{ label: '自定义', value: 'custom' },
|
||||
];
|
||||
|
||||
function getPresetRange(value: string): { start: Date; end: Date; label: string } {
|
||||
@@ -31,24 +35,24 @@ function getPresetRange(value: string): { start: Date; end: Date; label: string
|
||||
let label = '';
|
||||
|
||||
switch (value) {
|
||||
case '1m': start.setMinutes(now.getMinutes() - 1); label = '1分钟'; break;
|
||||
case '5m': start.setMinutes(now.getMinutes() - 5); label = '5分钟'; break;
|
||||
case '15m': start.setMinutes(now.getMinutes() - 15); label = '15分钟'; break;
|
||||
case '1h': start.setHours(now.getHours() - 1); label = '1小时'; break;
|
||||
case '4h': start.setHours(now.getHours() - 4); label = '4小时'; break;
|
||||
case '24h': start.setDate(now.getDate() - 1); label = '1天'; break;
|
||||
case '7d': start.setDate(now.getDate() - 7); label = '1周'; break;
|
||||
case '30d': start.setDate(now.getDate() - 30); label = '30天'; break;
|
||||
case '1m': start.setMinutes(now.getMinutes() - 1); label = '最近1分钟'; break;
|
||||
case '5m': start.setMinutes(now.getMinutes() - 5); label = '最近5分钟'; break;
|
||||
case '15m': start.setMinutes(now.getMinutes() - 15); label = '最近15分钟'; break;
|
||||
case '1h': start.setHours(now.getHours() - 1); label = '最近1小时'; break;
|
||||
case '4h': start.setHours(now.getHours() - 4); label = '最近4小时'; break;
|
||||
case '24h': start.setDate(now.getDate() - 1); label = '最近1天'; break;
|
||||
case '7d': start.setDate(now.getDate() - 7); label = '最近1周'; break;
|
||||
case '30d': start.setDate(now.getDate() - 30); label = '最近30天'; break;
|
||||
case 'today': start.setHours(0, 0, 0, 0); label = '今天'; break;
|
||||
case 'yesterday': start.setDate(now.getDate() - 1); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - 1); end.setHours(23, 59, 59, 999); label = '昨天'; break;
|
||||
case 'day_before_yesterday': start.setDate(now.getDate() - 2); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - 2); end.setHours(23, 59, 59, 999); label = '前天'; break;
|
||||
case 'this_week': { const d = now.getDay(); start.setDate(now.getDate() - d); start.setHours(0, 0, 0, 0); label = '本周'; break; }
|
||||
case 'last_week': { const d = now.getDay(); start.setDate(now.getDate() - d - 7); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - d); end.setHours(0, 0, 0, 0); label = '上周'; break; }
|
||||
case 'this_week': { const d = now.getDay(); const sun = d === 0 ? 0 : d; start.setDate(now.getDate() - sun); start.setHours(0, 0, 0, 0); label = '本周'; break; }
|
||||
case 'last_week': { const d = now.getDay(); const sun = d === 0 ? 0 : d; start.setDate(now.getDate() - sun - 7); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - sun); end.setHours(0, 0, 0, 0); label = '上周'; break; }
|
||||
case 'this_month': start.setDate(1); start.setHours(0, 0, 0, 0); label = '本月'; break;
|
||||
case 'last_month': start.setMonth(now.getMonth() - 1); start.setDate(1); start.setHours(0, 0, 0, 0); end.setMonth(now.getMonth()); end.setDate(1); end.setHours(0, 0, 0, 0); label = '上月'; break;
|
||||
case 'this_quarter': { const q = Math.floor(now.getMonth() / 3); start.setMonth(q * 3); start.setDate(1); start.setHours(0, 0, 0, 0); label = '本季度'; break; }
|
||||
case 'this_year': start.setMonth(0); start.setDate(1); start.setHours(0, 0, 0, 0); label = '本年度'; break;
|
||||
default: start.setHours(now.getHours() - 1); label = '1小时';
|
||||
default: start.setMinutes(now.getMinutes() - 5); label = '最近5分钟';
|
||||
}
|
||||
return { start, end, label };
|
||||
}
|
||||
@@ -65,7 +69,10 @@ interface Props {
|
||||
export function TimeRangePicker({ value, onChange }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activePreset, setActivePreset] = useState('5m');
|
||||
const [roundTime, setRoundTime] = useState(false);
|
||||
const [mode, setMode] = useState<'preset' | 'custom'>('preset');
|
||||
// 自定义日历模式 // Logan.Li
|
||||
const [customStart, setCustomStart] = useState(value.start);
|
||||
const [customEnd, setCustomEnd] = useState(value.end);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -76,21 +83,45 @@ export function TimeRangePicker({ value, onChange }: Props) {
|
||||
|
||||
const handlePreset = (presetValue: string) => {
|
||||
setActivePreset(presetValue);
|
||||
if (presetValue === 'custom') return;
|
||||
setMode('preset');
|
||||
const { start, end, label } = getPresetRange(presetValue);
|
||||
onChange({ start: formatDateTime(start), end: formatDateTime(end), label, type: 'relative' });
|
||||
const isRelative = ['1m','5m','15m','1h','4h','24h','7d','30d'].includes(presetValue);
|
||||
onChange({ start: formatDateTime(start), end: formatDateTime(end), label, type: isRelative ? 'relative' : 'absolute' });
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleCustomApply = () => {
|
||||
onChange({
|
||||
start: customStart,
|
||||
end: customEnd,
|
||||
label: '自定义',
|
||||
type: 'absolute',
|
||||
});
|
||||
setActivePreset('custom');
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const openCustom = () => {
|
||||
setMode('custom');
|
||||
setCustomStart(value.start);
|
||||
setCustomEnd(value.end);
|
||||
};
|
||||
|
||||
const isRelative = value.type === 'relative';
|
||||
const labelColor = isRelative ? '#22c55e' : 'var(--dg-blue)';
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
{/* 触发按钮 */}
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center gap-2 h-9 px-3 rounded border text-xs transition-colors"
|
||||
className="flex items-center gap-2 h-9 px-3 rounded border text-xs transition-colors flex-shrink-0"
|
||||
style={{ background: 'var(--dg-bg-tertiary)', borderColor: 'var(--dg-border)', color: 'var(--dg-text-primary)' }}
|
||||
>
|
||||
<span className="font-medium" style={{ color: 'var(--dg-blue)' }}>{value.label}</span>
|
||||
<span className="font-medium flex items-center gap-1" style={{ color: labelColor }}>
|
||||
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: labelColor }} />
|
||||
{value.label}
|
||||
</span>
|
||||
<span style={{ color: 'var(--dg-text-secondary)' }}>{value.start} ~ {value.end}</span>
|
||||
{open ? <ChevronUp className="h-3.5 w-3.5" style={{ color: 'var(--dg-text-muted)' }} /> : <ChevronDown className="h-3.5 w-3.5" style={{ color: 'var(--dg-text-muted)' }} />}
|
||||
</button>
|
||||
@@ -99,41 +130,96 @@ export function TimeRangePicker({ value, onChange }: Props) {
|
||||
{open && (
|
||||
<div
|
||||
className="absolute top-full left-0 mt-1 z-50 rounded-lg border shadow-xl p-4"
|
||||
style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)', width: '480px' }}
|
||||
style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)', width: '520px' }}
|
||||
>
|
||||
{/* 头部:时间范围显示 */}
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center gap-2 mb-3 pb-3" style={{ borderBottom: '1px solid var(--dg-border)' }}>
|
||||
<Calendar className="h-4 w-4" style={{ color: 'var(--dg-blue)' }} />
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}>时间选择</span>
|
||||
<a className="text-xs ml-2" style={{ color: 'var(--dg-blue)' }}>日历中选择</a>
|
||||
<button
|
||||
onClick={openCustom}
|
||||
className="text-xs ml-2 px-2 py-0.5 rounded transition-colors"
|
||||
style={{
|
||||
color: mode === 'custom' ? '#fff' : 'var(--dg-blue)',
|
||||
background: mode === 'custom' ? 'var(--dg-blue)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
日历中选择
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 当前范围 */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xs" style={{ color: 'var(--dg-text-secondary)' }}>{value.start} ~ {value.end}</span>
|
||||
<label className="flex items-center gap-1 text-[10px] ml-auto" style={{ color: 'var(--dg-text-muted)' }}>
|
||||
<input type="checkbox" checked={roundTime} onChange={e => setRoundTime(e.target.checked)} className="h-3 w-3 rounded" />
|
||||
整点时间
|
||||
</label>
|
||||
</div>
|
||||
{mode === 'preset' ? (
|
||||
<>
|
||||
{/* 相对时间 — 最近... */}
|
||||
<p className="text-[10px] mb-2 font-medium uppercase tracking-wide" style={{ color: '#22c55e' }}>相对时间(滚动窗口)</p>
|
||||
<div className="grid grid-cols-4 gap-1.5 mb-3">
|
||||
{RELATIVE_PRESETS.map(p => (
|
||||
<button
|
||||
key={p.value}
|
||||
onClick={() => handlePreset(p.value)}
|
||||
className="py-1.5 px-1 rounded text-[11px] transition-colors"
|
||||
style={{
|
||||
background: activePreset === p.value ? '#22c55e' : 'var(--dg-bg-tertiary)',
|
||||
color: activePreset === p.value ? '#fff' : 'var(--dg-text-secondary)',
|
||||
border: activePreset === p.value ? 'none' : '1px solid var(--dg-border)',
|
||||
}}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 预设按钮网格 */}
|
||||
<div className="grid grid-cols-5 gap-1.5">
|
||||
{PRESETS.map(p => (
|
||||
<button
|
||||
key={p.value}
|
||||
onClick={() => handlePreset(p.value)}
|
||||
className="py-1.5 px-1 rounded text-[11px] transition-colors"
|
||||
style={{
|
||||
background: activePreset === p.value ? 'var(--dg-blue)' : 'var(--dg-bg-tertiary)',
|
||||
color: activePreset === p.value ? '#fff' : 'var(--dg-text-secondary)',
|
||||
border: activePreset === p.value ? 'none' : '1px solid var(--dg-border)',
|
||||
}}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* 绝对时间 — 固定日历范围 */}
|
||||
<p className="text-[10px] mb-2 font-medium uppercase tracking-wide" style={{ color: 'var(--dg-blue)' }}>绝对时间(日历范围)</p>
|
||||
<div className="grid grid-cols-5 gap-1.5">
|
||||
{ABSOLUTE_PRESETS.map(p => (
|
||||
<button
|
||||
key={p.value}
|
||||
onClick={() => handlePreset(p.value)}
|
||||
className="py-1.5 px-1 rounded text-[11px] transition-colors"
|
||||
style={{
|
||||
background: activePreset === p.value ? 'var(--dg-blue)' : 'var(--dg-bg-tertiary)',
|
||||
color: activePreset === p.value ? '#fff' : 'var(--dg-text-secondary)',
|
||||
border: activePreset === p.value ? 'none' : '1px solid var(--dg-border)',
|
||||
}}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* 自定义日历模式 */
|
||||
<div className="space-y-3">
|
||||
<p className="text-[10px] font-medium uppercase" style={{ color: 'var(--dg-blue)' }}>自定义时间范围</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>开始时间</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={customStart.substring(0, 16)}
|
||||
onChange={e => setCustomStart(e.target.value + ':00')}
|
||||
className="w-full h-9 px-2 text-xs rounded"
|
||||
style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>结束时间</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={customEnd.substring(0, 16)}
|
||||
onChange={e => setCustomEnd(e.target.value + ':00')}
|
||||
className="w-full h-9 px-2 text-xs rounded"
|
||||
style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setMode('preset')} className="h-8 px-3 rounded text-xs" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-secondary)', background: 'var(--dg-bg-tertiary)' }}>取消</button>
|
||||
<button onClick={handleCustomApply} className="h-8 px-4 rounded text-xs font-medium text-white" style={{ background: 'var(--dg-blue)' }}>应用</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// UnifiedQueryPanel.tsx — 统一查询面板(查询体验优化版)// Logan.Li
|
||||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { Play, Loader2, ExternalLink, Clock, Save, ChevronDown, Bell } from 'lucide-react';
|
||||
// UnifiedQueryPanel.tsx — 统一查询面板(接入真实 API)// Logan.Li
|
||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import { Play, Loader2, Clock, Save, ChevronDown, Bell } from 'lucide-react';
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { TimeRangePicker } from './TimeRangePicker';
|
||||
import { LogFieldSidebar } from './LogFieldSidebar';
|
||||
@@ -11,27 +11,23 @@ import { LogClusterPanel } from './LogClusterPanel';
|
||||
import { SaveQueryDialog, SaveAlertDialog } from './SaveQueryDialog';
|
||||
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 { useToast } from '@/hooks/use-toast';
|
||||
|
||||
// 默认最近5分钟 // Logan.Li
|
||||
const DEFAULT_TIME_RANGE: TimeRange = {
|
||||
start: new Date(Date.now() - 5 * 60000).toISOString().replace('T', ' ').substring(0, 19),
|
||||
end: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
label: '5分钟',
|
||||
label: '最近5分钟',
|
||||
type: 'relative',
|
||||
};
|
||||
|
||||
// Mock 数据源(Loki & ES 双引擎) // Logan.Li
|
||||
const MOCK_SOURCES: Record<'loki' | 'elasticsearch', { id: number; name: string; url: string }[]> = {
|
||||
loki: [
|
||||
{ id: 1, name: 'prod-loki', url: 'http://loki.prod.local:3100' },
|
||||
{ id: 2, name: 'dev-loki', url: 'http://loki.dev.local:3100' },
|
||||
],
|
||||
elasticsearch: [
|
||||
{ id: 3, name: 'prod-es', url: 'http://es.prod.local:9200' },
|
||||
{ id: 4, name: 'dev-es', url: 'http://es.dev.local:9200' },
|
||||
],
|
||||
// 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
|
||||
@@ -87,19 +83,19 @@ function highlightSyntax(query: string, engine: 'loki' | 'elasticsearch'): strin
|
||||
if (!query) return '<span style="color:transparent">.</span>';
|
||||
if (engine === 'loki') {
|
||||
return query
|
||||
.replace(/([a-zA-Z_][\w-]*)(?==)/g, '<s c="#f59e0b">$1</s>') // 标签名
|
||||
.replace(/("(?:[^"\\]|\\.)*")/g, '<s c="#22c55e">$1</s>') // 字符串
|
||||
.replace(/(\|=|\!=|=~|!~|=)/g, '<s c="#f43f5e">$1</s>') // 操作符
|
||||
.replace(/(\|json|\|regexp|\|line_format|\|unwrap|\|pattern|\|label_format)/g, '<s c="#8b5cf6">$1</s>') // 管道
|
||||
.replace(/\b(count_over_time|sum|rate|avg|max|min|count|by|over)\b/g, '<s c="#06b6d4">$1</s>') // 函数
|
||||
.replace(/(\{|\})/g, '<s c="#f59e0b">$1</s>') // 花括号
|
||||
.replace(/<s c=/g, '<span style="color=') + '<span style="color:transparent">.</span>'; // 闭合
|
||||
.replace(/([a-zA-Z_][\w-]*)(?==)/g, '<s c="#f59e0b">$1</s>')
|
||||
.replace(/("(?:[^"\\]|\\.)*")/g, '<s c="#22c55e">$1</s>')
|
||||
.replace(/(\|=|\!=|=~|!~|=)/g, '<s c="#f43f5e">$1</s>')
|
||||
.replace(/(\|json|\|regexp|\|line_format|\|unwrap|\|pattern|\|label_format)/g, '<s c="#8b5cf6">$1</s>')
|
||||
.replace(/\b(count_over_time|sum|rate|avg|max|min|count|by|over)\b/g, '<s c="#06b6d4">$1</s>')
|
||||
.replace(/(\{|\})/g, '<s c="#f59e0b">$1</s>')
|
||||
.replace(/<s c=/g, '<span style="color=') + '<span style="color:transparent">.</span>';
|
||||
}
|
||||
return query
|
||||
.replace(/\b(AND|OR|NOT)\b/g, '<s c="#f43f5e">$1</s>') // 布尔
|
||||
.replace(/([a-zA-Z_@][\w.-]*):/g, '<s c="#f59e0b">$1</s>:') // 字段
|
||||
.replace(/("(?:[^"\\]|\\.)*")/g, '<s c="#22c55e">$1</s>') // 字符串
|
||||
.replace(/(\*|\?|\~|\^|\[|\]|\(|\))/g, '<s c="#8b5cf6">$1</s>') // 通配符
|
||||
.replace(/\b(AND|OR|NOT)\b/g, '<s c="#f43f5e">$1</s>')
|
||||
.replace(/([a-zA-Z_@][\w.-]*):/g, '<s c="#f59e0b">$1</s>:')
|
||||
.replace(/("(?:[^"\\]|\\.)*")/g, '<s c="#22c55e">$1</s>')
|
||||
.replace(/(\*|\?|\~|\^|\[|\]|\(|\))/g, '<s c="#8b5cf6">$1</s>')
|
||||
.replace(/<s c=/g, '<span style="color=') + '<span style="color:transparent">.</span>';
|
||||
}
|
||||
|
||||
@@ -131,14 +127,23 @@ function SyntaxHighlightInput({ value, onChange, engine, placeholder }: {
|
||||
interface Props { engine: LogEngine; onRuleCreated: () => void; }
|
||||
|
||||
export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
// SLS 回退到 Loki,仅支持双引擎 // Logan.Li
|
||||
const { toast } = useToast();
|
||||
const effEngine: 'loki' | 'elasticsearch' = engine === 'sls' ? 'loki' : engine;
|
||||
const meta = ENGINE_META[effEngine];
|
||||
const badge = ENGINE_BADGE[effEngine];
|
||||
const sources = MOCK_SOURCES[effEngine];
|
||||
|
||||
// 从后端加载真实数据源,无数据时用 demo 兜底 // Logan.Li
|
||||
const [sources, setSources] = useState<LogDataSource[]>(DEMO_SOURCES[effEngine]);
|
||||
useEffect(() => {
|
||||
logCenterApi.listDatasources(effEngine).then((res: any) => {
|
||||
const list: LogDataSource[] = Array.isArray(res) ? res : (res?.list || []);
|
||||
if (list.length > 0) setSources(list);
|
||||
else setSources(DEMO_SOURCES[effEngine]);
|
||||
}).catch(() => setSources(DEMO_SOURCES[effEngine]));
|
||||
}, [effEngine]);
|
||||
|
||||
const [query, setQuery] = useState(DEFAULT_QUERIES[effEngine]);
|
||||
const [dsId, setDsId] = useState(String(sources[0].id));
|
||||
const [dsId, setDsId] = useState(String(sources[0]?.id || ''));
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>(DEFAULT_TIME_RANGE);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [entries, setEntries] = useState<LogEntry[]>([]);
|
||||
@@ -161,30 +166,94 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
const [alertDialogOpen, setAlertDialogOpen] = useState(false);
|
||||
const [saveMenuOpen, setSaveMenuOpen] = useState(false);
|
||||
|
||||
// 数据源切换后更新 dsId // Logan.Li
|
||||
useEffect(() => {
|
||||
if (sources.length > 0) setDsId(String(sources[0].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]);
|
||||
const sortedEntries = useMemo(() => { const a = [...entries]; return sortDirection === 'asc' ? a.sort((x, y) => x.timestamp.localeCompare(y.timestamp)) : a.sort((x, y) => y.timestamp.localeCompare(x.timestamp)); }, [entries, sortDirection]);
|
||||
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]);
|
||||
|
||||
// 执行查询 // Logan.Li
|
||||
const handleRun = useCallback(() => {
|
||||
// 执行查询 — 接入真实 API(demo 源则使用 mock)// Logan.Li
|
||||
const handleRun = useCallback(async () => {
|
||||
if (!currentSource) return;
|
||||
const dsIdNum = currentSource.id;
|
||||
setLoading(true);
|
||||
const t0 = performance.now();
|
||||
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);
|
||||
historyStorage.add({ engine: effEngine as LogEngine, datasourceName: currentSource?.name || 'default', query, timeRange: timeRange.label, resultCount: mock.length });
|
||||
}, 600 + Math.random() * 500);
|
||||
}, [effEngine, query, currentSource, timeRange]);
|
||||
|
||||
// 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);
|
||||
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');
|
||||
let result: any;
|
||||
|
||||
if (effEngine === 'loki') {
|
||||
result = await logCenterApi.queryLoki({
|
||||
datasource_id: dsIdNum,
|
||||
query,
|
||||
limit: 200,
|
||||
start: startISO,
|
||||
end: endISO,
|
||||
direction: 'backward',
|
||||
});
|
||||
} else {
|
||||
result = await logCenterApi.queryElasticsearch({
|
||||
datasource_id: dsIdNum,
|
||||
query,
|
||||
size: 200,
|
||||
start: startISO,
|
||||
end: endISO,
|
||||
});
|
||||
}
|
||||
|
||||
const data = (result as any)?.data || result;
|
||||
const rawEntries: any[] = data?.entries || [];
|
||||
const mapped: LogEntry[] = rawEntries.map((e: any) => ({
|
||||
timestamp: e.timestamp || '',
|
||||
level: e.level || '',
|
||||
message: e.message || '',
|
||||
labels: e.labels || {},
|
||||
raw: e.raw || e.message || '',
|
||||
source: e.labels?.app || e.labels?.service_name || '',
|
||||
}));
|
||||
|
||||
setEntries(mapped);
|
||||
setHistogramData(generateHistogramData(mapped));
|
||||
setTotalCount(data?.total || mapped.length);
|
||||
setQueryTime(Math.round(performance.now() - t0));
|
||||
setStatTable(generateMockStatTable(effEngine as LogEngine));
|
||||
setClusters(generateMockClusters());
|
||||
setPage(1);
|
||||
historyStorage.add({ engine: effEngine as LogEngine, datasourceName: currentSource.name, query, timeRange: timeRange.label, resultCount: mapped.length });
|
||||
} catch (e: any) {
|
||||
toast({ title: '查询失败', description: e?.message || String(e), variant: 'destructive' });
|
||||
setEntries([]); setTotalCount(0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [effEngine, query, currentSource, timeRange, isDemo, 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); };
|
||||
|
||||
// 点击面积图 → 缩小时间范围到该区域 // Logan.Li
|
||||
const handleChartClick = useCallback((st: any) => {
|
||||
if (!st?.activeLabel) return;
|
||||
const idx = histogramData.findIndex(d => d.timestamp === st.activeLabel);
|
||||
@@ -195,9 +264,8 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* ===== 查询控制行:输入框前置 & 语法高亮 & 引擎徽标 ===== */}
|
||||
{/* ===== 查询控制行 ===== */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 引擎徽标 + 高亮查询输入(最醒目) */}
|
||||
<div className="flex-1 flex items-center gap-2 min-w-0">
|
||||
<div className="flex items-center gap-1.5 px-2.5 h-10 rounded flex-shrink-0" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)' }}>
|
||||
<span className="w-2 h-2 rounded-full" style={{ background: badge.dotColor }} />
|
||||
@@ -209,10 +277,10 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
<TimeRangePicker value={timeRange} onChange={setTimeRange} />
|
||||
|
||||
<select value={dsId} onChange={e => setDsId(e.target.value)} className="h-10 px-2 text-xs rounded flex-shrink-0" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}>
|
||||
{sources.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||
{sources.map(s => <option key={s.id} value={s.id}>{s.name}{isDemo ? ' (模拟)' : ''}</option>)}
|
||||
</select>
|
||||
|
||||
<button onClick={handleRun} disabled={loading} className="h-10 px-4 rounded text-xs font-medium text-white flex items-center gap-1.5 flex-shrink-0 transition-opacity hover:opacity-90" style={{ background: meta.color }}>
|
||||
<button onClick={handleRun} disabled={loading} className="h-10 px-4 rounded text-xs font-medium text-white flex items-center gap-1.5 flex-shrink-0 transition-opacity hover:opacity-90" style={{ background: '#22c55e' }}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />} 查询
|
||||
</button>
|
||||
|
||||
@@ -229,26 +297,20 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
{currentSource?.url && (
|
||||
<button onClick={() => window.open(currentSource.url, '_blank')} className="h-10 px-2.5 rounded text-xs flex items-center gap-1 flex-shrink-0" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}>
|
||||
{meta.externalLink} <ExternalLink className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button onClick={() => setHistoryPanelOpen(true)} className="h-10 w-10 flex items-center justify-center rounded flex-shrink-0" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }} title="查询历史">
|
||||
<Clock className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ===== 快捷查询助手(内联紧凑版,14+模板) ===== */}
|
||||
{/* 快捷查询助手 */}
|
||||
<QuickQueryBar engine={effEngine} templates={ALL_TEMPLATES[effEngine]} onSelect={setQuery} />
|
||||
|
||||
{/* ===== 统计条 + 工具栏 ===== */}
|
||||
{/* 统计条 + 工具栏 */}
|
||||
{entries.length > 0 && !loading && (
|
||||
<LogQueryToolbar total={totalCount} queryTime={queryTime} perPage={perPage} onPerPageChange={n => { setPerPage(n); setPage(1); }} page={page} totalPages={totalPages} onPageChange={setPage} sortDirection={sortDirection} onSortToggle={() => setSortDirection(d => d === 'desc' ? 'asc' : 'desc')} onExport={() => { const t = sortedEntries.map(e => `${e.timestamp}\t${e.level || ''}\t${e.message}`).join('\n'); const b = new Blob([t], { type: 'text/plain' }); const u = URL.createObjectURL(b); const a = document.createElement('a'); a.href = u; a.download = `${effEngine}-logs-${Date.now()}.txt`; a.click(); URL.revokeObjectURL(u); }} onCopy={() => navigator.clipboard.writeText(pagedEntries.map(e => `${e.timestamp}\t${e.level || ''}\t${e.message}`).join('\n'))} onBellClick={() => setAlertDialogOpen(true)} />
|
||||
)}
|
||||
|
||||
{/* ===== Log Volume 面积图(点击缩小时间范围) ===== */}
|
||||
{/* Log Volume 面积图 */}
|
||||
{entries.length > 0 && (
|
||||
<div className="h-[120px] rounded-lg border cursor-crosshair" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
@@ -269,7 +331,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== 结果标签页(紧凑内嵌样式) ===== */}
|
||||
{/* 结果标签页 */}
|
||||
{entries.length > 0 && (
|
||||
<div className="rounded-lg border" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
|
||||
<div className="flex items-center gap-0.5 px-2 pt-1.5" style={{ borderBottom: '1px solid var(--dg-border)' }}>
|
||||
@@ -285,7 +347,6 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="p-2">
|
||||
{/* 原始日志 */}
|
||||
{resultTab === 'logs' && (<>
|
||||
<div className="flex gap-3">
|
||||
<LogFieldSidebar fields={fields} selectedFields={selectedFields} onFieldClick={n => setQuery(p => `${p} ${n}=""`)} onToggleDisplayField={toggleField} loading={loading} />
|
||||
@@ -305,11 +366,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
<label className="flex items-center gap-1.5 text-[11px] cursor-pointer" style={{ color: 'var(--dg-text-muted)' }}><input type="checkbox" checked={wrapLines} onChange={e => setWrapLines(e.target.checked)} className="h-3.5 w-3.5 rounded" /> 换行</label>
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
{/* 统计图表 */}
|
||||
{resultTab === 'stats' && <StatChartPanel statTable={statTable} engine={effEngine as LogEngine} query={query} onSaveAsAlert={() => setSaveAlertOpen(true)} onSaveAsQuery={() => setSaveQueryOpen(true)} />}
|
||||
|
||||
{/* 日志聚类 */}
|
||||
{resultTab === 'cluster' && <LogClusterPanel clusters={clusters} />}
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,7 +380,14 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== 对话框 ===== */}
|
||||
{/* Loading */}
|
||||
{loading && entries.length === 0 && (
|
||||
<div className="border rounded-lg flex items-center justify-center" style={{ borderColor: 'var(--dg-border)', minHeight: '200px' }}>
|
||||
<Loader2 className="h-6 w-6 animate-spin" style={{ color: 'var(--dg-text-muted)' }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 对话框 */}
|
||||
<SaveQueryDialog open={saveQueryOpen} onClose={() => setSaveQueryOpen(false)} engine={effEngine as LogEngine} query={query} timeRange={timeRange} datasourceName={currentSource?.name} />
|
||||
<SaveAlertDialog open={saveAlertOpen} onClose={() => setSaveAlertOpen(false)} engine={effEngine as LogEngine} query={query} datasourceName={currentSource?.name} />
|
||||
<QueryHistoryPanel visible={historyPanelOpen} onClose={() => setHistoryPanelOpen(false)} onSelect={(it: QueryHistoryItem) => { setQuery(it.query); setHistoryPanelOpen(false); }} onCreateAlert={() => { setSaveAlertOpen(true); setHistoryPanelOpen(false); }} />
|
||||
@@ -332,7 +396,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 内联紧凑版快捷查询助手 ===== // Logan.Li
|
||||
// 内联紧凑版快捷查询助手
|
||||
function QuickQueryBar({ engine, templates, onSelect }: { engine: 'loki' | 'elasticsearch'; templates: { label: string; query: string }[]; onSelect: (q: string) => void; }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const labels: Record<'loki' | 'elasticsearch', string> = { loki: 'LogQL', elasticsearch: 'Lucene' };
|
||||
|
||||
Reference in new issue
Block a user