diff --git a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx index d995299ddc3..a7ac0d7fb0d 100644 --- a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx +++ b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx @@ -27,6 +27,8 @@ class UnthemedCodeEditor extends PureComponent { if (this.completionCancel) { this.completionCancel.dispose(); } + + this.props.onEditorWillUnmount?.(); } componentDidUpdate(oldProps: Props) { @@ -77,6 +79,13 @@ class UnthemedCodeEditor extends PureComponent { } }; + onFocus = () => { + const { onFocus } = this.props; + if (onFocus) { + onFocus(this.getEditorValue()); + } + }; + onSave = () => { const { onSave } = this.props; if (onSave) { @@ -164,7 +173,12 @@ class UnthemedCodeEditor extends PureComponent { } return ( -
+
void; + /** Callback before the edior has unmounted */ + onEditorWillUnmount?: () => void; + /** Handler to be performed when editor is blurred */ onBlur?: CodeEditorChangeHandler; + /** Handler to be performed when editor is focused */ + onFocus?: CodeEditorChangeHandler; + /** Handler to be performed whenever the text inside the editor changes */ onChange?: CodeEditorChangeHandler; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx index e3c2d301bba..52c44d1d252 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx @@ -1,5 +1,5 @@ import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; -import React, { ReactNode, useCallback } from 'react'; +import React, { ReactNode, useCallback, useRef } from 'react'; import { QueryEditorProps } from '@grafana/data'; import { CodeEditor, Monaco, Themeable2, withTheme2 } from '@grafana/ui'; @@ -7,7 +7,7 @@ import { CodeEditor, Monaco, Themeable2, withTheme2 } from '@grafana/ui'; import { CloudWatchDatasource } from '../../../datasource'; import language from '../../../language/logs/definition'; import { TRIGGER_SUGGEST } from '../../../language/monarch/commands'; -import { registerLanguage } from '../../../language/monarch/register'; +import { registerLanguage, reRegisterCompletionProvider } from '../../../language/monarch/register'; import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../../../types'; import { getStatsGroups } from '../../../utils/query/getStatsGroups'; import { LogGroupsFieldWrapper } from '../../shared/LogGroups/LogGroupsField'; @@ -22,6 +22,27 @@ export const CloudWatchLogsQueryFieldMonaco = (props: CloudWatchLogsQueryFieldPr const { query, datasource, onChange, ExtraFieldElement, data } = props; const showError = data?.error?.refId === query.refId; + const monacoRef = useRef(); + const disposalRef = useRef(); + + const onChangeLogs = useCallback( + async (query: CloudWatchLogsQuery) => { + onChange(query); + }, + [onChange] + ); + + const onFocus = useCallback(async () => { + disposalRef.current = await reRegisterCompletionProvider( + monacoRef.current!, + language, + datasource.logsCompletionItemProviderFunc({ + region: query.region, + logGroups: query.logGroups, + }), + disposalRef.current + ); + }, [datasource, query.logGroups, query.region]); const onChangeQuery = useCallback( (value: string) => { @@ -44,6 +65,17 @@ export const CloudWatchLogsQueryFieldMonaco = (props: CloudWatchLogsQueryFieldPr }, [onChangeQuery] ); + const onBeforeEditorMount = async (monaco: Monaco) => { + monacoRef.current = monaco; + disposalRef.current = await registerLanguage( + monaco, + language, + datasource.logsCompletionItemProviderFunc({ + region: query.region, + logGroups: query.logGroups, + }) + ); + }; return ( <> @@ -53,11 +85,11 @@ export const CloudWatchLogsQueryFieldMonaco = (props: CloudWatchLogsQueryFieldPr legacyLogGroupNames={query.logGroupNames} logGroups={query.logGroups} onChange={(logGroups) => { - onChange({ ...query, logGroups, logGroupNames: undefined }); + onChangeLogs({ ...query, logGroups, logGroupNames: undefined }); }} //legacy props legacyOnChange={(logGroupNames) => { - onChange({ ...query, logGroupNames }); + onChangeLogs({ ...query, logGroupNames }); }} />
@@ -90,11 +122,12 @@ export const CloudWatchLogsQueryFieldMonaco = (props: CloudWatchLogsQueryFieldPr if (value !== query.expression) { onChangeQuery(value); } + disposalRef.current?.dispose(); }} - onBeforeEditorMount={(monaco: Monaco) => - registerLanguage(monaco, language, datasource.logsCompletionItemProvider) - } + onFocus={onFocus} + onBeforeEditorMount={onBeforeEditorMount} onEditorDidMount={onEditorMount} + onEditorWillUnmount={() => disposalRef.current?.dispose()} />
{ExtraFieldElement} diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index e31c85c804b..4d2a757700b 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -22,7 +22,11 @@ import { DEFAULT_METRICS_QUERY, getDefaultLogsQuery } from './defaultQueries'; import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from './guards'; import { CloudWatchLogsLanguageProvider } from './language/cloudwatch-logs/CloudWatchLogsLanguageProvider'; import { SQLCompletionItemProvider } from './language/cloudwatch-sql/completion/CompletionItemProvider'; -import { LogsCompletionItemProvider } from './language/logs/completion/CompletionItemProvider'; +import { + LogsCompletionItemProvider, + LogsCompletionItemProviderFunc, + queryContext, +} from './language/logs/completion/CompletionItemProvider'; import { MetricMathCompletionItemProvider } from './language/metric-math/completion/CompletionItemProvider'; import { CloudWatchAnnotationQueryRunner } from './query-runner/CloudWatchAnnotationQueryRunner'; import { CloudWatchLogsQueryRunner } from './query-runner/CloudWatchLogsQueryRunner'; @@ -45,7 +49,7 @@ export class CloudWatchDatasource languageProvider: CloudWatchLogsLanguageProvider; sqlCompletionItemProvider: SQLCompletionItemProvider; metricMathCompletionItemProvider: MetricMathCompletionItemProvider; - logsCompletionItemProvider: LogsCompletionItemProvider; + logsCompletionItemProviderFunc: (queryContext: queryContext) => LogsCompletionItemProvider; defaultLogGroups?: string[]; type = 'cloudwatch'; @@ -67,7 +71,7 @@ export class CloudWatchDatasource this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.resources, this.templateSrv); this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.resources, this.templateSrv); this.metricsQueryRunner = new CloudWatchMetricsQueryRunner(instanceSettings, templateSrv, super.query.bind(this)); - this.logsCompletionItemProvider = new LogsCompletionItemProvider(this.resources, this.templateSrv); + this.logsCompletionItemProviderFunc = LogsCompletionItemProviderFunc(this.resources, this.templateSrv); this.logsQueryRunner = new CloudWatchLogsQueryRunner( instanceSettings, templateSrv, diff --git a/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.test.ts b/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.test.ts index cbdfaf73d18..63ddcc5e1cb 100644 --- a/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.test.ts @@ -6,6 +6,8 @@ import { emptyQuery, filterQuery, newCommandQuery, sortQuery } from '../../../__ import MonacoMock from '../../../__mocks__/monarch/Monaco'; import TextModel from '../../../__mocks__/monarch/TextModel'; import { ResourcesAPI } from '../../../resources/ResourcesAPI'; +import { ResourceResponse } from '../../../resources/types'; +import { LogGroup, LogGroupField } from '../../../types'; import cloudWatchLogsLanguageDefinition from '../definition'; import { LOGS_COMMANDS, LOGS_FUNCTION_OPERATORS, SORT_DIRECTION_KEYWORDS } from '../language'; @@ -18,14 +20,19 @@ jest.mock('monaco-editor/esm/vs/editor/editor.api', () => ({ const getSuggestions = async ( value: string, position: monacoTypes.IPosition, - variables: CustomVariableModel[] = [] + variables: CustomVariableModel[] = [], + logGroups: LogGroup[] = [], + fields: Array> = [] ) => { const setup = new LogsCompletionItemProvider( { getActualRegion: () => 'us-east-2', } as ResourcesAPI, - setupMockedTemplateService(variables) + setupMockedTemplateService(variables), + { region: 'default', logGroups } ); + + setup.resources.getLogGroupFields = jest.fn().mockResolvedValue(fields); const monaco = MonacoMock as Monaco; const provider = setup.getCompletionProvider(monaco, cloudWatchLogsLanguageDefinition); const { suggestions } = await provider.provideCompletionItems( @@ -76,5 +83,17 @@ describe('LogsCompletionItemProvider', () => { const expectedLabels = [...LOGS_COMMANDS, expectedTemplateVariableLabel]; expect(suggestionLabels).toEqual(expect.arrayContaining(expectedLabels)); }); + + it('fetches fields when logGroups are set', async () => { + const suggestions = await getSuggestions( + sortQuery.query, + sortQuery.position, + [], + [{ arn: 'foo', name: 'bar' }], + [{ value: { name: '@field' } }] + ); + const suggestionLabels = suggestions.map((s) => s.label); + expect(suggestionLabels).toEqual(expect.arrayContaining(['@field'])); + }); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.ts index 4f804a08ea8..d5e9dc3c318 100644 --- a/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.ts @@ -1,7 +1,8 @@ -import { getTemplateSrv, type TemplateSrv } from '@grafana/runtime'; +import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { Monaco, monacoTypes } from '@grafana/ui'; import { type ResourcesAPI } from '../../../resources/ResourcesAPI'; +import { LogGroup } from '../../../types'; import { CompletionItemProvider } from '../../monarch/CompletionItemProvider'; import { LinkedToken } from '../../monarch/LinkedToken'; import { TRIGGER_SUGGEST } from '../../monarch/commands'; @@ -12,12 +13,26 @@ import { getStatementPosition } from './statementPosition'; import { getSuggestionKinds } from './suggestionKinds'; import { LogsTokenTypes } from './types'; +export type queryContext = { + logGroups?: LogGroup[]; + region: string; +}; + +export function LogsCompletionItemProviderFunc(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) { + return (queryContext: queryContext) => { + return new LogsCompletionItemProvider(resources, templateSrv, queryContext); + }; +} + export class LogsCompletionItemProvider extends CompletionItemProvider { - constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) { + queryContext: queryContext; + + constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv(), queryContext: queryContext) { super(resources, templateSrv); this.getStatementPosition = getStatementPosition; this.getSuggestionKinds = getSuggestionKinds; this.tokenTypes = LogsTokenTypes; + this.queryContext = queryContext; } async getSuggestions( @@ -56,6 +71,7 @@ export class LogsCompletionItemProvider extends CompletionItemProvider { insertText: `${command} $0`, insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, command: TRIGGER_SUGGEST, + kind: monaco.languages.CompletionItemKind.Method, }); }); break; @@ -65,12 +81,32 @@ export class LogsCompletionItemProvider extends CompletionItemProvider { insertText: `${f}($0)`, insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, command: TRIGGER_SUGGEST, + kind: monaco.languages.CompletionItemKind.Function, }); }); + + if (this.queryContext.logGroups && this.queryContext.logGroups.length > 0) { + let fields = await this.fetchFields(this.queryContext.logGroups, this.queryContext.region); + fields.push('@log'); + fields.forEach((field) => { + if (field !== '') { + addSuggestion(field, { + range, + label: field, + insertText: field, + kind: monaco.languages.CompletionItemKind.Field, + sortText: CompletionItemPriority.High, + }); + } + }); + } break; case SuggestionKind.SortOrderDirectionKeyword: SORT_DIRECTION_KEYWORDS.forEach((direction) => { - addSuggestion(direction, { sortText: CompletionItemPriority.High }); + addSuggestion(direction, { + sortText: CompletionItemPriority.High, + kind: monaco.languages.CompletionItemKind.Operator, + }); }); break; case SuggestionKind.InKeyword: @@ -82,19 +118,31 @@ export class LogsCompletionItemProvider extends CompletionItemProvider { }); break; } + } - this.templateSrv.getVariables().map((v) => { - const variable = `$${v.name}`; - addSuggestion(variable, { - range, - label: variable, - insertText: variable, - kind: monaco.languages.CompletionItemKind.Variable, - sortText: CompletionItemPriority.Low, - }); + this.templateSrv.getVariables().map((v) => { + const variable = `$${v.name}`; + addSuggestion(variable, { + range, + label: variable, + insertText: variable, + kind: monaco.languages.CompletionItemKind.Variable, + sortText: CompletionItemPriority.Low, }); - } + }); return suggestions; } + + private fetchFields = async (logGroups: LogGroup[], region: string): Promise => { + const results = await Promise.all( + logGroups.map((logGroup) => + this.resources + .getLogGroupFields({ logGroupName: logGroup.name, arn: logGroup.arn, region }) + .then((fields) => fields.filter((f) => f).map((f) => f.value.name ?? '')) + ) + ); + // Deduplicate fields + return [...new Set(results.flat())]; + }; } diff --git a/public/app/plugins/datasource/cloudwatch/language/monarch/register.ts b/public/app/plugins/datasource/cloudwatch/language/monarch/register.ts index 4b552a4763a..ace6756e213 100644 --- a/public/app/plugins/datasource/cloudwatch/language/monarch/register.ts +++ b/public/app/plugins/datasource/cloudwatch/language/monarch/register.ts @@ -15,7 +15,23 @@ export type LanguageDefinition = { }>; }; -export const registerLanguage = ( +export const reRegisterCompletionProvider = async ( + monaco: Monaco, + language: LanguageDefinition, + completionItemProvider: Completeable, + disposal?: monacoType.IDisposable +) => { + const { id, loader } = language; + disposal?.dispose(); + return loader().then((monarch) => { + return monaco.languages.registerCompletionItemProvider( + id, + completionItemProvider.getCompletionProvider(monaco, language) + ); + }); +}; + +export const registerLanguage = async ( monaco: Monaco, language: LanguageDefinition, completionItemProvider: Completeable @@ -28,9 +44,12 @@ export const registerLanguage = ( } monaco.languages.register({ id }); - loader().then((monarch) => { + return loader().then((monarch) => { monaco.languages.setMonarchTokensProvider(id, monarch.language); monaco.languages.setLanguageConfiguration(id, monarch.conf); - monaco.languages.registerCompletionItemProvider(id, completionItemProvider.getCompletionProvider(monaco, language)); + return monaco.languages.registerCompletionItemProvider( + id, + completionItemProvider.getCompletionProvider(monaco, language) + ); }); };