From 23eba136f6ed1556451aed29e9e7e98c370934f1 Mon Sep 17 00:00:00 2001 From: jsers Date: Fri, 28 Aug 2026 17:35:36 +0800 Subject: [PATCH 1/2] chore(deps): bump @fc-components/monaco-editor to 0.6.0 --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index f35f989b0..3b150ccb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "@dnd-kit/sortable": "7.0.2", "@fc-components/codemirror-promql": "^0.19.13", "@fc-components/es-query": "^0.0.3", - "@fc-components/monaco-editor": "^0.5.12", + "@fc-components/monaco-editor": "^0.6.0", "@fc-components/use-antd-resizable-header": "^2.8.16", "@fc-plot/ts-graph": "^0.27.2", "@microsoft/fetch-event-source": "^2.0.1", @@ -5096,9 +5096,9 @@ } }, "node_modules/@fc-components/monaco-editor": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/@fc-components/monaco-editor/-/monaco-editor-0.5.12.tgz", - "integrity": "sha512-+RoElV16hcD8qWFx85L/1LYYK+2EVtsZ+cOKXFM0ATKIIr1uFcLQkvESQA+3lirLdLrhzflPOhDtxjQF2pj7Gg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@fc-components/monaco-editor/-/monaco-editor-0.6.0.tgz", + "integrity": "sha512-cvoFXZRsvqU+CyraV05K57WDaOjzH6olpHtANdBjqbxhIzHQZjpSX8bRIjouvoxynS83WZO+Ekbxaydb06QMBg==", "license": "MIT", "dependencies": { "@emotion/css": "11.13.4", @@ -34243,9 +34243,9 @@ "requires": {} }, "@fc-components/monaco-editor": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/@fc-components/monaco-editor/-/monaco-editor-0.5.12.tgz", - "integrity": "sha512-+RoElV16hcD8qWFx85L/1LYYK+2EVtsZ+cOKXFM0ATKIIr1uFcLQkvESQA+3lirLdLrhzflPOhDtxjQF2pj7Gg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@fc-components/monaco-editor/-/monaco-editor-0.6.0.tgz", + "integrity": "sha512-cvoFXZRsvqU+CyraV05K57WDaOjzH6olpHtANdBjqbxhIzHQZjpSX8bRIjouvoxynS83WZO+Ekbxaydb06QMBg==", "requires": { "@emotion/css": "11.13.4", "@fc-components/lezer-metricsql": "^0.0.2", diff --git a/package.json b/package.json index 5ef7a11bf..da2ef604a 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@dnd-kit/sortable": "7.0.2", "@fc-components/codemirror-promql": "^0.19.13", "@fc-components/es-query": "^0.0.3", - "@fc-components/monaco-editor": "^0.5.12", + "@fc-components/monaco-editor": "^0.6.0", "@fc-components/use-antd-resizable-header": "^2.8.16", "@fc-plot/ts-graph": "^0.27.2", "@microsoft/fetch-event-source": "^2.0.1", From 7190c8501098ffec859222445ff1bd2cc2964109 Mon Sep 17 00:00:00 2001 From: jsers Date: Sun, 30 Aug 2026 00:31:47 +0800 Subject: [PATCH 2/2] feat(kql): migrate KQLInput to @fc-components/monaco-editor Replace the CodeMirror-based KQL editor with KQLMonacoEditor from @fc-components/monaco-editor (bumped to ^0.6.2). Reuse the existing elasticsearch completion client for field/value suggestions. Drop the now-unused CMTheme, completion, grammar, kql, style and utils modules. --- package-lock.json | 14 +- package.json | 2 +- src/components/KQLInput/CMTheme.tsx | 238 -------------------- src/components/KQLInput/complete/hybrid.ts | 248 --------------------- src/components/KQLInput/complete/index.ts | 29 --- src/components/KQLInput/grammar/parser.js | 67 ------ src/components/KQLInput/index.tsx | 244 ++++++++------------ src/components/KQLInput/kql.ts | 75 ------- src/components/KQLInput/style.less | 68 ------ src/components/KQLInput/utils.ts | 65 ------ 10 files changed, 97 insertions(+), 953 deletions(-) delete mode 100644 src/components/KQLInput/CMTheme.tsx delete mode 100644 src/components/KQLInput/complete/hybrid.ts delete mode 100644 src/components/KQLInput/complete/index.ts delete mode 100644 src/components/KQLInput/grammar/parser.js delete mode 100644 src/components/KQLInput/kql.ts delete mode 100644 src/components/KQLInput/style.less delete mode 100644 src/components/KQLInput/utils.ts diff --git a/package-lock.json b/package-lock.json index 3b150ccb4..d1c2d2edd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "@dnd-kit/sortable": "7.0.2", "@fc-components/codemirror-promql": "^0.19.13", "@fc-components/es-query": "^0.0.3", - "@fc-components/monaco-editor": "^0.6.0", + "@fc-components/monaco-editor": "^0.6.2", "@fc-components/use-antd-resizable-header": "^2.8.16", "@fc-plot/ts-graph": "^0.27.2", "@microsoft/fetch-event-source": "^2.0.1", @@ -5096,9 +5096,9 @@ } }, "node_modules/@fc-components/monaco-editor": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@fc-components/monaco-editor/-/monaco-editor-0.6.0.tgz", - "integrity": "sha512-cvoFXZRsvqU+CyraV05K57WDaOjzH6olpHtANdBjqbxhIzHQZjpSX8bRIjouvoxynS83WZO+Ekbxaydb06QMBg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@fc-components/monaco-editor/-/monaco-editor-0.6.2.tgz", + "integrity": "sha512-axpTD5y6xLxF52hhm9a/9RehpSwLzOFeE7BxtWI49FlpDHVS77DwONgt/7oOIGfj3r3mWWl1mCk/2ADWKJoQuw==", "license": "MIT", "dependencies": { "@emotion/css": "11.13.4", @@ -34243,9 +34243,9 @@ "requires": {} }, "@fc-components/monaco-editor": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@fc-components/monaco-editor/-/monaco-editor-0.6.0.tgz", - "integrity": "sha512-cvoFXZRsvqU+CyraV05K57WDaOjzH6olpHtANdBjqbxhIzHQZjpSX8bRIjouvoxynS83WZO+Ekbxaydb06QMBg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@fc-components/monaco-editor/-/monaco-editor-0.6.2.tgz", + "integrity": "sha512-axpTD5y6xLxF52hhm9a/9RehpSwLzOFeE7BxtWI49FlpDHVS77DwONgt/7oOIGfj3r3mWWl1mCk/2ADWKJoQuw==", "requires": { "@emotion/css": "11.13.4", "@fc-components/lezer-metricsql": "^0.0.2", diff --git a/package.json b/package.json index da2ef604a..8885e7ed0 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@dnd-kit/sortable": "7.0.2", "@fc-components/codemirror-promql": "^0.19.13", "@fc-components/es-query": "^0.0.3", - "@fc-components/monaco-editor": "^0.6.0", + "@fc-components/monaco-editor": "^0.6.2", "@fc-components/use-antd-resizable-header": "^2.8.16", "@fc-plot/ts-graph": "^0.27.2", "@microsoft/fetch-event-source": "^2.0.1", diff --git a/src/components/KQLInput/CMTheme.tsx b/src/components/KQLInput/CMTheme.tsx deleted file mode 100644 index 2f580b9e9..000000000 --- a/src/components/KQLInput/CMTheme.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { HighlightStyle, tags } from '@codemirror/highlight'; -import { EditorView } from '@codemirror/view'; - -export const baseTheme = EditorView.theme({ - '.cm-content': { - padding: 0, - }, - '&.cm-editor.cm-focused': { - outline: 'unset', - }, - '.cm-scroller': { - maxHeight: '100px', - fontFamily: 'Consolas,Monaco,sans-serif,PingFangSC-Regular,microsoft yahei ui,microsoft yahei,simsun,"sans-serif"', - }, - '.cm-matchingBracket': { - fontWeight: 'bold', - outline: '1px dashed transparent', - }, - '.cm-nonmatchingBracket': { borderColor: 'red' }, - - '.cm-tooltip.cm-tooltip-autocomplete': { - '& > ul': { - maxHeight: '350px', - maxWidth: 'unset', - }, - '& > ul > li': { - padding: '2px 1em 2px 3px', - overflowY: 'hidden', - }, - '& > ul > li[aria-selected]': { - color: 'unset', - }, - minWidth: '30%', - }, - - '.cm-completionDetail': { - float: 'right', - color: '#999', - }, - - '.cm-tooltip.cm-completionInfo': { - marginTop: '-11px', - padding: '10px', - border: 'none', - minWidth: '250px', - maxWidth: 'min-content', - }, - - '.cm-completionInfo.cm-completionInfo-right': { - '&:before': { - content: "' '", - height: '0', - position: 'absolute', - width: '0', - left: '-20px', - borderWidth: '10px', - borderStyle: 'solid', - borderColor: 'transparent', - }, - marginLeft: '12px', - }, - '.cm-completionInfo.cm-completionInfo-left': { - '&:before': { - content: "' '", - height: '0', - position: 'absolute', - width: '0', - right: '-20px', - borderWidth: '10px', - borderStyle: 'solid', - borderColor: 'transparent', - }, - marginRight: '12px', - }, - - '.cm-completionMatchedText': { - textDecoration: 'none', - fontWeight: 'bold', - color: '#3ba1fb', - }, - - '.cm-selectionMatch': { - backgroundColor: 'unset', - }, - - '.cm-diagnostic': { - '&.cm-diagnostic-error': { - borderLeft: '3px solid #e65013', - }, - }, - - '.cm-completionIcon': { - boxSizing: 'content-box', - fontSize: '16px', - lineHeight: '1', - marginRight: '10px', - verticalAlign: 'top', - '&:after': { content: "'\\ea88'" }, - fontFamily: 'codicon', - paddingRight: '0', - opacity: '1', - color: '#2ca9fd', - }, - - '.cm-completionIcon-function, .cm-completionIcon-method': { - '&:after': { content: "'\\ea8c'" }, - color: '#652d90', - }, - '.cm-completionIcon-class': { - '&:after': { content: "'○'" }, - }, - '.cm-completionIcon-interface': { - '&:after': { content: "'◌'" }, - }, - '.cm-completionIcon-variable': { - '&:after': { content: "'𝑥'" }, - }, - '.cm-completionIcon-constant': { - '&:after': { content: "'\\eb5f'" }, - color: '#2ca9fd', - }, - '.cm-completionIcon-type': { - '&:after': { content: "'𝑡'" }, - }, - '.cm-completionIcon-enum': { - '&:after': { content: "'∪'" }, - }, - '.cm-completionIcon-property': { - '&:after': { content: "'□'" }, - }, - '.cm-completionIcon-keyword': { - '&:after': { content: "'\\eb62'" }, - color: '#616161', - }, - '.cm-completionIcon-namespace': { - '&:after': { content: "'▢'" }, - }, - '.cm-completionIcon-text': { - '&:after': { content: "'\\ea95'" }, - color: '#ee9d28', - }, -}); - -export const lightTheme = EditorView.theme( - { - '.cm-tooltip': { - backgroundColor: '#f8f8f8', - borderColor: 'rgba(52, 79, 113, 0.2)', - }, - - '.cm-tooltip.cm-tooltip-autocomplete': { - '& li:hover': { - backgroundColor: '#ddd', - }, - '& > ul > li[aria-selected]': { - backgroundColor: '#d6ebff', - color: 'unset', - }, - }, - - '.cm-tooltip.cm-completionInfo': { - backgroundColor: '#f5f5f5', - }, - - '.cm-tooltip > .cm-completionInfo.cm-completionInfo-right': { - '&:before': { - borderRightColor: '#f5f5f5', - }, - }, - '.cm-tooltip > .cm-completionInfo.cm-completionInfo-left': { - '&:before': { - borderLeftColor: '#f5f5f5', - }, - }, - - '.cm-line': { - '&::selection': { - backgroundColor: '#add6ff', - }, - '& > span::selection': { - backgroundColor: '#add6ff', - }, - }, - }, - { dark: false }, -); - -export const darkTheme = EditorView.theme( - { - '.cm-content': { - caretColor: '#fff', - }, - - '.cm-tooltip.cm-tooltip-autocomplete': { - '& li:hover': { - backgroundColor: '#55555c', - }, - '& > ul > li[aria-selected]': { - backgroundColor: '#1d4f78', - color: 'unset', - }, - }, - - '.cm-tooltip.cm-completionInfo': { - backgroundColor: '#333338', - }, - - '.cm-tooltip > .cm-completionInfo.cm-completionInfo-right': { - '&:before': { - borderRightColor: '#333338', - }, - }, - '.cm-tooltip > .cm-completionInfo.cm-completionInfo-left': { - '&:before': { - borderLeftColor: '#333338', - }, - }, - - '.cm-line': { - '&::selection': { - backgroundColor: '#767676', - }, - '& > span::selection': { - backgroundColor: '#767676', - }, - }, - }, - { dark: true }, -); -export const highlighter = HighlightStyle.define([ - { tag: tags.number, class: 'cmt-number' }, - { tag: tags.string, class: 'cmt-string' }, - { tag: tags.keyword, class: 'cmt-keyword' }, - { tag: tags.labelName, class: 'cmt-labelName' }, - { tag: tags.invalid, class: 'cmt-invalid' }, - { tag: tags.comment, class: 'cmt-comment' }, - { tag: tags.logicOperator, class: 'cmt-logic-operator' }, -]); diff --git a/src/components/KQLInput/complete/hybrid.ts b/src/components/KQLInput/complete/hybrid.ts deleted file mode 100644 index 1bb8a746d..000000000 --- a/src/components/KQLInput/complete/hybrid.ts +++ /dev/null @@ -1,248 +0,0 @@ -import _ from 'lodash'; -import i18next from 'i18next'; -import { SyntaxNode } from 'lezer-tree'; -import { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; -import { syntaxTree } from '@codemirror/language'; -import { EditorState } from '@codemirror/state'; -import { Client } from '../client'; -import { CompleteStrategy } from '.'; -import { KQL, Expr, VectorSelector, FieldName, MatchOperator, Colon, BinaryExpr, FieldValue, StringLiteral, And, Or } from '../grammar/parser'; - -export enum ContextKind { - FieldName = 'fieldName', - MatchOperator = 'matchOperator', - FieldValue = 'fieldValue', - BinaryOperator = 'binaryOperator', -} - -export interface Context { - kind: ContextKind; - fieldName?: string; - quoted?: boolean; -} - -function arrayToCompletionResult(data: Completion[], from: number, to: number, historicalRecords): CompletionResult { - let options = data; - if (historicalRecords && to === 0) { - const snippets: Completion[] = _.map(historicalRecords, (item) => { - const snippet = item[0]; - return { - label: snippet, - type: 'snippet', - detail: i18next.t('logql:historicalRecords'), - apply: snippet, - boost: 99, - section: { - name: i18next.t('logql:historicalRecords'), - header: i18next.t('logql:historicalRecords'), - rank: 0, - }, - }; - }); - options = _.concat(snippets, options); - } - return { - from: from, - to: to, - options: options, - span: /^[a-zA-Z0-9_:]+$/, - } as CompletionResult; -} - -function computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node: SyntaxNode, pos: number): number { - let start = node.from + 1; - if (node.firstChild !== null) { - start = pos; - } - return start; -} - -export function computeStartCompletePosition(node: SyntaxNode, pos: number): number { - let start = node.from; - if (node.type.id === KQL || (node.type.id === 0 && (node.prevSibling?.type.id === And || node.prevSibling?.type.id === Or))) { - start = computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node, pos); - } else if ( - node.type.id === FieldValue || - (node.type.id === StringLiteral && node.parent?.type.id === FieldValue) || - (node.type.id === 0 && node.prevSibling?.type.id === VectorSelector) || - (node.type.id === 0 && node.prevSibling?.type.id === Expr) || - (node.type.id === 0 && node.prevSibling?.type.id === FieldName) || - (node.type.id === 0 && node.prevSibling?.type.id === MatchOperator) || - node.type.id === Colon - ) { - start++; - } - return start; -} - -export function analyzeCompletion(state: EditorState, node: SyntaxNode): Context[] { - const result: Context[] = []; - switch (node.type.id) { - case 0: - if (node.prevSibling) { - const prevSibling = node.prevSibling; - if (prevSibling.type.id === VectorSelector) { - result.push({ kind: ContextKind.BinaryOperator }); - break; - } else if (prevSibling.type.id === Expr) { - result.push({ kind: ContextKind.BinaryOperator }); - break; - } else if (prevSibling.type.id === And || prevSibling.type.id === Or) { - result.push({ kind: ContextKind.FieldName }); - break; - } else if (prevSibling.type.id === FieldName) { - result.push({ kind: ContextKind.MatchOperator }); - break; - } else if (prevSibling.type.id === MatchOperator) { - const firstChild = prevSibling.firstChild; - let prevSibling02 = prevSibling.prevSibling; - if (prevSibling02 && prevSibling02.type.id === 0) { - prevSibling02 = prevSibling02.prevSibling; - } - if (firstChild && firstChild.type.id === Colon && prevSibling02 && prevSibling02.type.id === FieldName) { - result.push({ kind: ContextKind.FieldValue, fieldName: state.sliceDoc(prevSibling02.from, prevSibling02.to), quoted: true }); - } - break; - } - } - break; - case KQL: - if (node.firstChild) { - const firstChild = node.firstChild; - if ((firstChild && firstChild.type.id === 0) || !firstChild) { - result.push({ kind: ContextKind.FieldName }); - break; - } - } - break; - case FieldName: - if (node.parent) { - const parent = node.parent; - if (parent && parent.type.id === VectorSelector) { - result.push({ kind: ContextKind.FieldName }); - break; - } - } - case Colon: - if (node.parent) { - const parent = node.parent; - if (parent && parent.type.id === MatchOperator) { - const prevSibling = parent.prevSibling; - if (prevSibling && prevSibling.type.id === FieldName) { - result.push({ kind: ContextKind.FieldValue, fieldName: state.sliceDoc(prevSibling.from, prevSibling.to), quoted: true }); - } - } - } - break; - case StringLiteral: - if (node.parent) { - const parent = node.parent; - if (parent.type.id === FieldValue) { - let prevSibling01 = parent.prevSibling; - if (prevSibling01 && prevSibling01.type.id === 0) { - prevSibling01 = prevSibling01.prevSibling; - } - if (prevSibling01 && prevSibling01.type.id === MatchOperator) { - const firstChild = prevSibling01.firstChild; - let prevSibling02 = prevSibling01.prevSibling; - if (prevSibling02 && prevSibling02.type.id === 0) { - prevSibling02 = prevSibling02.prevSibling; - } - if (firstChild && firstChild.type.id === Colon && prevSibling02 && prevSibling02.type.id === FieldName) { - result.push({ kind: ContextKind.FieldValue, fieldName: state.sliceDoc(prevSibling02.from, prevSibling02.to) }); - } - } - } - } - break; - } - return result; -} - -export class HybridComplete implements CompleteStrategy { - private readonly client: Client | undefined; - private readonly historicalRecords: [string, number][] | undefined; - - constructor(client?: Client, historicalRecords?: [string, number][]) { - this.client = client; - this.historicalRecords = historicalRecords; - } - - getClient(): Client | undefined { - return this.client; - } - - KQL(context: CompletionContext): Promise | CompletionResult | null { - const { state, pos } = context; - const tree = syntaxTree(state).resolve(pos, -1) as any; - const contexts = analyzeCompletion(state, tree); - let asyncResult: Promise = Promise.resolve([]); - for (const context of contexts) { - switch (context.kind) { - case ContextKind.FieldName: - asyncResult = asyncResult.then((result) => { - return this.autocompleteFieldName(result); - }); - break; - case ContextKind.FieldValue: - asyncResult = asyncResult.then((result) => { - if (context.fieldName) { - return this.autocompleteFieldValue(result, context.fieldName, context.quoted); - } - return result; - }); - break; - case ContextKind.BinaryOperator: - asyncResult = asyncResult.then((result) => { - return _.concat(result, [ - { label: 'AND', type: 'keyword', detail: i18next.t('kql:combiningKeyword'), boost: 0 }, - { label: 'OR', type: 'keyword', detail: i18next.t('kql:combiningKeyword'), boost: 0 }, - ]); - }); - break; - case ContextKind.MatchOperator: - asyncResult = asyncResult.then((result) => { - return _.concat(result, [ - { label: ':', type: 'keyword', detail: i18next.t('kql:matchOperator'), boost: 0 }, - { label: '<=', type: 'keyword', detail: i18next.t('kql:matchOperator'), boost: 0 }, - { label: '>=', type: 'keyword', detail: i18next.t('kql:matchOperator'), boost: 0 }, - { label: '<', type: 'keyword', detail: i18next.t('kql:matchOperator'), boost: 0 }, - { label: '>', type: 'keyword', detail: i18next.t('kql:matchOperator'), boost: 0 }, - { label: ': *', type: 'keyword', detail: i18next.t('kql:matchOperator'), boost: 0 }, - ]); - }); - break; - } - } - return asyncResult.then((result) => { - return arrayToCompletionResult(result, computeStartCompletePosition(tree, pos), pos, this.historicalRecords); - }); - } - - private autocompleteFieldName(result: Completion[]): Completion[] | Promise { - if (!this.client) { - return result; - } - return this.client.fields().then((fields: string[]) => { - return _.concat( - result, - _.map(fields, (value) => ({ label: value, type: 'constant', detail: i18next.t('kql:fieldName'), boost: 0 })), - ); - }); - } - - private autocompleteFieldValue(result: Completion[], field: string, quoted?: boolean): Completion[] | Promise { - if (!this.client) { - return result; - } - return this.client.fieldValues(field).then((fieldValues: string[]) => { - if (quoted) { - fieldValues = _.map(fieldValues, (value) => `"${value}"`); - } - return _.concat( - result, - _.map(fieldValues, (value) => ({ label: value, type: 'constant', detail: i18next.t('kql:fieldValue'), boost: 0 })), - ); - }); - } -} diff --git a/src/components/KQLInput/complete/index.ts b/src/components/KQLInput/complete/index.ts deleted file mode 100644 index 7fb91d6a5..000000000 --- a/src/components/KQLInput/complete/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CompletionContext, CompletionResult } from '@codemirror/autocomplete'; -import { HybridComplete } from './hybrid'; -import { Config } from '../client'; -import { HTTPClient as ESHTTPClient, CachedClient as ESCachedClient } from '../client/elasticsearch'; - -export interface CompleteStrategy { - KQL(context: CompletionContext): Promise | CompletionResult | null; -} - -export interface CompleteConfiguration { - historicalRecords: [string, number][]; - remote?: Config; - maxMetricsMetadata?: number; - completeStrategy?: CompleteStrategy; -} - -export function newCompleteStrategy(conf?: CompleteConfiguration): CompleteStrategy { - if (conf?.completeStrategy) { - return conf.completeStrategy; - } - if (conf?.remote) { - let client; - let cacheClient; - client = new ESHTTPClient(conf.remote); - cacheClient = new ESCachedClient(client, conf.remote.cache); - return new HybridComplete(cacheClient, conf.historicalRecords); - } - return new HybridComplete(); -} diff --git a/src/components/KQLInput/grammar/parser.js b/src/components/KQLInput/grammar/parser.js deleted file mode 100644 index e9ddfca63..000000000 --- a/src/components/KQLInput/grammar/parser.js +++ /dev/null @@ -1,67 +0,0 @@ -import { LRParser } from '@lezer/lr'; -import { NodeProp } from '@lezer/common'; - -// This file was generated by lezer-generator. You probably shouldn't edit it. -const parser = LRParser.deserialize({ - version: 13, - states: - "#`OVQPOOQ_QQOOOVQPO'#CcOgQPO'#CdOOQO'#C_'#C_OVQPO,58zOVQPO,58zOxQQO,58}OOQO'#Cf'#CfO!TQPO,59OOOQO1G.f1G.fO!cQQO1G.fOOQO1G.i1G.iOVQPO'#CpOOQO'#Cl'#ClOOQO1G.j1G.jO!tQQO,59[OOQO1G.v1G.v", - stateData: '#P~OPOS~OXROgQO~OTTOUUO~OZWO[WO]WO^WO_WO~OTTOUUOh[O~Oa^Ob^Oc^Oi]O~OTTOUSieSihSijSi~OTTOUUOjaO~O', - goto: '!VePPPfuPPuuP|PPPPP!PPPP!SQPOQVQQYTQZUR`]ZSOQTU]RXRR_XR^X', - nodeNames: - '⚠ LineComment KQL Expr BinaryExpr And Or ParenExpr VectorSelector FieldName MatchOperator Colon Gtr Gte Lss Lte FieldValue Asterisk StringLiteral NumberLiteral NestedQuery', - maxTerm: 26, - nodeProps: [[NodeProp.group, 3, 'Statement']], - skippedNodes: [0, 1], - repeatNodeCount: 0, - tokenData: - "*t~Rers!dst#Wxy#cyz#hz{#m!Q![#r![!]%T!^!_%Y!`!a%g!b!c%t!c!d&Y!d!q%t!q!r'v!r!}%t#R#S%t#T#U(x#U#c%t#c#d*O#d#o%t#o#p*j#q#r*o~!iUb~OY!dZr!drs!{s#O!d#O#P#Q#P~!d~#QOb~~#TPO~!d~#]QP~OY#WZ~#W~#hOg~~#mOh~~#rOa~~#wSc~!O!P$T!Q![#r!g!h$i#X#Y$i~$WP!Q![$Z~$`Rc~!Q![$Z!g!h$i#X#Y$i~$lR{|$u}!O$u!Q![${~$xP!Q![${~%QPc~!Q![${~%YOZ~~%_P^~!_!`%b~%gO_~~%lP[~!_!`%o~%tO]~P%yTXP!O!P%t!Q![%t!c!}%t#R#S%t#T#o%tR&_VXP!O!P%t!Q![%t!c!p%t!p!q&t!q!}%t#R#S%t#T#o%tR&yVXP!O!P%t!Q![%t!c!f%t!f!g'`!g!}%t#R#S%t#T#o%tR'gTTQXP!O!P%t!Q![%t!c!}%t#R#S%t#T#o%tR'{VXP!O!P%t!Q![%t!c!t%t!t!u(b!u!}%t#R#S%t#T#o%tR(iTXPUQ!O!P%t!Q![%t!c!}%t#R#S%t#T#o%tR(}VXP!O!P%t!Q![%t!c!}%t#R#S%t#T#b%t#b#c)d#c#o%tR)iVXP!O!P%t!Q![%t!c!}%t#R#S%t#T#W%t#W#X'`#X#o%tR*TVXP!O!P%t!Q![%t!c!}%t#R#S%t#T#f%t#f#g(b#g#o%t~*oOi~~*tOj~", - tokenizers: [0, 1], - topRules: { KQL: [0, 2] }, - tokenPrec: 0, -}); -// This file was generated by lezer-generator. You probably shouldn't edit it. -const LineComment = 1, - KQL = 2, - Expr = 3, - BinaryExpr = 4, - And = 5, - Or = 6, - ParenExpr = 7, - VectorSelector = 8, - FieldName = 9, - MatchOperator = 10, - Colon = 11, - Gtr = 12, - Gte = 13, - Lss = 14, - Lte = 15, - FieldValue = 16, - Asterisk = 17, - StringLiteral = 18, - NumberLiteral = 19, - NestedQuery = 20; - -export { - And, - Asterisk, - BinaryExpr, - Colon, - Expr, - FieldName, - FieldValue, - Gte, - Gtr, - KQL, - LineComment, - Lss, - Lte, - MatchOperator, - NestedQuery, - NumberLiteral, - Or, - ParenExpr, - StringLiteral, - VectorSelector, - parser, -}; diff --git a/src/components/KQLInput/index.tsx b/src/components/KQLInput/index.tsx index 78b4d664f..6e2a66aa2 100644 --- a/src/components/KQLInput/index.tsx +++ b/src/components/KQLInput/index.tsx @@ -1,28 +1,12 @@ -import React, { useRef, useEffect, useContext } from 'react'; -import classNames from 'classnames'; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import _ from 'lodash'; import i18next from 'i18next'; +import { KQLMonacoEditor } from '@fc-components/monaco-editor'; import { IRawTimeRange } from '@/components/TimeRangePicker'; -import { EditorView, highlightSpecialChars, keymap, ViewUpdate, placeholder as placeholderFunc } from '@codemirror/view'; -import { EditorState, Prec, Compartment } from '@codemirror/state'; -import { indentOnInput } from '@codemirror/language'; -import { history, historyKeymap } from '@codemirror/history'; -import { defaultKeymap, insertNewlineAndIndent } from '@codemirror/commands'; -import { bracketMatching } from '@codemirror/matchbrackets'; -import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets'; -import { highlightSelectionMatches } from '@codemirror/search'; -import { commentKeymap } from '@codemirror/comment'; -import { lintKeymap } from '@codemirror/lint'; -import { autocompletion, completionKeymap, startCompletion, closeCompletion } from '@codemirror/autocomplete'; import { CommonStateContext } from '@/App'; -import { kQLExtension } from './kql'; -import { baseTheme, lightTheme, darkTheme, highlighter } from './CMTheme'; -import './style.less'; +import { HTTPClient as ESHTTPClient, CachedClient as ESCachedClient } from './client/elasticsearch'; import './locale'; -const dynamicConfigCompartment = new Compartment(); -const QLExtension = new kQLExtension(); - export interface Props { datasourceValue?: number; query: { @@ -37,16 +21,29 @@ export interface Props { onChange?: (expr?: string) => void; executeQuery?: (expr?: string) => void; completeEnabled?: boolean; - trigger?: ('onBlur' | 'onEnter')[]; // 触发 onChang 的事件 + trigger?: ('onBlur' | 'onEnter' | 'onChange')[]; // 触发 onChange 的事件 placeholder?: string | false; onEnter?: () => void; } -export default function index(props: Props) { +// editor 类型与 @fc-components/monaco-editor 同源,避免类型冲突 +type KQLEditorProps = React.ComponentProps; +type MonacoEditorInstance = Parameters>[0]; + +const handleEditorDidMount = (editor: MonacoEditorInstance) => { + // 点击输入框时唤起补全,与旧 CodeMirror 编辑器行为一致(旧编辑器点击总是重新触发补全) + editor.onMouseDown(() => { + setTimeout(() => { + editor.trigger('kql', 'editor.action.triggerSuggest', {}); + }, 0); + }); +}; + +export default function KQLInput(props: Props) { const { datasourceValue, query, - historicalRecords, + historicalRecords, // 新编辑器暂不支持历史记录补全,保留 prop 以兼容旧调用方 value, onChange, executeQuery, @@ -56,152 +53,89 @@ export default function index(props: Props) { placeholder, onEnter, } = props; - const containerRef = useRef(null); - const viewRef = useRef(null); - const executeQueryCallback = useRef(executeQuery); - const realValue = useRef(value || ''); - query.range = query.range || { - start: 'now-12h', - end: 'now', - }; const { darkMode } = useContext(CommonStateContext); - const dynamicConfig = [darkMode ? darkTheme : lightTheme]; + const [innerValue, setInnerValue] = useState(value); + const innerValueRef = useRef(value); + // KQLMonacoEditor 的 onBlur/onEnter 仅在挂载时注册一次,闭包会过期, + // 这里通过 ref 始终拿到最新的 props(与 react-monaco-editor 内部 onChangeRef 同一模式) + const latestPropsRef = useRef({ value, onChange, executeQuery, onEnter, trigger }); + latestPropsRef.current = { value, onChange, executeQuery, onEnter, trigger }; useEffect(() => { - executeQueryCallback.current = executeQuery; - QLExtension.activateCompletion(true).setComplete( - completeEnabled - ? { - remote: { - datasourceValue, - query, - }, - historicalRecords, - } - : undefined, - ); - - const view = viewRef.current; - if (view === null) { - if (!containerRef.current) { - throw new Error('expected CodeMirror container element to exist'); - } - - const startState = EditorState.create({ - doc: value, - extensions: [ - baseTheme, - highlightSpecialChars(), - history(), - EditorState.allowMultipleSelections.of(true), - indentOnInput(), - bracketMatching(), - closeBrackets(), - autocompletion({ - maxRenderedOptions: 10, - }), - highlightSelectionMatches(), - highlighter, - EditorView.lineWrapping, - keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap, ...commentKeymap, ...completionKeymap, ...lintKeymap]), - placeholderFunc(placeholder === false || !placeholder ? i18next.t('kql:search') : placeholder), - QLExtension.asExtension(), - EditorView.editable.of(!readonly), - dynamicConfigCompartment.of(dynamicConfig), - keymap.of([ - { - key: 'Escape', - run: (v: EditorView): boolean => { - v.contentDOM.blur(); - return false; - }, - }, - ]), - Prec.override( - keymap.of([ - { - key: 'Enter', - run: (v: EditorView): boolean => { - if (typeof executeQueryCallback.current === 'function') { - executeQueryCallback.current(realValue.current); - } - if (typeof onChange === 'function' && _.includes(trigger, 'onEnter')) { - onChange(realValue.current); - } - if (onEnter) { - onEnter(); - } - return true; - }, - }, - { - key: 'Shift-Enter', - run: insertNewlineAndIndent, - }, - ]), - ), - EditorView.updateListener.of((update: ViewUpdate): void => { - if (typeof onChange === 'function') { - const val = update.state.doc.toString(); - if (val !== realValue.current) { - realValue.current = val; - if (_.includes(trigger, 'onChange')) { - onChange(val); - } - if (val === '' && viewRef.current) { - startCompletion(viewRef.current); - } - } - } - }), - ], - }); + innerValueRef.current = value; + setInnerValue(value); + }, [value]); - const view = new EditorView({ - state: startState, - parent: containerRef.current, - }); + const queryKey = JSON.stringify(query); + const queryWithDefaultRange = useMemo( + () => ({ + ...query, + range: query.range || { + start: 'now-12h', + end: 'now', + }, + }), + [queryKey], + ); - viewRef.current = view; + const completeClient = useMemo(() => { + if (!completeEnabled) { + return undefined; } - }, [completeEnabled, datasourceValue, JSON.stringify(query), JSON.stringify(historicalRecords)]); + return new ESCachedClient( + new ESHTTPClient({ + datasourceValue, + query: queryWithDefaultRange, + }), + ); + }, [completeEnabled, datasourceValue, JSON.stringify(queryWithDefaultRange)]); - useEffect(() => { - if (realValue.current !== value) { - const oldValue = realValue.current; - realValue.current = value || ''; - const view = viewRef.current; - if (view === null) { - return; - } - view.dispatch( - view.state.update({ - changes: { from: 0, to: oldValue?.length || 0, insert: value }, - }), - ); - } - }, [value]); + const fetchFieldNames = useCallback(async () => { + if (!completeClient) return []; + return completeClient.fields(); + }, [completeClient]); + + const fetchFieldValues = useCallback( + async (fieldName: string) => { + if (!completeClient) return []; + return completeClient.fieldValues(fieldName); + }, + [completeClient], + ); return ( -
{ - if (typeof onChange === 'function' && _.includes(trigger, 'onBlur')) { - if (realValue.current !== value) { - onChange(realValue.current); - } + { + innerValueRef.current = newValue; + setInnerValue(newValue); + if (_.includes(latestPropsRef.current.trigger, 'onChange')) { + latestPropsRef.current.onChange?.(newValue); } - if (viewRef.current) { - closeCompletion(viewRef.current); + }} + onBlur={() => { + const { value, onChange, trigger } = latestPropsRef.current; + if (_.includes(trigger, 'onBlur') && innerValueRef.current !== value) { + onChange?.(innerValueRef.current); } }} - onClick={() => { - if (viewRef.current) { - startCompletion(viewRef.current); + onEnter={() => { + const { onChange, executeQuery, onEnter: onEnterProp, trigger } = latestPropsRef.current; + executeQuery?.(innerValueRef.current); + if (_.includes(trigger, 'onEnter')) { + onChange?.(innerValueRef.current); } + onEnterProp?.(); }} - > -
-
+ /> ); } diff --git a/src/components/KQLInput/kql.ts b/src/components/KQLInput/kql.ts deleted file mode 100644 index 3b8955905..000000000 --- a/src/components/KQLInput/kql.ts +++ /dev/null @@ -1,75 +0,0 @@ -import _ from 'lodash'; -import { styleTags, tags } from '@codemirror/highlight'; -import { Extension } from '@codemirror/state'; -import { CompleteConfiguration, CompleteStrategy, newCompleteStrategy } from './complete'; -import { CompletionContext } from '@codemirror/autocomplete'; -import { LRLanguage } from '@codemirror/language'; -import { parser } from './grammar/parser'; - -export enum LanguageType { - KQL = 'KQL', -} - -export function KQLLanguage(top: LanguageType): LRLanguage { - return LRLanguage.define({ - parser: parser.configure({ - top: top, - props: [ - styleTags({ - LineComment: tags.comment, - FieldName: tags.labelName, - FieldValue: tags.string, - StringLiteral: tags.string, - NumberLiteral: tags.number, - 'And Or': tags.logicOperator, - '( )': tags.paren, - '{ }': tags.brace, - Asterisk: tags.operator, - '⚠': tags.invalid, - }), - ], - }), - languageData: { - closeBrackets: { brackets: ['(', '{', '"'] }, - commentTokens: { line: '#' }, - }, - }); -} - -export class kQLExtension { - private complete: CompleteStrategy; - private enableCompletion: boolean; - - constructor() { - this.complete = newCompleteStrategy(); - this.enableCompletion = true; - } - - setComplete(conf?: CompleteConfiguration): kQLExtension { - this.complete = newCompleteStrategy(conf); - return this; - } - - getComplete(): CompleteStrategy { - return this.complete; - } - - activateCompletion(activate: boolean): kQLExtension { - this.enableCompletion = activate; - return this; - } - - asExtension(languageType = LanguageType.KQL): Extension { - const language = KQLLanguage(languageType); - let extension: Extension = [language]; - if (this.enableCompletion) { - const completion = language.data.of({ - autocomplete: (context: CompletionContext) => { - return this.complete.KQL(context); - }, - }); - extension = extension.concat(completion); - } - return extension; - } -} diff --git a/src/components/KQLInput/style.less b/src/components/KQLInput/style.less deleted file mode 100644 index 895418545..000000000 --- a/src/components/KQLInput/style.less +++ /dev/null @@ -1,68 +0,0 @@ -.kql-input { - .cmt-name { - color: #000; - } - .cmt-number { - color: #09885a; - } - .cmt-string { - color: #a31515; - } - .cmt-keyword { - color: #008080; - } - .cmt-variableName { - color: #008080; - } - .cmt-labelName { - color: #800000; - } - .cmt-modifier { - color: #008080; - } - .cmt-invalid { - color: red; - } - .cmt-comment { - color: #888; - font-style: italic; - } - .cmt-logic-operator { - color: #FF752A; - } -} -.theme-dark { - .kql-input { - .cmt-name { - color: #000; - } - .cmt-number { - color: #b5cea8; - } - .cmt-string { - color: #ce9178; - } - .cmt-keyword { - color: #008080; - } - .cmt-variableName { - color: #ce9178; - } - .cmt-labelName { - color: #73bf69; - } - .cmt-modifier { - color: #008080; - } - .cmt-invalid { - color: red; - } - .cmt-comment { - color: #888; - font-style: italic; - } - .cmt-logic-operator { - color: #FF752A; - } - } -} \ No newline at end of file diff --git a/src/components/KQLInput/utils.ts b/src/components/KQLInput/utils.ts deleted file mode 100644 index 2a1efe1e0..000000000 --- a/src/components/KQLInput/utils.ts +++ /dev/null @@ -1,65 +0,0 @@ -import _ from 'lodash'; - -export const setLocalQueryHistory = (datasourceValue: number | undefined, query: string) => { - if (!datasourceValue || !query) return; - const localKey = `es-query-history-${datasourceValue}`; - query = _.trim(query); - const queryHistoryStr = localStorage.getItem(localKey); - let queryHistoryMap = new Map(); - if (queryHistoryStr) { - try { - const queryHistory = JSON.parse(queryHistoryStr); - queryHistoryMap = new Map(queryHistory); - } catch (e) { - console.error(e); - } - } - if (queryHistoryMap.has(query)) { - const count = queryHistoryMap.get(query); - let newCount = 1; - if (_.isNumber(count)) { - newCount = count + 1; - } - queryHistoryMap.set(query, newCount); - } else { - if (queryHistoryMap.size < 20) { - queryHistoryMap.set(query, 1); - } else { - const minCount = _.min(Array.from(queryHistoryMap.values())); - if (minCount) { - for (const x of queryHistoryMap.entries()) { - if (x[1] === minCount) { - queryHistoryMap.delete(x[0]); - break; - } - } - queryHistoryMap.set(query, 1); - } - } - } - const newQueryHistory: [string, number][] = []; - for (const x of queryHistoryMap.entries()) { - newQueryHistory.push(x); - } - localStorage.setItem(localKey, JSON.stringify(newQueryHistory)); -}; - -export const getLocalQueryHistory = (datasourceValue?: number) => { - if (!datasourceValue) return []; - const localKey = `es-query-history-${datasourceValue}`; - const queryHistoryStr = localStorage.getItem(localKey); - let queryHistoryMap = new Map(); - if (queryHistoryStr) { - try { - const queryHistory = JSON.parse(queryHistoryStr); - queryHistoryMap = new Map(queryHistory); - } catch (e) { - console.error(e); - } - } - const queryHistory: [string, number][] = []; - for (const x of queryHistoryMap.entries()) { - queryHistory.push(x); - } - return _.slice(_.reverse(_.sortBy(queryHistory, (item) => item[1])), 0, 5); -};