Files
aiops/frontend/src/components/monitor/log-center/query-autocomplete.ts
T
Logan 7e1c8f583c feat: 工具配置增强、站点拨测与日志中心多项改进
- 工具配置:新增卸载 API/UI、Go 工具链、流水线构建自动安装依赖
- 修复 ListTools VerifyTool 递归栈溢出导致获取工具配置失败
- Node.js Alpine 安装改用 unofficial-builds musl 预编译包与 index.json 解析 LTS
- 站点拨测:详情页路由、告警自动恢复、并发拨测与默认可观测频率
- 日志中心:RBAC、查询构建器、ES/Loki 增强与审计 request/response 展示
- 发布配置:构建节点、流水线远程执行、Deploy RBAC 导航拆分
2026-07-02 07:47:33 +08:00

114 lines
3.5 KiB
TypeScript

// 查询输入自动补全上下文检测 // Logan.Li
import { COMMON_ES_FIELDS, ES_KEYWORD_SUGGESTIONS, LOKI_PIPE_SUGGESTIONS } from './query-syntax';
export interface AutocompleteContext {
prefix: string;
replaceStart: number;
replaceEnd: number;
kind: 'label' | 'value' | 'pipe' | 'field' | 'keyword';
labelName?: string;
}
const LOKI_SELECTOR_RE = /\{([^}]*)\}/g;
export function detectAutocompleteContext(
query: string,
cursor: number,
engine: 'loki' | 'elasticsearch',
): AutocompleteContext | null {
const before = query.slice(0, cursor);
if (engine === 'loki') {
if (before.endsWith('|') || before.endsWith('| ')) {
return { prefix: '', replaceStart: cursor, replaceEnd: cursor, kind: 'pipe' };
}
const selectorMatch = [...query.matchAll(LOKI_SELECTOR_RE)].find((m) => {
const start = m.index ?? 0;
const end = start + m[0].length;
return cursor >= start && cursor <= end;
});
if (!selectorMatch) return null;
const inner = selectorMatch[1];
const innerStart = (selectorMatch.index ?? 0) + 1;
const rel = cursor - innerStart;
const innerBefore = inner.slice(0, rel);
const valueMatch = innerBefore.match(/([a-zA-Z_][\w.-]*)\s*(=~|!~|=|!=)\s*"?([^"]*)$/);
if (valueMatch) {
const labelName = valueMatch[1];
const valStart = innerStart + innerBefore.length - (valueMatch[3]?.length || 0);
return {
prefix: valueMatch[3] || '',
replaceStart: valStart,
replaceEnd: cursor,
kind: 'value',
labelName,
};
}
const labelMatch = innerBefore.match(/(?:^|,)\s*([a-zA-Z_][\w.-]*)?$/);
if (labelMatch) {
const partial = labelMatch[1] || '';
return {
prefix: partial,
replaceStart: cursor - partial.length,
replaceEnd: cursor,
kind: 'label',
};
}
return null;
}
const fieldMatch = before.match(/(?:^|\s)([a-zA-Z_@][\w.-]*)$/);
if (fieldMatch) {
const partial = fieldMatch[1];
return {
prefix: partial,
replaceStart: cursor - partial.length,
replaceEnd: cursor,
kind: 'field',
};
}
if (/\b(AND|OR|NOT)\s*$/i.test(before) || before.endsWith(' ')) {
return { prefix: '', replaceStart: cursor, replaceEnd: cursor, kind: 'keyword' };
}
return null;
}
export function filterSuggestions(
ctx: AutocompleteContext,
labels: string[],
labelValues: string[],
mappingFields: string[],
): string[] {
const p = ctx.prefix.toLowerCase();
const match = (items: string[]) => items.filter((s) => s.toLowerCase().startsWith(p)).slice(0, 20);
switch (ctx.kind) {
case 'label':
return match(labels);
case 'value':
return match(labelValues);
case 'pipe':
return LOKI_PIPE_SUGGESTIONS;
case 'field':
return match([...new Set([...mappingFields, ...COMMON_ES_FIELDS])]);
case 'keyword':
return ES_KEYWORD_SUGGESTIONS;
default:
return [];
}
}
export function applySuggestion(query: string, ctx: AutocompleteContext, suggestion: string): { text: string; cursor: number } {
const left = query.slice(0, ctx.replaceStart);
const right = query.slice(ctx.replaceEnd);
let insert = suggestion;
if (ctx.kind === 'value') {
insert = suggestion.includes(' ') ? `"${suggestion}"` : suggestion;
} else if (ctx.kind === 'label') {
insert = `${suggestion}=`;
} else if (ctx.kind === 'field') {
insert = `${suggestion}:`;
}
const text = left + insert + right;
return { text, cursor: left.length + insert.length };
}