feat: 工具配置增强、站点拨测与日志中心多项改进

- 工具配置:新增卸载 API/UI、Go 工具链、流水线构建自动安装依赖
- 修复 ListTools VerifyTool 递归栈溢出导致获取工具配置失败
- Node.js Alpine 安装改用 unofficial-builds musl 预编译包与 index.json 解析 LTS
- 站点拨测:详情页路由、告警自动恢复、并发拨测与默认可观测频率
- 日志中心:RBAC、查询构建器、ES/Loki 增强与审计 request/response 展示
- 发布配置:构建节点、流水线远程执行、Deploy RBAC 导航拆分
This commit is contained in:
Logan committed 2026-07-02 07:47:33 +08:00
1 parent cde7bbe540
commit 7e1c8f583c
84 files changed
+7729 -1507

No files matched your search

@@ -0,0 +1,119 @@
import { useState, useEffect, useCallback } from 'react';
import { ChevronDown, ChevronRight, Loader2, Tags } from 'lucide-react';
import { logCenterApi } from '@/api/log/log-center';
import { extractLokiSelector, mergeLokiSelector } from './query-builder';
import type { TimeRange } from './types-v3';
interface Props {
datasourceId?: number;
query: string;
timeRange: TimeRange;
onQueryChange: (q: string) => void;
}
// Loki Label 浏览器 — 标签名 → 标签值 → 注入 LogQL 选择器 // Logan.Li
export function LokiLabelBrowser({ datasourceId, query, timeRange, onQueryChange }: Props) {
const [open, setOpen] = useState(false);
const [labels, setLabels] = useState<string[]>([]);
const [activeLabel, setActiveLabel] = useState('');
const [values, setValues] = useState<string[]>([]);
const [loadingLabels, setLoadingLabels] = useState(false);
const [loadingValues, setLoadingValues] = useState(false);
const [selected, setSelected] = useState<Record<string, string>>({});
const timeParams = useCallback(() => ({
start: timeRange.start.replace(' ', 'T'),
end: timeRange.end.replace(' ', 'T'),
}), [timeRange]);
useEffect(() => {
if (!open || !datasourceId) return;
setLoadingLabels(true);
logCenterApi.listLokiLabels(datasourceId, timeParams())
.then((res) => setLabels(Array.isArray(res) ? res : []))
.catch(() => setLabels([]))
.finally(() => setLoadingLabels(false));
}, [open, datasourceId, timeParams]);
useEffect(() => {
if (!activeLabel || !datasourceId) {
setValues([]);
return;
}
setLoadingValues(true);
const selector = extractLokiSelector(query);
logCenterApi.listLokiLabelValues(datasourceId, activeLabel, {
...timeParams(),
...(selector ? { query: selector } : {}),
})
.then((res) => setValues(Array.isArray(res) ? res : []))
.catch(() => setValues([]))
.finally(() => setLoadingValues(false));
}, [activeLabel, datasourceId, query, timeParams]);
const applySelection = (label: string, value: string) => {
const next = { ...selected, [label]: value };
setSelected(next);
onQueryChange(mergeLokiSelector(query, next));
};
return (
<div className="rounded-lg border" style={{ borderColor: 'var(--dg-border)', background: 'var(--dg-bg-secondary)' }}>
<button
type="button"
onClick={() => setOpen(!open)}
className="w-full flex items-center gap-2 px-3 py-2 text-xs font-medium"
style={{ color: 'var(--dg-text-secondary)' }}
>
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
<Tags className="h-3.5 w-3.5" style={{ color: '#06b6d4' }} />
Label 浏览器
{Object.keys(selected).length > 0 && (
<span className="text-[10px] px-1.5 py-0.5 rounded" style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-muted)' }}>
已选 {Object.keys(selected).length}
</span>
)}
</button>
{open && (
<div className="flex border-t min-h-[140px]" style={{ borderColor: 'var(--dg-border)' }}>
<div className="w-36 border-r overflow-y-auto max-h-48" style={{ borderColor: 'var(--dg-border)' }}>
{loadingLabels ? (
<div className="flex justify-center py-6"><Loader2 className="h-4 w-4 animate-spin text-[var(--dg-text-muted)]" /></div>
) : labels.map((lb) => (
<button
key={lb}
type="button"
onClick={() => setActiveLabel(lb)}
className="w-full text-left px-2 py-1.5 text-[11px] font-mono truncate"
style={{
color: activeLabel === lb ? '#06b6d4' : 'var(--dg-text-secondary)',
background: activeLabel === lb ? 'var(--dg-bg-tertiary)' : 'transparent',
}}
>
{lb}
</button>
))}
</div>
<div className="flex-1 overflow-y-auto max-h-48 p-1">
{!activeLabel && <p className="text-[11px] px-2 py-4 text-center" style={{ color: 'var(--dg-text-muted)' }}>选择左侧标签</p>}
{loadingValues && <div className="flex justify-center py-6"><Loader2 className="h-4 w-4 animate-spin" /></div>}
{!loadingValues && activeLabel && values.map((v) => (
<button
key={v}
type="button"
onClick={() => applySelection(activeLabel, v)}
className="w-full text-left px-2 py-1 text-[11px] font-mono truncate rounded hover:opacity-80"
style={{
color: selected[activeLabel] === v ? '#22c55e' : 'var(--dg-text-primary)',
background: selected[activeLabel] === v ? 'rgba(34,197,94,0.1)' : 'transparent',
}}
>
{v}
</button>
))}
</div>
</div>
)}
</div>
);
}