feat(monitor): F6 日志告警引擎 — AlertEngine 支持 type=log 规则查询Loki/ES+触发/恢复 + 数据源认证升级(none/basic/bearer/custom) + 从查询创建告警真实API对接
This commit is contained in:
1 parent
894cb16a05
commit
6bba6144f3
17 files changed
+657
-74
No files matched your search
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { alertApi } from '@/api';
|
||||
import { logCenterApi, type LogDataSource } from '@/api/monitor/log-center';
|
||||
import { parseChannels, getChannelIcon, getChannelTypeLabel } from './alertShared';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
@@ -25,6 +26,7 @@ const emptyRuleForm = {
|
||||
expression: '',
|
||||
severity: 'warning',
|
||||
duration: 300,
|
||||
datasource_id: undefined as number | undefined,
|
||||
notify_channels: [] as string[],
|
||||
};
|
||||
|
||||
@@ -46,6 +48,18 @@ export function AlertRuleDialog({
|
||||
}) {
|
||||
const [ruleForm, setRuleForm] = useState({ ...emptyRuleForm });
|
||||
const [ruleSaving, setRuleSaving] = useState(false);
|
||||
const [logSources, setLogSources] = useState<LogDataSource[]>([]);
|
||||
|
||||
// 日志规则打开时加载对应类型的数据源列表 // Logan.Li
|
||||
useEffect(() => {
|
||||
if (!open || ruleForm.type !== 'log') {
|
||||
setLogSources([]);
|
||||
return;
|
||||
}
|
||||
logCenterApi.listDatasources(ruleForm.source).then((res) => {
|
||||
setLogSources(res?.list || []);
|
||||
}).catch(() => setLogSources([]));
|
||||
}, [open, ruleForm.type, ruleForm.source]);
|
||||
|
||||
// 打开时同步表单(编辑时填充,新建时重置)
|
||||
useEffect(() => {
|
||||
@@ -59,6 +73,7 @@ export function AlertRuleDialog({
|
||||
expression: editingRule.expression || '',
|
||||
severity: editingRule.severity || 'warning',
|
||||
duration: editingRule.duration || 300,
|
||||
datasource_id: editingRule.datasource_id,
|
||||
notify_channels: parseChannels(editingRule.notify_channels),
|
||||
});
|
||||
} else {
|
||||
@@ -91,6 +106,7 @@ export function AlertRuleDialog({
|
||||
expression: ruleForm.expression.trim(),
|
||||
severity: ruleForm.severity,
|
||||
duration: Number(ruleForm.duration) || 0,
|
||||
datasource_id: ruleForm.datasource_id,
|
||||
notify_channels: ruleForm.notify_channels,
|
||||
};
|
||||
if (editingRule) {
|
||||
@@ -162,7 +178,7 @@ export function AlertRuleDialog({
|
||||
<Label className="text-slate-300">数据源</Label>
|
||||
<select
|
||||
value={ruleForm.source}
|
||||
onChange={(e) => setRuleForm({ ...ruleForm, source: e.target.value })}
|
||||
onChange={(e) => setRuleForm({ ...ruleForm, source: e.target.value, datasource_id: undefined })}
|
||||
className="w-full h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
|
||||
>
|
||||
{ruleForm.type === 'log' ? (
|
||||
@@ -181,6 +197,25 @@ export function AlertRuleDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ruleForm.type === 'log' && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-slate-300">日志数据源 <span className="text-red-400">*</span></Label>
|
||||
<select
|
||||
value={ruleForm.datasource_id || ''}
|
||||
onChange={(e) => setRuleForm({ ...ruleForm, datasource_id: Number(e.target.value) || undefined })}
|
||||
className="w-full h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
|
||||
>
|
||||
<option value="">请选择数据源</option>
|
||||
{logSources.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.name}{s.is_default ? ' (默认)' : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
{logSources.length === 0 && (
|
||||
<p className="text-xs text-amber-400/80">请先前往「日志库」添加 {ruleForm.source === 'loki' ? 'Loki' : 'Elasticsearch'} 数据源</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-slate-300">触发表达式 <span className="text-red-400">*</span></Label>
|
||||
<Input
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// LogAlertFromQueryDialog.tsx — 从查询创建告警规则 // Logan.Li
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, BellRing, Loader2, Flame, Database, Cloud } from 'lucide-react';
|
||||
import { alertApi } from '@/api';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type { LogEngine } from './types-v3';
|
||||
|
||||
interface LogQueryContext {
|
||||
@@ -26,6 +28,7 @@ const ENGINE_META: Record<LogEngine, { name: string; Icon: typeof Flame; color:
|
||||
};
|
||||
|
||||
export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Props) {
|
||||
const { toast } = useToast();
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [expression, setExpression] = useState('');
|
||||
@@ -54,15 +57,30 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
|
||||
const meta = ENGINE_META[ctx.engine];
|
||||
const { Icon, color } = meta;
|
||||
|
||||
const handleSave = () => {
|
||||
if (!name.trim() || !expression.trim()) return;
|
||||
const handleSave = async () => {
|
||||
if (!name.trim() || !expression.trim() || !ctx?.datasourceId) return;
|
||||
setSaving(true);
|
||||
setTimeout(() => {
|
||||
setSaving(false);
|
||||
try {
|
||||
await alertApi.createRule({
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
type: 'log',
|
||||
source: ctx.engine === 'sls' ? 'loki' : ctx.engine,
|
||||
expression: expression.trim(),
|
||||
severity,
|
||||
duration: Number(duration) || 300,
|
||||
datasource_id: ctx.datasourceId,
|
||||
notify_channels: [],
|
||||
});
|
||||
setSaved(true);
|
||||
toast({ title: '告警规则已创建' });
|
||||
onSaved?.();
|
||||
setTimeout(() => onOpenChange(false), 800);
|
||||
}, 500);
|
||||
} catch (e: any) {
|
||||
toast({ title: '创建失败', description: e?.message || String(e), variant: 'destructive' });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -28,9 +28,11 @@ export function LogDatasourceDialog({
|
||||
const [name, setName] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [defaultIndex, setDefaultIndex] = useState('logstash-*');
|
||||
const [basicAuth, setBasicAuth] = useState(false);
|
||||
const [authType, setAuthType] = useState<LogDataSource['auth_type']>('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);
|
||||
@@ -45,38 +47,52 @@ export function LogDatasourceDialog({
|
||||
setName(editing.name);
|
||||
setUrl(editing.url);
|
||||
setDefaultIndex(editing.default_index || 'logstash-*');
|
||||
setBasicAuth(!!editing.basic_auth);
|
||||
// 兼容旧版:未设置 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.url, !!editing.basic_auth, editing.basic_auth_user || '', '');
|
||||
fetchIndices(editing);
|
||||
}
|
||||
} else {
|
||||
setName(engine === 'loki' ? 'Loki' : 'Elasticsearch');
|
||||
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-*');
|
||||
setBasicAuth(false);
|
||||
setAuthType('none');
|
||||
setUser('');
|
||||
setPass('');
|
||||
setToken('');
|
||||
setHeaders([]);
|
||||
setSkipTLS(false);
|
||||
setIsDefault(false);
|
||||
setIndices([]);
|
||||
}
|
||||
}, [open, editing, engine]);
|
||||
|
||||
const fetchIndices = async (esUrl: string, auth: boolean, authUser: string, authPass: string) => {
|
||||
if (!esUrl) return;
|
||||
const fetchIndices = async (ds: LogDataSource) => {
|
||||
if (!ds.url || ds.type !== 'elasticsearch') return;
|
||||
setLoadingIndices(true);
|
||||
try {
|
||||
const result = await logCenterApi.listIndices({
|
||||
url: esUrl,
|
||||
basic_auth: auth ? 'true' : 'false',
|
||||
basic_auth_user: authUser,
|
||||
basic_auth_pass: authPass,
|
||||
skip_tls_verify: skipTLS ? 'true' : 'false',
|
||||
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 {
|
||||
@@ -91,9 +107,12 @@ export function LogDatasourceDialog({
|
||||
name: name.trim(),
|
||||
type: engine,
|
||||
url: url.trim(),
|
||||
basic_auth: basicAuth,
|
||||
basic_auth_user: user,
|
||||
basic_auth_pass: pass,
|
||||
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,
|
||||
@@ -166,7 +185,21 @@ export function LogDatasourceDialog({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-1.5 text-[10px] text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||
onClick={() => fetchIndices(url, basicAuth, user, pass)}
|
||||
onClick={() => fetchIndices({
|
||||
id: editing?.id || 0,
|
||||
name: name.trim(),
|
||||
type: engine,
|
||||
url: url.trim(),
|
||||
auth_type: authType,
|
||||
basic_auth: authType === 'basic',
|
||||
basic_auth_user: user,
|
||||
basic_auth_pass: pass,
|
||||
token,
|
||||
headers: JSON.stringify(headers.filter(h => h.name)),
|
||||
skip_tls_verify: skipTLS,
|
||||
default_index: defaultIndex,
|
||||
is_default: isDefault,
|
||||
} as LogDataSource)}
|
||||
disabled={!url || loadingIndices}
|
||||
>
|
||||
{loadingIndices ? (
|
||||
@@ -177,34 +210,84 @@ export function LogDatasourceDialog({
|
||||
获取索引
|
||||
</Button>
|
||||
</div>
|
||||
{indices.length > 0 ? (
|
||||
<Select value={defaultIndex} onValueChange={setDefaultIndex}>
|
||||
<SelectTrigger className="mt-1 h-9 w-full font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-h-48">
|
||||
{indices.map((idx) => (
|
||||
<SelectItem key={idx} value={idx} className="font-mono text-xs">
|
||||
{idx}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input value={defaultIndex} onChange={e => setDefaultIndex(e.target.value)} placeholder="索引名或通配符,如 logstash-*" className="mt-1 h-9 font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
|
||||
<Input value={defaultIndex} onChange={e => 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 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1 max-h-24 overflow-auto">
|
||||
{indices.slice(0, 30).map((idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
onClick={() => setDefaultIndex(idx)}
|
||||
className="px-1.5 py-0.5 rounded text-[10px] font-mono border transition-colors hover:opacity-80"
|
||||
style={{ background: 'var(--dg-bg-tertiary)', borderColor: 'var(--dg-border)', color: 'var(--dg-text-secondary)' }}
|
||||
title={idx}
|
||||
>
|
||||
{idx.length > 28 ? `${idx.slice(0, 28)}...` : idx}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* 常见通配模式 // Logan.Li */}
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{['.monitoring-es-7-*', 'logstash-*', 'filebeat-*', 'metrics-*', '*-2026.06.*'].map((pattern) => (
|
||||
<button
|
||||
key={pattern}
|
||||
type="button"
|
||||
onClick={() => setDefaultIndex(pattern)}
|
||||
className="px-1.5 py-0.5 rounded text-[10px] font-mono border transition-colors hover:opacity-80"
|
||||
style={{ background: 'rgba(34,197,94,0.08)', borderColor: 'rgba(34,197,94,0.25)', color: '#22c55e' }}
|
||||
>
|
||||
{pattern}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-[var(--dg-text-muted)]">基础认证</Label>
|
||||
<Switch checked={basicAuth} onCheckedChange={setBasicAuth} />
|
||||
<div>
|
||||
<Label className="text-xs text-[var(--dg-text-muted)]">认证方式</Label>
|
||||
<Select value={authType} onValueChange={(v) => setAuthType(v as LogDataSource['auth_type'])}>
|
||||
<SelectTrigger className="mt-1 h-9 w-full bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="none" className="text-xs">无认证</SelectItem>
|
||||
<SelectItem value="basic" className="text-xs">Basic Auth</SelectItem>
|
||||
<SelectItem value="bearer" className="text-xs">Bearer Token</SelectItem>
|
||||
<SelectItem value="custom" className="text-xs">自定义 Headers</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{basicAuth && (
|
||||
{authType === 'basic' && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Input value={user} onChange={e => setUser(e.target.value)} placeholder="用户名" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
|
||||
<Input type="password" value={pass} onChange={e => setPass(e.target.value)} placeholder="密码" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
|
||||
</div>
|
||||
)}
|
||||
{authType === 'bearer' && (
|
||||
<div>
|
||||
<Input type="password" value={token} onChange={e => setToken(e.target.value)} placeholder="Bearer Token" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
|
||||
</div>
|
||||
)}
|
||||
{authType === 'custom' && (
|
||||
<div className="space-y-2">
|
||||
{headers.map((h, i) => (
|
||||
<div key={i} className="grid grid-cols-2 gap-2">
|
||||
<Input value={h.name} onChange={e => {
|
||||
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)]" />
|
||||
<Input value={h.value} onChange={e => {
|
||||
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)]" />
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs border-[var(--dg-border)]" onClick={() => setHeaders([...headers, { name: '', value: '' }])}>添加 Header</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-[var(--dg-text-muted)]">跳过 TLS 校验</Label>
|
||||
<Switch checked={skipTLS} onCheckedChange={setSkipTLS} />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SaveQueryDialog.tsx — 另存为查询 / 另存为告警 // Logan.Li
|
||||
import { useState } from 'react';
|
||||
import { X, Save, BellRing, Loader2 } from 'lucide-react';
|
||||
import { alertApi } from '@/api';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type { LogEngine, TimeRange } from './types-v3';
|
||||
import { ENGINE_META } from './types-v3';
|
||||
|
||||
@@ -63,11 +65,13 @@ interface SaveAlertProps {
|
||||
onClose: () => void;
|
||||
engine: LogEngine;
|
||||
query: string;
|
||||
datasourceId?: number;
|
||||
datasourceName?: string;
|
||||
onSave?: () => void;
|
||||
}
|
||||
|
||||
export function SaveAlertDialog({ open, onClose, engine, query, datasourceName, onSave }: SaveAlertProps) {
|
||||
export function SaveAlertDialog({ open, onClose, engine, query, datasourceId, datasourceName, onSave }: SaveAlertProps) {
|
||||
const { toast } = useToast();
|
||||
const [name, setName] = useState('');
|
||||
const [severity, setSeverity] = useState('warning');
|
||||
const [condition, setCondition] = useState('count > 0');
|
||||
@@ -78,10 +82,30 @@ export function SaveAlertDialog({ open, onClose, engine, query, datasourceName,
|
||||
|
||||
const meta = ENGINE_META[engine];
|
||||
|
||||
const handleSave = () => {
|
||||
if (!name.trim()) return;
|
||||
const handleSave = async () => {
|
||||
if (!name.trim() || !datasourceId) return;
|
||||
setSaving(true);
|
||||
setTimeout(() => { setSaving(false); setSaved(true); onSave?.(); setTimeout(() => { setSaved(false); onClose(); }, 800); }, 500);
|
||||
try {
|
||||
await alertApi.createRule({
|
||||
name: name.trim(),
|
||||
description: `从查询页创建: ${datasourceName || '默认数据源'}`,
|
||||
type: 'log',
|
||||
source: engine === 'sls' ? 'loki' : engine,
|
||||
expression: query.trim(),
|
||||
severity,
|
||||
duration: 300,
|
||||
datasource_id: datasourceId,
|
||||
notify_channels: [],
|
||||
});
|
||||
setSaved(true);
|
||||
toast({ title: '告警规则已创建' });
|
||||
onSave?.();
|
||||
setTimeout(() => { setSaved(false); onClose(); }, 800);
|
||||
} catch (e: any) {
|
||||
toast({ title: '创建失败', description: e?.message || String(e), variant: 'destructive' });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -149,6 +149,8 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
|
||||
const [query, setQuery] = useState(DEFAULT_QUERIES[effEngine]);
|
||||
const [dsId, setDsId] = useState(String(sources[0]?.id || ''));
|
||||
// ES 查询允许在面板内临时覆盖索引(默认取自数据源配置)// Logan.Li
|
||||
const [esIndex, setEsIndex] = useState('');
|
||||
// API 单次查询返回条数上限(后端限制 500),Loki 用 limit / ES 用 size // Logan.Li
|
||||
const [queryLimit, setQueryLimit] = useState(200);
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>(DEFAULT_TIME_RANGE);
|
||||
@@ -182,6 +184,11 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
}
|
||||
}, [sources]);
|
||||
|
||||
// 当前数据源变化时,同步 ES 临时索引框为数据源的默认索引 // Logan.Li
|
||||
useEffect(() => {
|
||||
setEsIndex(currentSource?.default_index || '');
|
||||
}, [currentSource]);
|
||||
|
||||
const currentSource = sources.find(s => String(s.id) === dsId);
|
||||
|
||||
const fields = useMemo(() => extractFields(entries), [entries]);
|
||||
@@ -214,6 +221,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
} else {
|
||||
result = await logCenterApi.queryElasticsearch({
|
||||
datasource_id: dsIdNum,
|
||||
index: esIndex || undefined,
|
||||
query,
|
||||
size: queryLimit,
|
||||
start: startISO,
|
||||
@@ -243,12 +251,17 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
setHasQueried(true);
|
||||
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' });
|
||||
const msg = e?.message || String(e);
|
||||
const title = msg.includes('401') || msg.includes('403') ? '认证失败'
|
||||
: msg.includes('404') ? '接口不存在'
|
||||
: msg.includes('无法连接') || msg.includes('timeout') ? '连接失败'
|
||||
: '查询失败';
|
||||
toast({ title, description: msg, variant: 'destructive' });
|
||||
setEntries([]); setTotalCount(0); setHasQueried(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [effEngine, query, currentSource, timeRange, toast, queryLimit]);
|
||||
}, [effEngine, query, currentSource, timeRange, toast, queryLimit, esIndex]);
|
||||
|
||||
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,6 +293,17 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
{sources.map(s => <option key={s.id} value={s.id}>{s.name}{s.is_default ? ' (默认)' : ''}</option>)}
|
||||
</select>
|
||||
|
||||
{effEngine === 'elasticsearch' && (
|
||||
<input
|
||||
value={esIndex}
|
||||
onChange={e => setEsIndex(e.target.value)}
|
||||
placeholder="索引通配符"
|
||||
title="临时覆盖数据源默认索引,留空使用数据源配置"
|
||||
className="h-10 px-2 text-xs rounded flex-shrink-0 font-mono"
|
||||
style={{ width: '150px', background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1.5 h-10 px-2 rounded flex-shrink-0" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)' }}>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">上限</span>
|
||||
<input
|
||||
@@ -430,7 +454,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
|
||||
{/* 对话框 */}
|
||||
<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} />
|
||||
<SaveAlertDialog open={saveAlertOpen} onClose={() => setSaveAlertOpen(false)} engine={effEngine as LogEngine} query={query} datasourceId={currentSource?.id} datasourceName={currentSource?.name} />
|
||||
<QueryHistoryPanel visible={historyPanelOpen} onClose={() => setHistoryPanelOpen(false)} onSelect={(it: QueryHistoryItem) => { setQuery(it.query); setHistoryPanelOpen(false); }} onCreateAlert={() => { setSaveAlertOpen(true); setHistoryPanelOpen(false); }} />
|
||||
<LogAlertFromQueryDialog open={alertDialogOpen} onOpenChange={setAlertDialogOpen} ctx={{ engine: effEngine as LogEngine, datasourceId: currentSource?.id, datasourceName: currentSource?.name, query, timeRange: timeRange.label }} onSaved={onRuleCreated} />
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user