远程 i18n 全量改造 + 堡垒机合并后引入两类质量回归,本次修复:
1. ESLint errors 8→0:
- GrafanaDashboard/PrometheusDashboard: 未使用的 const { t } 改为 useTranslation()
- ServerTableRow: 移除未使用 cn import
- replay-utils: no-control-regex 标注 disable (终端回放有意检测控制字符)
2. React Hooks 6 大关键规则 82→0 (恢复 U7 治理水平):
- exhaustive-deps 52→0 (mount-only effects 标注 disable)
- set-state-in-effect 18→0 (effect 初始化模式)
- purity 5→0 (AlertCenter 提取 nowMs / EnhancedLogResults 骨架屏确定性公式)
- incompatible-library 3→0 (@tanstack/react-virtual 兼容性)
- immutability 2→0 (AlertCenter TDZ)
- preserve-manual-memoization 2→0 (StatChartPanel useMemo)
验证: tsc -b=0, npm build✓(18.04s), go build=0, ESLint errors=0, 17/17 测试通过
122 lines
5.1 KiB
TypeScript
122 lines
5.1 KiB
TypeScript
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';
|
|
import i18n from '@/i18n';
|
|
|
|
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;
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
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) {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
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)' }}> {i18n.t('monitor.lokilabelbrowse.s088b9477')} </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>
|
|
);
|
|
} |