- 工具配置:新增卸载 API/UI、Go 工具链、流水线构建自动安装依赖 - 修复 ListTools VerifyTool 递归栈溢出导致获取工具配置失败 - Node.js Alpine 安装改用 unofficial-builds musl 预编译包与 index.json 解析 LTS - 站点拨测:详情页路由、告警自动恢复、并发拨测与默认可观测频率 - 日志中心:RBAC、查询构建器、ES/Loki 增强与审计 request/response 展示 - 发布配置:构建节点、流水线远程执行、Deploy RBAC 导航拆分
114 lines
3.5 KiB
TypeScript
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 };
|
|
} |