@fc-components/monaco-editor
Advanced tools
| export interface LogQLHistoryItem { | ||
| query: string; | ||
| usageCount: number; | ||
| lastUsedAt: number; | ||
| } | ||
| export declare function readHistory(historyKey: string | undefined): LogQLHistoryItem[]; | ||
| export declare function saveHistory(historyKey: string | undefined, query: string, now?: number): LogQLHistoryItem[]; | ||
| export declare function getHistoryCompletionItems(historyKey: string | undefined): LogQLHistoryItem[]; |
| import { getHistoryCompletionItems, readHistory, saveHistory } from '../history'; | ||
| describe('LogQL query history', () => { | ||
| const historyKey = 'logql-history-test'; | ||
| beforeEach(() => { | ||
| window.localStorage.clear(); | ||
| }); | ||
| it('does not read or write history without a key', () => { | ||
| expect(saveHistory(undefined, 'service:api', 1)).toEqual([]); | ||
| expect(window.localStorage.length).toBe(0); | ||
| expect(readHistory(undefined)).toEqual([]); | ||
| }); | ||
| it('does not save blank queries', () => { | ||
| expect(saveHistory(historyKey, ' ', 1)).toEqual([]); | ||
| expect(window.localStorage.getItem(historyKey)).toBeNull(); | ||
| }); | ||
| it('increments the usage count for an existing query', () => { | ||
| saveHistory(historyKey, 'service:api', 1); | ||
| saveHistory(historyKey, 'service:api', 2); | ||
| expect(readHistory(historyKey)).toEqual([{ query: 'service:api', usageCount: 2, lastUsedAt: 2 }]); | ||
| }); | ||
| it('evicts the least-used and then oldest query after 20 items', () => { | ||
| for (let index = 0; index < 20; index += 1) { | ||
| saveHistory(historyKey, `query-${index}`, index + 1); | ||
| } | ||
| saveHistory(historyKey, 'query-19', 30); | ||
| saveHistory(historyKey, 'query-new', 31); | ||
| const history = readHistory(historyKey); | ||
| expect(history).toHaveLength(20); | ||
| expect(history.find((item) => item.query === 'query-0')).toBeUndefined(); | ||
| expect(history.find((item) => item.query === 'query-19')?.usageCount).toBe(2); | ||
| }); | ||
| it('returns only the ten most-used records for completion', () => { | ||
| for (let index = 0; index < 12; index += 1) { | ||
| for (let count = 0; count <= index; count += 1) { | ||
| saveHistory(historyKey, `query-${index}`, index * 100 + count); | ||
| } | ||
| } | ||
| const completions = getHistoryCompletionItems(historyKey); | ||
| expect(completions).toHaveLength(10); | ||
| expect(completions[0].query).toBe('query-11'); | ||
| expect(completions[9].query).toBe('query-2'); | ||
| }); | ||
| it('silently ignores invalid cached data', () => { | ||
| window.localStorage.setItem(historyKey, '{not json'); | ||
| expect(readHistory(historyKey)).toEqual([]); | ||
| expect(() => saveHistory(historyKey, 'service:api', 1)).not.toThrow(); | ||
| }); | ||
| it('silently handles unavailable localStorage', () => { | ||
| const getItem = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { | ||
| throw new Error('unavailable'); | ||
| }); | ||
| const setItem = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { | ||
| throw new Error('unavailable'); | ||
| }); | ||
| expect(() => saveHistory(historyKey, 'service:api', 1)).not.toThrow(); | ||
| getItem.mockRestore(); | ||
| setItem.mockRestore(); | ||
| }); | ||
| }); |
| export interface LogQLHistoryItem { | ||
| query: string; | ||
| usageCount: number; | ||
| lastUsedAt: number; | ||
| } | ||
| const MAX_HISTORY_ITEMS = 20; | ||
| const MAX_COMPLETION_ITEMS = 10; | ||
| function isHistoryItem(value: unknown): value is LogQLHistoryItem { | ||
| if (typeof value !== 'object' || value === null) return false; | ||
| const item = value as Record<string, unknown>; | ||
| return ( | ||
| typeof item.query === 'string' && | ||
| item.query.trim().length > 0 && | ||
| typeof item.usageCount === 'number' && | ||
| Number.isFinite(item.usageCount) && | ||
| item.usageCount > 0 && | ||
| typeof item.lastUsedAt === 'number' && | ||
| Number.isFinite(item.lastUsedAt) | ||
| ); | ||
| } | ||
| function sortByUsage(items: LogQLHistoryItem[]): LogQLHistoryItem[] { | ||
| return [...items].sort((a, b) => b.usageCount - a.usageCount || b.lastUsedAt - a.lastUsedAt); | ||
| } | ||
| export function readHistory(historyKey: string | undefined): LogQLHistoryItem[] { | ||
| if (!historyKey || typeof window === 'undefined') return []; | ||
| try { | ||
| const value = window.localStorage.getItem(historyKey); | ||
| if (!value) return []; | ||
| const parsed: unknown = JSON.parse(value); | ||
| if (!Array.isArray(parsed)) return []; | ||
| return sortByUsage(parsed.filter(isHistoryItem)).slice(0, MAX_HISTORY_ITEMS); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
| export function saveHistory(historyKey: string | undefined, query: string, now = Date.now()): LogQLHistoryItem[] { | ||
| const normalizedQuery = query.trim(); | ||
| if (!historyKey || !normalizedQuery || typeof window === 'undefined') return []; | ||
| const history = readHistory(historyKey); | ||
| const existing = history.find((item) => item.query === normalizedQuery); | ||
| const next = existing | ||
| ? history.map((item) => (item === existing ? { ...item, usageCount: item.usageCount + 1, lastUsedAt: now } : item)) | ||
| : [...history, { query: normalizedQuery, usageCount: 1, lastUsedAt: now }]; | ||
| const sorted = sortByUsage(next).slice(0, MAX_HISTORY_ITEMS); | ||
| try { | ||
| window.localStorage.setItem(historyKey, JSON.stringify(sorted)); | ||
| } catch { | ||
| // Storage may be unavailable (e.g. private browsing or a full quota). | ||
| } | ||
| return sorted; | ||
| } | ||
| export function getHistoryCompletionItems(historyKey: string | undefined): LogQLHistoryItem[] { | ||
| return readHistory(historyKey).slice(0, MAX_COMPLETION_ITEMS); | ||
| } |
+1
-1
@@ -15,3 +15,3 @@ import promql from './promql'; | ||
| export { loki as LokiMonacoEditor }; | ||
| export type { LogQLEditorProps, LogQLVendor, LogQLSection, LogQLFieldCompletionContext, VendorConfig } from './logql/types'; | ||
| export type { LogQLEditorProps, LogQLVendor, LogQLSection, LogQLLanguage, LogQLFieldCompletionContext, VendorConfig } from './logql/types'; | ||
| export type { LokiEditorProps, LabelMatcher } from './loki/types'; |
| import * as monaco from 'monaco-editor'; | ||
| import type { LogQLFieldCompletionContext, VendorConfig } from '../types'; | ||
| import type { LogQLFieldCompletionContext, LogQLLanguage, VendorConfig } from '../types'; | ||
| import type { Situation } from '../parser/types'; | ||
| import type { LogQLHistoryItem } from '../history'; | ||
| export interface BuildCompletionsArgs { | ||
@@ -9,2 +10,5 @@ vendor: VendorConfig; | ||
| variables: string[]; | ||
| language?: LogQLLanguage; | ||
| history?: LogQLHistoryItem[]; | ||
| historyRange?: monaco.IRange; | ||
| fetchFieldNames?: (ctx: LogQLFieldCompletionContext) => Promise<string[]>; | ||
@@ -11,0 +15,0 @@ fetchFieldValues?: (fieldName: string, ctx: LogQLFieldCompletionContext) => Promise<string[]>; |
| import * as monaco from 'monaco-editor'; | ||
| import type { editor } from 'monaco-editor'; | ||
| import type { LogQLFieldCompletionContext, VendorConfig } from '../types'; | ||
| import type { LogQLFieldCompletionContext, LogQLLanguage, VendorConfig } from '../types'; | ||
| import type { MutableRefObject } from 'react'; | ||
@@ -8,2 +8,5 @@ export interface ProviderOptions { | ||
| getVariables: () => string[]; | ||
| getLanguage: () => LogQLLanguage; | ||
| getHistoryKey: () => string | undefined; | ||
| getCanShowHistory: () => boolean; | ||
| getFetchFieldNames: () => ((ctx: LogQLFieldCompletionContext) => Promise<string[]>) | undefined; | ||
@@ -10,0 +13,0 @@ getFetchFieldValues: () => ((fieldName: string, ctx: LogQLFieldCompletionContext) => Promise<string[]>) | undefined; |
| import React from 'react'; | ||
| import type { LogQLEditorProps } from './types'; | ||
| export type { LogQLEditorProps, LogQLFieldCompletionContext, LogQLVendor, LogQLSection, VendorConfig } from './types'; | ||
| export type { LogQLEditorProps, LogQLFieldCompletionContext, LogQLLanguage, LogQLVendor, LogQLSection, VendorConfig } from './types'; | ||
| declare const LogQLEditor: React.FC<LogQLEditorProps>; | ||
@@ -5,0 +5,0 @@ export default LogQLEditor; |
| import type * as monacoTypes from 'monaco-editor/esm/vs/editor/editor.api'; | ||
| export declare type LogQLVendor = 'sls' | 'cls' | 'tls' | 'lts' | 'bls'; | ||
| export declare type LogQLSection = 'search' | 'analyze'; | ||
| export declare type LogQLLanguage = 'zh_CN' | 'en_US'; | ||
| export interface LogQLFieldCompletionContext { | ||
@@ -43,2 +44,6 @@ vendor: LogQLVendor; | ||
| variables?: string[]; | ||
| /** Language used by built-in completion labels. Defaults to `zh_CN`. */ | ||
| language?: LogQLLanguage; | ||
| /** Enables locally persisted query history under this localStorage key. */ | ||
| historyKey?: string; | ||
| onChange?: (value: string) => void; | ||
@@ -45,0 +50,0 @@ onEnter?: (value: string) => void; |
+1
-1
@@ -6,3 +6,3 @@ { | ||
| }, | ||
| "version": "0.5.5", | ||
| "version": "0.5.6", | ||
| "license": "MIT", | ||
@@ -9,0 +9,0 @@ "main": "dist/index.js", |
+1
-1
@@ -16,3 +16,3 @@ import promql from './promql'; | ||
| export { loki as LokiMonacoEditor }; | ||
| export type { LogQLEditorProps, LogQLVendor, LogQLSection, LogQLFieldCompletionContext, VendorConfig } from './logql/types'; | ||
| export type { LogQLEditorProps, LogQLVendor, LogQLSection, LogQLLanguage, LogQLFieldCompletionContext, VendorConfig } from './logql/types'; | ||
| export type { LokiEditorProps, LabelMatcher } from './loki/types'; |
@@ -76,2 +76,35 @@ import { buildCompletions } from '../completion/completions'; | ||
| it('places history completions ahead of all other suggestions', async () => { | ||
| const items = await buildCompletions({ | ||
| vendor: SLS, | ||
| situation: makeSituation({ kind: 'FIELD_NAME' }), | ||
| range: RANGE, | ||
| historyRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 4 }, | ||
| history: [ | ||
| { query: 'service:api', usageCount: 5, lastUsedAt: 2 }, | ||
| { query: 'level:ERROR', usageCount: 2, lastUsedAt: 1 }, | ||
| ], | ||
| variables: ['env'], | ||
| }); | ||
| expect(items.slice(0, 2).map((item) => item.label)).toEqual([ | ||
| { label: 'service:api', description: '历史记录' }, | ||
| { label: 'level:ERROR', description: '历史记录' }, | ||
| ]); | ||
| expect(items[0].range).toEqual({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 4 }); | ||
| }); | ||
| it('localizes the history completion label', async () => { | ||
| const items = await buildCompletions({ | ||
| vendor: SLS, | ||
| situation: makeSituation({ kind: 'FIELD_NAME' }), | ||
| range: RANGE, | ||
| variables: [], | ||
| language: 'en_US', | ||
| history: [{ query: 'service:api', usageCount: 1, lastUsedAt: 1 }], | ||
| }); | ||
| expect(items[0].label).toEqual({ label: 'service:api', description: 'History' }); | ||
| }); | ||
| it('uses the vendor field-assign operator when constructing field-name insert text', async () => { | ||
@@ -78,0 +111,0 @@ const slsItems = await buildCompletions({ |
@@ -15,2 +15,5 @@ import { getLogQLCompletionProvider } from '../completion/getCompletionProvider'; | ||
| })); | ||
| jest.mock('../history', () => ({ | ||
| getHistoryCompletionItems: jest.fn(), | ||
| })); | ||
@@ -20,2 +23,3 @@ import { parse } from '../parser'; | ||
| import { buildCompletions } from '../completion/completions'; | ||
| import { getHistoryCompletionItems } from '../history'; | ||
@@ -25,2 +29,3 @@ const mockParse = parse as jest.Mock; | ||
| const mockBuildCompletions = buildCompletions as jest.Mock; | ||
| const mockGetHistoryCompletionItems = getHistoryCompletionItems as jest.Mock; | ||
@@ -51,2 +56,3 @@ function createMockModel(id: string, value = ''): monaco.editor.ITextModel { | ||
| mockBuildCompletions.mockResolvedValue([]); | ||
| mockGetHistoryCompletionItems.mockReturnValue([]); | ||
| }); | ||
@@ -63,2 +69,5 @@ | ||
| getVariables: () => [], | ||
| getLanguage: () => 'zh_CN', | ||
| getHistoryKey: () => undefined, | ||
| getCanShowHistory: () => false, | ||
| getFetchFieldNames: () => undefined, | ||
@@ -84,2 +93,5 @@ getFetchFieldValues: () => undefined, | ||
| getVariables: () => [], | ||
| getLanguage: () => 'zh_CN', | ||
| getHistoryKey: () => undefined, | ||
| getCanShowHistory: () => false, | ||
| getFetchFieldNames: () => undefined, | ||
@@ -100,2 +112,62 @@ getFetchFieldValues: () => undefined, | ||
| it('includes history only while typing from an empty search input', async () => { | ||
| const model = createMockModel('same-model-2', 'serv'); | ||
| const editorRef = { current: createMockEditor(model) }; | ||
| const opts: ProviderOptions = { | ||
| vendor: {} as any, | ||
| getVariables: () => [], | ||
| getLanguage: () => 'zh_CN', | ||
| getHistoryKey: () => 'logql-history', | ||
| getCanShowHistory: () => true, | ||
| getFetchFieldNames: () => undefined, | ||
| getFetchFieldValues: () => undefined, | ||
| editorRef, | ||
| }; | ||
| mockLocate.mockReturnValue({ | ||
| section: 'search', | ||
| kind: 'FIELD_NAME', | ||
| prefix: 'serv', | ||
| replaceRange: { start: 0, end: 4 }, | ||
| }); | ||
| mockGetHistoryCompletionItems.mockReturnValue([{ query: 'service:api', usageCount: 2, lastUsedAt: 1 }]); | ||
| const provider = getLogQLCompletionProvider(opts); | ||
| await provider.provideCompletionItems!(model, createMockPosition(5)); | ||
| expect(mockGetHistoryCompletionItems).toHaveBeenCalledWith('logql-history'); | ||
| expect(mockBuildCompletions).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| history: [{ query: 'service:api', usageCount: 2, lastUsedAt: 1 }], | ||
| historyRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }, | ||
| }), | ||
| ); | ||
| }); | ||
| it('does not request history outside the input-start context', async () => { | ||
| const model = createMockModel('same-model-3', 'service:api AND '); | ||
| const editorRef = { current: createMockEditor(model) }; | ||
| const opts: ProviderOptions = { | ||
| vendor: {} as any, | ||
| getVariables: () => [], | ||
| getLanguage: () => 'zh_CN', | ||
| getHistoryKey: () => 'logql-history', | ||
| getCanShowHistory: () => true, | ||
| getFetchFieldNames: () => undefined, | ||
| getFetchFieldValues: () => undefined, | ||
| editorRef, | ||
| }; | ||
| mockLocate.mockReturnValue({ | ||
| section: 'search', | ||
| kind: 'FIELD_NAME', | ||
| prefix: '', | ||
| replaceRange: { start: 12, end: 12 }, | ||
| }); | ||
| const provider = getLogQLCompletionProvider(opts); | ||
| await provider.provideCompletionItems!(model, createMockPosition(18)); | ||
| expect(mockGetHistoryCompletionItems).not.toHaveBeenCalled(); | ||
| expect(mockBuildCompletions).toHaveBeenCalledWith(expect.objectContaining({ history: [] })); | ||
| }); | ||
| it('returns empty suggestions when editorRef.current is null', async () => { | ||
@@ -108,2 +180,5 @@ const model = createMockModel('some-model', 'test'); | ||
| getVariables: () => [], | ||
| getLanguage: () => 'zh_CN', | ||
| getHistoryKey: () => undefined, | ||
| getCanShowHistory: () => false, | ||
| getFetchFieldNames: () => undefined, | ||
@@ -128,2 +203,5 @@ getFetchFieldValues: () => undefined, | ||
| getVariables: () => [], | ||
| getLanguage: () => 'zh_CN', | ||
| getHistoryKey: () => undefined, | ||
| getCanShowHistory: () => false, | ||
| getFetchFieldNames: () => undefined, | ||
@@ -149,2 +227,5 @@ getFetchFieldValues: () => undefined, | ||
| getVariables: () => [], | ||
| getLanguage: () => 'zh_CN', | ||
| getHistoryKey: () => undefined, | ||
| getCanShowHistory: () => false, | ||
| getFetchFieldNames: () => undefined, | ||
@@ -151,0 +232,0 @@ getFetchFieldValues: () => undefined, |
| import * as monaco from 'monaco-editor'; | ||
| import type { LogQLFieldCompletionContext, VendorConfig } from '../types'; | ||
| import type { LogQLFieldCompletionContext, LogQLLanguage, VendorConfig } from '../types'; | ||
| import type { Situation } from '../parser/types'; | ||
| import type { LogQLHistoryItem } from '../history'; | ||
@@ -10,2 +11,5 @@ export interface BuildCompletionsArgs { | ||
| variables: string[]; | ||
| language?: LogQLLanguage; | ||
| history?: LogQLHistoryItem[]; | ||
| historyRange?: monaco.IRange; | ||
| fetchFieldNames?: (ctx: LogQLFieldCompletionContext) => Promise<string[]>; | ||
@@ -16,12 +20,13 @@ fetchFieldValues?: (fieldName: string, ctx: LogQLFieldCompletionContext) => Promise<string[]>; | ||
| const SORT = { | ||
| variable: '0', | ||
| fieldValue: '1', | ||
| fieldName: '2', | ||
| builtinField: '3', | ||
| keyword: '4', | ||
| function: '5', | ||
| history: '0', | ||
| variable: '1', | ||
| fieldValue: '2', | ||
| fieldName: '3', | ||
| builtinField: '4', | ||
| keyword: '5', | ||
| function: '6', | ||
| }; | ||
| export async function buildCompletions(args: BuildCompletionsArgs): Promise<monaco.languages.CompletionItem[]> { | ||
| const { vendor, situation, range, variables } = args; | ||
| const { vendor, situation, range, variables, language = 'zh_CN', history = [], historyRange = range } = args; | ||
| const items: monaco.languages.CompletionItem[] = []; | ||
@@ -45,2 +50,3 @@ const ctx: LogQLFieldCompletionContext = { | ||
| case 'FIELD_NAME': { | ||
| items.push(...historyItems(history, historyRange, language)); | ||
| items.push(...variableItems(variables, range)); | ||
@@ -83,2 +89,19 @@ items.push(...keywordItems(vendor.searchKeywords, range)); | ||
| function historyItems( | ||
| history: LogQLHistoryItem[], | ||
| range: monaco.IRange, | ||
| language: LogQLLanguage, | ||
| ): monaco.languages.CompletionItem[] { | ||
| return history.map((item, index) => ({ | ||
| label: { | ||
| label: item.query, | ||
| description: language === 'en_US' ? 'History' : '历史记录', | ||
| }, | ||
| kind: monaco.languages.CompletionItemKind.Reference, | ||
| insertText: item.query, | ||
| sortText: SORT.history + String(index).padStart(2, '0'), | ||
| range, | ||
| })); | ||
| } | ||
| /** | ||
@@ -85,0 +108,0 @@ * Deduplicate completion items by their label to avoid showing duplicates |
@@ -5,5 +5,6 @@ import * as monaco from 'monaco-editor'; | ||
| import { locate } from '../parser/locate'; | ||
| import type { LogQLFieldCompletionContext, VendorConfig } from '../types'; | ||
| import type { LogQLFieldCompletionContext, LogQLLanguage, VendorConfig } from '../types'; | ||
| import { buildCompletions } from './completions'; | ||
| import type { MutableRefObject } from 'react'; | ||
| import { getHistoryCompletionItems } from '../history'; | ||
@@ -13,2 +14,5 @@ export interface ProviderOptions { | ||
| getVariables: () => string[]; | ||
| getLanguage: () => LogQLLanguage; | ||
| getHistoryKey: () => string | undefined; | ||
| getCanShowHistory: () => boolean; | ||
| getFetchFieldNames: () => ((ctx: LogQLFieldCompletionContext) => Promise<string[]>) | undefined; | ||
@@ -40,2 +44,12 @@ getFetchFieldValues: () => ((fieldName: string, ctx: LogQLFieldCompletionContext) => Promise<string[]>) | undefined; | ||
| }; | ||
| const isHistoryContext = | ||
| opts.getCanShowHistory() && | ||
| situation.section === 'search' && | ||
| offset === value.length; | ||
| const fullRange: monaco.IRange = { | ||
| startLineNumber: 1, | ||
| startColumn: 1, | ||
| endLineNumber: position.lineNumber, | ||
| endColumn: position.column, | ||
| }; | ||
@@ -47,2 +61,5 @@ const suggestions = await buildCompletions({ | ||
| variables: opts.getVariables(), | ||
| language: opts.getLanguage(), | ||
| history: isHistoryContext ? getHistoryCompletionItems(opts.getHistoryKey()) : [], | ||
| historyRange: fullRange, | ||
| fetchFieldNames: opts.getFetchFieldNames(), | ||
@@ -49,0 +66,0 @@ fetchFieldValues: opts.getFetchFieldValues(), |
+35
-1
@@ -12,4 +12,5 @@ import React, { useEffect, useRef } from 'react'; | ||
| import type { LogQLEditorProps } from './types'; | ||
| import { saveHistory } from './history'; | ||
| export type { LogQLEditorProps, LogQLFieldCompletionContext, LogQLVendor, LogQLSection, VendorConfig } from './types'; | ||
| export type { LogQLEditorProps, LogQLFieldCompletionContext, LogQLLanguage, LogQLVendor, LogQLSection, VendorConfig } from './types'; | ||
@@ -46,2 +47,14 @@ const SIZE_MAP: Record< | ||
| const completionLayoutClassName = css` | ||
| .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .monaco-icon-label { | ||
| flex-shrink: 1; | ||
| min-width: 0; | ||
| } | ||
| .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right { | ||
| flex: 0 0 auto; | ||
| max-width: none; | ||
| } | ||
| `; | ||
| const registeredLanguages = new Set<string>(); | ||
@@ -131,2 +144,4 @@ | ||
| variables, | ||
| language = 'zh_CN', | ||
| historyKey, | ||
| onChange, | ||
@@ -146,2 +161,5 @@ onEnter, | ||
| const variablesRef = useRef<string[]>(variables ?? []); | ||
| const languageRef = useRef(language); | ||
| const historyKeyRef = useRef<string | undefined>(historyKey); | ||
| const canShowHistoryRef = useRef(value === ''); | ||
| const fetchFieldNamesRef = useRef<typeof fetchFieldNames>(fetchFieldNames); | ||
@@ -154,2 +172,8 @@ const fetchFieldValuesRef = useRef<typeof fetchFieldValues>(fetchFieldValues); | ||
| useEffect(() => { | ||
| languageRef.current = language; | ||
| }, [language]); | ||
| useEffect(() => { | ||
| historyKeyRef.current = historyKey; | ||
| }, [historyKey]); | ||
| useEffect(() => { | ||
| fetchFieldNamesRef.current = fetchFieldNames; | ||
@@ -170,2 +194,5 @@ }, [fetchFieldNames]); | ||
| getVariables: () => variablesRef.current, | ||
| getLanguage: () => languageRef.current, | ||
| getHistoryKey: () => historyKeyRef.current, | ||
| getCanShowHistory: () => canShowHistoryRef.current, | ||
| getFetchFieldNames: () => fetchFieldNamesRef.current, | ||
@@ -199,2 +226,3 @@ getFetchFieldValues: () => fetchFieldValuesRef.current, | ||
| isEditorFocused.set(false); | ||
| saveHistory(historyKeyRef.current, editor.getValue()); | ||
| onBlur?.(editor.getValue()); | ||
@@ -209,2 +237,3 @@ const position = editor.getPosition(); | ||
| isEditorFocused.set(true); | ||
| canShowHistoryRef.current = editor.getValue() === ''; | ||
| onFocus?.(editor.getValue()); | ||
@@ -246,2 +275,3 @@ }); | ||
| () => { | ||
| saveHistory(historyKeyRef.current, editor.getValue()); | ||
| onEnter?.(editor.getValue()); | ||
@@ -257,2 +287,5 @@ }, | ||
| const changeDisposable = model.onDidChangeContent((e) => { | ||
| if (editor.getValue() === '') { | ||
| canShowHistoryRef.current = true; | ||
| } | ||
| const insertedDollar = e.changes.some((c) => c.text === '$'); | ||
@@ -282,2 +315,3 @@ if (insertedDollar) { | ||
| (readOnly ? ` ${containerReadOnlyClassName}` : '') + | ||
| ` ${completionLayoutClassName}` + | ||
| (className ? ` ${className}` : '') | ||
@@ -284,0 +318,0 @@ } |
@@ -32,2 +32,4 @@ # LogQL Editor | ||
| enableAutocomplete | ||
| language="zh_CN" | ||
| historyKey="my-logql-query-history" | ||
| variables={['host', 'env', 'service']} | ||
@@ -59,2 +61,4 @@ fetchFieldNames={async ({ section }) => { | ||
| | `variables` | `string[]` | - | 变量名称列表(不含 `$`) | | ||
| | `language` | `'zh_CN' \| 'en_US'` | `'zh_CN'` | 内置补全文案语言 | | ||
| | `historyKey` | `string` | - | 本地历史记录缓存 key;传入后启用历史记录 | | ||
| | `maxHeight` | `number \| string` | - | 最大高度 | | ||
@@ -86,2 +90,3 @@ | `fontSize` | `number` | - | 字体大小 | | ||
| - 各厂商内置字段(如 `__source__`、`__path__`)已预配置 | ||
| - 配置 `historyKey` 后,回车或失焦会保存非空查询;检索输入为空并开始输入时,历史查询会置顶展示(按使用次数取前 10 条) | ||
@@ -94,2 +99,2 @@ ## 高级用法 | ||
| import { parse, locate, getVendorConfig } from '@fc-components/monaco-editor'; | ||
| ``` | ||
| ``` |
@@ -7,2 +7,4 @@ import type * as monacoTypes from 'monaco-editor/esm/vs/editor/editor.api'; | ||
| export type LogQLLanguage = 'zh_CN' | 'en_US'; | ||
| export interface LogQLFieldCompletionContext { | ||
@@ -51,2 +53,6 @@ vendor: LogQLVendor; | ||
| variables?: string[]; | ||
| /** Language used by built-in completion labels. Defaults to `zh_CN`. */ | ||
| language?: LogQLLanguage; | ||
| /** Enables locally persisted query history under this localStorage key. */ | ||
| historyKey?: string; | ||
| onChange?: (value: string) => void; | ||
@@ -53,0 +59,0 @@ onEnter?: (value: string) => void; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
3293342
1.7%160
1.91%35101
1.57%