mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Cloudwatch: Dynamic labels autocomplete (#49794)
* add completeable interface * add basic labels language * render monaco editor for label field * align styling in math expression field * add unit tests * fix broken test * remove unused import * use theme * remove comment * pr feedback * fix broken imports * improve test * make it possible to override code editor styles * use input styles and align border styles
This commit is contained in:
@@ -111,7 +111,7 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
|||||||
const value = this.props.value ?? '';
|
const value = this.props.value ?? '';
|
||||||
const longText = value.length > 100;
|
const longText = value.length > 100;
|
||||||
|
|
||||||
const styles = getStyles(theme);
|
const containerStyles = this.props.containerStyles ?? getStyles(theme).container;
|
||||||
|
|
||||||
const options: MonacoOptions = {
|
const options: MonacoOptions = {
|
||||||
wordWrap: 'off',
|
wordWrap: 'off',
|
||||||
@@ -143,7 +143,7 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} onBlur={this.onBlur} aria-label={selectors.components.CodeEditor.container}>
|
<div className={containerStyles} onBlur={this.onBlur} aria-label={selectors.components.CodeEditor.container}>
|
||||||
<ReactMonacoEditorLazy
|
<ReactMonacoEditorLazy
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export interface CodeEditorProps {
|
|||||||
* Language agnostic suggestion completions -- typically for template variables
|
* Language agnostic suggestion completions -- typically for template variables
|
||||||
*/
|
*/
|
||||||
getSuggestions?: CodeEditorSuggestionProvider;
|
getSuggestions?: CodeEditorSuggestionProvider;
|
||||||
|
|
||||||
|
containerStyles?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { monacoTypes } from '@grafana/ui';
|
||||||
|
|
||||||
|
export const afterLabelValue = {
|
||||||
|
query: '${DATAPOINT_COUNT} ',
|
||||||
|
tokens: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
type: 'predefined.cloudwatch-dynamicLabels',
|
||||||
|
language: 'cloudwatch-dynamicLabels',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 18,
|
||||||
|
type: 'white.cloudwatch-dynamicLabels',
|
||||||
|
language: 'cloudwatch-dynamicLabels',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
] as monacoTypes.Token[][],
|
||||||
|
position: {
|
||||||
|
lineNumber: 1,
|
||||||
|
column: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { afterLabelValue } from './afterLabelValue';
|
||||||
|
export { insideLabelValue } from './insideLabelValue';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { monacoTypes } from '@grafana/ui';
|
||||||
|
|
||||||
|
export const insideLabelValue = {
|
||||||
|
query: '${DATAPOINT_COUNT} ',
|
||||||
|
tokens: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
type: 'predefined.cloudwatch-dynamicLabels',
|
||||||
|
language: 'cloudwatch-dynamicLabels',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 18,
|
||||||
|
type: 'white.cloudwatch-dynamicLabels',
|
||||||
|
language: 'cloudwatch-dynamicLabels',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
] as monacoTypes.Token[][],
|
||||||
|
position: {
|
||||||
|
lineNumber: 1,
|
||||||
|
column: 10,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { monacoTypes } from '@grafana/ui';
|
|||||||
|
|
||||||
import { Monaco } from '../../monarch/types';
|
import { Monaco } from '../../monarch/types';
|
||||||
import * as SQLTestData from '../cloudwatch-sql-test-data';
|
import * as SQLTestData from '../cloudwatch-sql-test-data';
|
||||||
|
import * as DynamicLabelTestData from '../dynamic-label-test-data';
|
||||||
import * as MetricMathTestData from '../metric-math-test-data';
|
import * as MetricMathTestData from '../metric-math-test-data';
|
||||||
|
|
||||||
// Stub for the Monaco instance.
|
// Stub for the Monaco instance.
|
||||||
@@ -30,6 +31,14 @@ const MonacoMock: Monaco = {
|
|||||||
};
|
};
|
||||||
return TestData[value];
|
return TestData[value];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (languageId === 'cloudwatch-dynamicLabels') {
|
||||||
|
const TestData = {
|
||||||
|
[DynamicLabelTestData.afterLabelValue.query]: DynamicLabelTestData.afterLabelValue.tokens,
|
||||||
|
[DynamicLabelTestData.insideLabelValue.query]: DynamicLabelTestData.insideLabelValue.tokens,
|
||||||
|
};
|
||||||
|
return TestData[value];
|
||||||
|
}
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
|
import React, { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
import { CodeEditor, getInputStyles, Monaco, useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { DynamicLabelsCompletionItemProvider } from '../dynamic-labels/CompletionItemProvider';
|
||||||
|
import language from '../dynamic-labels/definition';
|
||||||
|
import { TRIGGER_SUGGEST } from '../monarch/commands';
|
||||||
|
import { registerLanguage } from '../monarch/register';
|
||||||
|
|
||||||
|
const dynamicLabelsCompletionItemProvider = new DynamicLabelsCompletionItemProvider();
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onChange: (query: string) => void;
|
||||||
|
onRunQuery: () => void;
|
||||||
|
label: string;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props) {
|
||||||
|
const theme = useTheme2();
|
||||||
|
const styles = getInputStyles({ theme, width });
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const onEditorMount = useCallback(
|
||||||
|
(editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||||
|
editor.onDidFocusEditorText(() => editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {}));
|
||||||
|
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
|
||||||
|
const text = editor.getValue();
|
||||||
|
onChange(text);
|
||||||
|
onRunQuery();
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerDiv = containerRef.current;
|
||||||
|
containerDiv !== null && editor.layout({ width: containerDiv.clientWidth, height: containerDiv.clientHeight });
|
||||||
|
},
|
||||||
|
[onChange, onRunQuery]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className={cx(styles.wrapper)}>
|
||||||
|
<CodeEditor
|
||||||
|
containerStyles={css`
|
||||||
|
border: 1px solid ${theme.colors.action.disabledBackground};
|
||||||
|
&:hover {
|
||||||
|
border-color: ${theme.components.input.borderColor};
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
monacoOptions={{
|
||||||
|
// without this setting, the auto-resize functionality causes an infinite loop, don't remove it!
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
|
||||||
|
// These additional options are style focused and are a subset of those in the query editor in Prometheus
|
||||||
|
fontSize: 14,
|
||||||
|
lineNumbers: 'off',
|
||||||
|
renderLineHighlight: 'none',
|
||||||
|
overviewRulerLanes: 0,
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'hidden',
|
||||||
|
horizontal: 'hidden',
|
||||||
|
},
|
||||||
|
suggestFontSize: 12,
|
||||||
|
padding: {
|
||||||
|
top: 6,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
language={language.id}
|
||||||
|
value={label}
|
||||||
|
onBlur={(value) => {
|
||||||
|
if (value !== label) {
|
||||||
|
onChange(value);
|
||||||
|
onRunQuery();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBeforeEditorMount={(monaco: Monaco) =>
|
||||||
|
registerLanguage(monaco, language, dynamicLabelsCompletionItemProvider)
|
||||||
|
}
|
||||||
|
onEditorDidMount={onEditorMount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export function MathExpressionQueryField({
|
|||||||
const updateElementHeight = () => {
|
const updateElementHeight = () => {
|
||||||
const containerDiv = containerRef.current;
|
const containerDiv = containerRef.current;
|
||||||
if (containerDiv !== null && editor.getContentHeight() < 200) {
|
if (containerDiv !== null && editor.getContentHeight() < 200) {
|
||||||
const pixelHeight = editor.getContentHeight();
|
const pixelHeight = Math.max(32, editor.getContentHeight());
|
||||||
containerDiv.style.height = `${pixelHeight}px`;
|
containerDiv.style.height = `${pixelHeight}px`;
|
||||||
containerDiv.style.width = '100%';
|
containerDiv.style.width = '100%';
|
||||||
const pixelWidth = containerDiv.clientWidth;
|
const pixelWidth = containerDiv.clientWidth;
|
||||||
@@ -68,6 +68,9 @@ export function MathExpressionQueryField({
|
|||||||
},
|
},
|
||||||
suggestFontSize: 12,
|
suggestFontSize: 12,
|
||||||
wordWrap: 'on',
|
wordWrap: 'on',
|
||||||
|
padding: {
|
||||||
|
top: 6,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
language={language.id}
|
language={language.id}
|
||||||
value={query}
|
value={query}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import selectEvent from 'react-select-event';
|
|||||||
|
|
||||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
import * as ui from '@grafana/ui';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
import { CustomVariableModel, initialVariableModelState } from '../../../../../features/variables/types';
|
import { CustomVariableModel, initialVariableModelState } from '../../../../../features/variables/types';
|
||||||
@@ -12,6 +13,13 @@ import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../../typ
|
|||||||
|
|
||||||
import { MetricsQueryEditor, Props } from './MetricsQueryEditor';
|
import { MetricsQueryEditor, Props } from './MetricsQueryEditor';
|
||||||
|
|
||||||
|
jest.mock('@grafana/ui', () => ({
|
||||||
|
...jest.requireActual<typeof ui>('@grafana/ui'),
|
||||||
|
CodeEditor: function CodeEditor({ value }: { value: string }) {
|
||||||
|
return <pre>{value}</pre>;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const setup = () => {
|
const setup = () => {
|
||||||
const instanceSettings = {
|
const instanceSettings = {
|
||||||
jsonData: { defaultRegion: 'us-east-1' },
|
jsonData: { defaultRegion: 'us-east-1' },
|
||||||
@@ -173,9 +181,7 @@ describe('QueryEditor', () => {
|
|||||||
|
|
||||||
expect(screen.getByText('Label')).toBeInTheDocument();
|
expect(screen.getByText('Label')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('Alias')).toBeNull();
|
expect(screen.queryByText('Alias')).toBeNull();
|
||||||
expect(screen.getByLabelText('Label - optional')).toHaveValue(
|
expect(screen.getByText("Period: ${PROP('Period')} InstanceId: ${PROP('Dim.InstanceId')}"));
|
||||||
"Period: ${PROP('Period')} InstanceId: ${PROP('Dim.InstanceId')}"
|
|
||||||
);
|
|
||||||
|
|
||||||
config.featureToggles.cloudWatchDynamicLabels = originalValue;
|
config.featureToggles.cloudWatchDynamicLabels = originalValue;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
MetricQueryType,
|
MetricQueryType,
|
||||||
MetricStat,
|
MetricStat,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
import { DynamicLabelsField } from '../DynamicLabelsField';
|
||||||
import QueryHeader from '../QueryHeader';
|
import QueryHeader from '../QueryHeader';
|
||||||
|
|
||||||
import { Alias } from './Alias';
|
import { Alias } from './Alias';
|
||||||
@@ -138,14 +139,12 @@ export const MetricsQueryEditor = (props: Props) => {
|
|||||||
optional
|
optional
|
||||||
tooltip="Change time series legend name using Dynamic labels. See documentation for details."
|
tooltip="Change time series legend name using Dynamic labels. See documentation for details."
|
||||||
>
|
>
|
||||||
<Input
|
<DynamicLabelsField
|
||||||
id={`${query.refId}-cloudwatch-metric-query-editor-label`}
|
width={52}
|
||||||
onBlur={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
value={preparedQuery.label ?? ''}
|
label={preparedQuery.label ?? ''}
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
onChange={(label) => props.onChange({ ...query, label })}
|
||||||
onChange({ ...preparedQuery, label: event.target.value })
|
></DynamicLabelsField>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</EditorField>
|
</EditorField>
|
||||||
) : (
|
) : (
|
||||||
<EditorField
|
<EditorField
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { CompletionItemPriority } from '@grafana/experimental';
|
||||||
|
import { Monaco, monacoTypes } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { afterLabelValue, insideLabelValue } from '../__mocks__/dynamic-label-test-data';
|
||||||
|
import MonacoMock from '../__mocks__/monarch/Monaco';
|
||||||
|
import TextModel from '../__mocks__/monarch/TextModel';
|
||||||
|
|
||||||
|
import { DynamicLabelsCompletionItemProvider } from './CompletionItemProvider';
|
||||||
|
import cloudWatchDynamicLabelsLanguageDefinition from './definition';
|
||||||
|
import { DYNAMIC_LABEL_PATTERNS } from './language';
|
||||||
|
|
||||||
|
const getSuggestions = async (value: string, position: monacoTypes.IPosition) => {
|
||||||
|
const setup = new DynamicLabelsCompletionItemProvider();
|
||||||
|
const monaco = MonacoMock as Monaco;
|
||||||
|
const provider = setup.getCompletionProvider(monaco, cloudWatchDynamicLabelsLanguageDefinition);
|
||||||
|
const { suggestions } = await provider.provideCompletionItems(
|
||||||
|
TextModel(value) as monacoTypes.editor.ITextModel,
|
||||||
|
position
|
||||||
|
);
|
||||||
|
return suggestions;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Dynamic labels: CompletionItemProvider', () => {
|
||||||
|
describe('getSuggestions', () => {
|
||||||
|
it('returns all dynamic labels in case current token is a whitespace', async () => {
|
||||||
|
const { query, position } = afterLabelValue;
|
||||||
|
const suggestions = await getSuggestions(query, position);
|
||||||
|
expect(suggestions.length).toEqual(DYNAMIC_LABEL_PATTERNS.length + 1); // + 1 for the dimension suggestions
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return suggestion for dimension label that has high prio', async () => {
|
||||||
|
const { query, position } = afterLabelValue;
|
||||||
|
const suggestions = await getSuggestions(query, position);
|
||||||
|
expect(suggestions.length).toBeTruthy();
|
||||||
|
const highPrioSuggestsions = suggestions.filter((s) => s.sortText === CompletionItemPriority.High);
|
||||||
|
expect(highPrioSuggestsions.length).toBe(1);
|
||||||
|
expect(highPrioSuggestsions[0].label).toBe("${PROP('Dim.')}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doesnt return suggestions if cursor is inside a dynamic label', async () => {
|
||||||
|
const { query, position } = insideLabelValue;
|
||||||
|
const suggestions = await getSuggestions(query, position);
|
||||||
|
expect(suggestions.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { Monaco, monacoTypes } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { linkedTokenBuilder } from '../monarch/linkedTokenBuilder';
|
||||||
|
import { LanguageDefinition } from '../monarch/register';
|
||||||
|
import { Completeable, CompletionItemPriority, TokenTypes } from '../monarch/types';
|
||||||
|
|
||||||
|
import { DYNAMIC_LABEL_PATTERNS } from './language';
|
||||||
|
|
||||||
|
type CompletionItem = monacoTypes.languages.CompletionItem;
|
||||||
|
|
||||||
|
export class DynamicLabelsCompletionItemProvider implements Completeable {
|
||||||
|
tokenTypes: TokenTypes;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.tokenTypes = {
|
||||||
|
Parenthesis: 'delimiter.parenthesis.cloudwatch-dynamicLabels',
|
||||||
|
Whitespace: 'white.cloudwatch-dynamicLabels',
|
||||||
|
Keyword: 'keyword.cloudwatch-dynamicLabels',
|
||||||
|
Delimiter: 'delimiter.cloudwatch-dynamicLabels',
|
||||||
|
Operator: 'operator.cloudwatch-dynamicLabels',
|
||||||
|
Identifier: 'identifier.cloudwatch-dynamicLabels',
|
||||||
|
Type: 'type.cloudwatch-dynamicLabels',
|
||||||
|
Function: 'predefined.cloudwatch-dynamicLabels',
|
||||||
|
Number: 'number.cloudwatch-dynamicLabels',
|
||||||
|
String: 'string.cloudwatch-dynamicLabels',
|
||||||
|
Variable: 'variable.cloudwatch-dynamicLabels',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// called by registerLanguage and passed to monaco with registerCompletionItemProvider
|
||||||
|
// returns an object that implements https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.CompletionItemProvider.html
|
||||||
|
getCompletionProvider(monaco: Monaco, languageDefinition: LanguageDefinition) {
|
||||||
|
return {
|
||||||
|
triggerCharacters: [' ', '$', ',', '(', "'"], // one of these characters indicates that it is time to look for a suggestion
|
||||||
|
provideCompletionItems: async (model: monacoTypes.editor.ITextModel, position: monacoTypes.IPosition) => {
|
||||||
|
const currentToken = linkedTokenBuilder(monaco, languageDefinition, model, position, this.tokenTypes);
|
||||||
|
const invalidRangeToken = currentToken?.isWhiteSpace() || currentToken?.isParenthesis();
|
||||||
|
const range =
|
||||||
|
invalidRangeToken || !currentToken?.range ? monaco.Range.fromPositions(position) : currentToken?.range;
|
||||||
|
const toCompletionItem = (value: string, rest: Partial<CompletionItem> = {}) => {
|
||||||
|
const item: CompletionItem = {
|
||||||
|
label: value,
|
||||||
|
insertText: value,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Field,
|
||||||
|
range,
|
||||||
|
sortText: CompletionItemPriority.Medium,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
let suggestions: CompletionItem[] = [];
|
||||||
|
const next = currentToken?.next;
|
||||||
|
if (!currentToken?.isFunction() && (!next || next.isWhiteSpace())) {
|
||||||
|
suggestions = DYNAMIC_LABEL_PATTERNS.map((val) => toCompletionItem(val));
|
||||||
|
// always insert suggestion for dimension value and allow user to complete pattern by providing the dimension name
|
||||||
|
suggestions.push(
|
||||||
|
toCompletionItem("${PROP('Dim.')}", {
|
||||||
|
sortText: CompletionItemPriority.High,
|
||||||
|
insertText: `\${PROP('Dim.$0')} `,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { LanguageDefinition } from '../monarch/register';
|
||||||
|
|
||||||
|
const cloudWatchDynamicLabelsLanguageDefinition: LanguageDefinition = {
|
||||||
|
id: 'cloudwatch-dynamicLabels',
|
||||||
|
extensions: [],
|
||||||
|
aliases: [],
|
||||||
|
mimetypes: [],
|
||||||
|
loader: () => import('./language'),
|
||||||
|
};
|
||||||
|
export default cloudWatchDynamicLabelsLanguageDefinition;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
|
|
||||||
|
// Dynamic labels: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/graph-dynamic-labels.html
|
||||||
|
export const DYNAMIC_LABEL_PATTERNS = [
|
||||||
|
'${DATAPOINT_COUNT}',
|
||||||
|
'${FIRST}',
|
||||||
|
'${FIRST_LAST_RANGE}',
|
||||||
|
'${FIRST_LAST_TIME_RANGE}',
|
||||||
|
'${FIRST_TIME}',
|
||||||
|
'${FIRST_TIME_RELATIVE}',
|
||||||
|
'${LABEL}',
|
||||||
|
'${LAST}',
|
||||||
|
'${LAST_TIME}',
|
||||||
|
'${LAST_TIME_RELATIVE}',
|
||||||
|
'${MAX}',
|
||||||
|
'${MAX_TIME}',
|
||||||
|
'${MAX_TIME_RELATIVE}',
|
||||||
|
'${MIN}',
|
||||||
|
'${MIN_MAX_RANGE}',
|
||||||
|
'${MIN_MAX_TIME_RANGE}',
|
||||||
|
'${MIN_TIME}',
|
||||||
|
'${MIN_TIME_RELATIVE}',
|
||||||
|
"${PROP('AccountId')}",
|
||||||
|
"${PROP('MetricName')}",
|
||||||
|
"${PROP('Namespace')}",
|
||||||
|
"${PROP('Period')}",
|
||||||
|
"${PROP('Region')}",
|
||||||
|
"${PROP('Stat')}",
|
||||||
|
'${SUM}',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const language: monacoType.languages.IMonarchLanguage = {
|
||||||
|
id: 'dynamicLabels',
|
||||||
|
ignoreCase: false,
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
{ include: '@whitespace' },
|
||||||
|
{ include: '@builtInFunctions' },
|
||||||
|
{ include: '@string' },
|
||||||
|
[/\$\{PROP\('Dim.[a-zA-Z0-9-_]?.*'\)\}+/, 'predefined'], //custom handling for dimension patterns
|
||||||
|
],
|
||||||
|
builtInFunctions: [[DYNAMIC_LABEL_PATTERNS.map(escapeRegExp).join('|'), 'predefined']],
|
||||||
|
whitespace: [[/\s+/, 'white']],
|
||||||
|
string: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const conf: monacoType.languages.LanguageConfiguration = {};
|
||||||
|
|
||||||
|
function escapeRegExp(string: string) {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { CloudWatchDatasource } from '../datasource';
|
|||||||
import { LinkedToken } from './LinkedToken';
|
import { LinkedToken } from './LinkedToken';
|
||||||
import { linkedTokenBuilder } from './linkedTokenBuilder';
|
import { linkedTokenBuilder } from './linkedTokenBuilder';
|
||||||
import { LanguageDefinition } from './register';
|
import { LanguageDefinition } from './register';
|
||||||
import { StatementPosition, SuggestionKind, TokenTypes } from './types';
|
import { Completeable, StatementPosition, SuggestionKind, TokenTypes } from './types';
|
||||||
|
|
||||||
type CompletionItem = monacoTypes.languages.CompletionItem;
|
type CompletionItem = monacoTypes.languages.CompletionItem;
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ CompletionItemProvider is an extendable class which needs to implement :
|
|||||||
- getSuggestionKinds
|
- getSuggestionKinds
|
||||||
- getSuggestions
|
- getSuggestions
|
||||||
*/
|
*/
|
||||||
export class CompletionItemProvider {
|
export class CompletionItemProvider implements Completeable {
|
||||||
templateVariables: string[];
|
templateVariables: string[];
|
||||||
datasource: CloudWatchDatasource;
|
datasource: CloudWatchDatasource;
|
||||||
templateSrv: TemplateSrv;
|
templateSrv: TemplateSrv;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
|
|||||||
|
|
||||||
import { Monaco } from '@grafana/ui';
|
import { Monaco } from '@grafana/ui';
|
||||||
|
|
||||||
import { CompletionItemProvider } from './CompletionItemProvider';
|
import { Completeable } from './types';
|
||||||
|
|
||||||
export type LanguageDefinition = {
|
export type LanguageDefinition = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,7 +18,7 @@ export type LanguageDefinition = {
|
|||||||
export const registerLanguage = (
|
export const registerLanguage = (
|
||||||
monaco: Monaco,
|
monaco: Monaco,
|
||||||
language: LanguageDefinition,
|
language: LanguageDefinition,
|
||||||
completionItemProvider: CompletionItemProvider
|
completionItemProvider: Completeable
|
||||||
) => {
|
) => {
|
||||||
const { id, loader } = language;
|
const { id, loader } = language;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { monacoTypes } from '@grafana/ui';
|
import { monacoTypes } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { LanguageDefinition } from './register';
|
||||||
|
|
||||||
export interface TokenTypes {
|
export interface TokenTypes {
|
||||||
Parenthesis: string;
|
Parenthesis: string;
|
||||||
Whitespace: string;
|
Whitespace: string;
|
||||||
@@ -98,3 +100,10 @@ export interface Monaco {
|
|||||||
Range: Range;
|
Range: Range;
|
||||||
languages: Languages;
|
languages: Languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Completeable {
|
||||||
|
getCompletionProvider(
|
||||||
|
monaco: Monaco,
|
||||||
|
languageDefinition: LanguageDefinition
|
||||||
|
): monacoTypes.languages.CompletionItemProvider;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user