Alerting: Adds contact point template syntax highlighting (#51559)

This commit is contained in:
Gilles De Mey 2022-06-30 14:01:02 +02:00 committed by GitHub
parent cfa877b33a
commit a1fb73c503
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 266 additions and 11 deletions

View File

@ -4271,9 +4271,9 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx:3093815801": [
[126, 10, 1271, "Do not use any type assertions.", "441563991"]
],
"public/app/features/alerting/unified/components/receivers/TemplateForm.tsx:312681720": [
[101, 29, 12, "Do not use any type assertions.", "3304544793"],
[101, 38, 3, "Unexpected any. Specify a different type.", "193409811"]
"public/app/features/alerting/unified/components/receivers/TemplateForm.tsx:2509382344": [
[106, 29, 12, "Do not use any type assertions.", "3304544793"],
[106, 38, 3, "Unexpected any. Specify a different type.", "193409811"]
],
"public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx:2192936556": [
[31, 28, 30, "Do not use any type assertions.", "1841535999"],

View File

@ -0,0 +1,53 @@
/**
* This file contains the template editor we'll be using for alertmanager templates.
*
* It includes auto-complete for template data and syntax highlighting
*/
import { editor } from 'monaco-editor';
import React, { FC } from 'react';
import { CodeEditor } from '@grafana/ui';
import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types';
import goTemplateLanguageDefinition, { GO_TEMPLATE_LANGUAGE_ID } from './editor/definition';
import { registerLanguage } from './editor/register';
const getSuggestions = () => {
return [];
};
type TemplateEditorProps = Omit<CodeEditorProps, 'language' | 'theme'> & {
autoHeight?: boolean;
};
const TemplateEditor: FC<TemplateEditorProps> = (props) => {
const shouldAutoHeight = Boolean(props.autoHeight);
const onEditorDidMount = (editor: editor.IStandaloneCodeEditor) => {
if (shouldAutoHeight) {
const contentHeight = editor.getContentHeight();
try {
// we're passing NaN in to the width because the type definition wants a number (NaN is a number, go figure)
// but the width could be defined as a string "auto", passing NaN seems to just ignore our width update here
editor.layout({ height: contentHeight, width: NaN });
} catch (err) {}
}
};
return (
<CodeEditor
showLineNumbers={true}
getSuggestions={getSuggestions}
showMiniMap={false}
{...props}
onEditorDidMount={onEditorDidMount}
onBeforeEditorMount={(monaco) => {
registerLanguage(monaco, goTemplateLanguageDefinition);
}}
language={GO_TEMPLATE_LANGUAGE_ID}
/>
);
};
export { TemplateEditor };

View File

@ -2,9 +2,10 @@ import { css } from '@emotion/css';
import React, { FC } from 'react';
import { useForm, Validate } from 'react-hook-form';
import { useDispatch } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Button, Field, FieldSet, Input, LinkButton, TextArea, useStyles2 } from '@grafana/ui';
import { Alert, Button, Field, FieldSet, Input, LinkButton, useStyles2 } from '@grafana/ui';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
@ -14,6 +15,8 @@ import { makeAMLink } from '../../utils/misc';
import { ensureDefine } from '../../utils/templates';
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
import { TemplateEditor } from './TemplateEditor';
interface Values {
name: string;
content: string;
@ -83,6 +86,8 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
handleSubmit,
register,
formState: { errors },
getValues,
setValue,
} = useForm<Values>({
mode: 'onSubmit',
defaultValues: existing ?? defaults,
@ -143,12 +148,18 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
invalid={!!errors.content?.message}
required
>
<TextArea
{...register('content', { required: { value: true, message: 'Required.' } })}
className={styles.textarea}
placeholder="Message"
rows={12}
/>
<div className={styles.editWrapper}>
<AutoSizer>
{({ width, height }) => (
<TemplateEditor
value={getValues('content')}
width={width}
height={height}
onBlur={(value) => setValue('content', value)}
/>
)}
</AutoSizer>
</div>
</Field>
<div className={styles.buttons}>
{loading && (
@ -189,4 +200,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
textarea: css`
max-width: 758px;
`,
editWrapper: css`
display: block;
position: relative;
width: 640px;
height: 320px;
`,
});

View File

@ -16,6 +16,7 @@ import { ProvisioningBadge } from '../Provisioning';
import { ActionIcon } from '../rules/ActionIcon';
import { ReceiversSection } from './ReceiversSection';
import { TemplateEditor } from './TemplateEditor';
interface Props {
config: AlertManagerCortexConfig;
@ -128,7 +129,17 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
<td></td>
<td colSpan={2}>
<DetailsField label="Description" horizontal={true}>
<pre>{template}</pre>
<TemplateEditor
width={'auto'}
height={'auto'}
autoHeight={true}
value={template}
showLineNumbers={false}
monacoOptions={{
readOnly: true,
scrollBeyondLastLine: false,
}}
/>
</DetailsField>
</td>
</tr>

View File

@ -0,0 +1,12 @@
import { LanguageDefinition } from './register';
export const GO_TEMPLATE_LANGUAGE_ID = 'go-template';
const goTemplateLanguageDefinition: LanguageDefinition = {
id: GO_TEMPLATE_LANGUAGE_ID,
extensions: [],
aliases: [],
mimetypes: [],
loader: () => import('./language'),
};
export default goTemplateLanguageDefinition;

View File

@ -0,0 +1,128 @@
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
// these map to the builtin token types
enum TokenType {
Delimiter = 'delimiter',
Keyword = 'keyword',
Function = 'type.identifier',
String = 'string',
Variable = 'variable.name',
Number = 'number',
Comment = 'comment',
Operator = 'operator',
Identifier = 'idenfifier',
}
// list of available functions in Alertmanager templates
// see https://cs.github.com/prometheus/alertmanager/blob/805e505288ce82c3e2b625a3ca63aaf2b0aa9cea/template/template.go?q=join#L132-L151
const availableAlertManagerFunctions = [
'toUpper',
'toLower',
'title',
'join',
'match',
'safeHtml',
'reReplaceAll',
'stringSlice',
];
// built-in functions for Go templates
const builtinFunctions = [
'and',
'call',
'html',
'index',
'slice',
'js',
'len',
'not',
'or',
'print',
'printf',
'println',
'urlquery',
];
// boolean functions
const booleanFunctions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge'];
// Go template keywords
const keywords = ['define', 'if', 'else', 'end', 'range', 'break', 'continue', 'template', 'block', 'with'];
// Monarch language definition, see https://microsoft.github.io/monaco-editor/monarch.html
// check https://github.com/microsoft/monaco-editor/blob/main/src/basic-languages/go/go.ts for an example
// see https://pkg.go.dev/text/template for the available keywords etc
export const language: monacoType.languages.IMonarchLanguage = {
defaultToken: '', // change this to "invalid" to find tokens that were never matched
keywords: keywords,
functions: [...builtinFunctions, ...booleanFunctions, ...availableAlertManagerFunctions],
operators: ['|'],
tokenizer: {
root: [
// strings
[/"/, TokenType.String, '@string'],
[/`/, TokenType.String, '@rawstring'],
// numbers
[/\d*\d+[eE]([\-+]?\d+)?/, 'number.float'],
[/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'],
[/0[xX][0-9a-fA-F']*[0-9a-fA-F]/, 'number.hex'],
[/0[0-7']*[0-7]/, 'number.octal'],
[/0[bB][0-1']*[0-1]/, 'number.binary'],
[/\d[\d']*/, TokenType.Number],
[/\d/, TokenType.Number],
// delimiter: after number because of .\d floats
[/[;,.]/, TokenType.Delimiter],
// delimiters
[/{{-?/, TokenType.Delimiter],
[/-?}}/, TokenType.Delimiter],
// variables
// [/\.([A-Za-z]+)?/, TokenType.Variable],
// identifiers and keywords
[
/[a-zA-Z_]\w*/,
{
cases: {
'@keywords': { token: TokenType.Keyword },
'@functions': { token: TokenType.Function },
'@default': TokenType.Identifier,
},
},
],
// comments
[/\/\*.*\*\//, TokenType.Comment],
],
string: [
[/[^\\"]+/, TokenType.String],
[/\\./, 'string.escape.invalid'],
[/"/, TokenType.String, '@pop'],
],
rawstring: [
[/[^\`]/, TokenType.String],
[/`/, TokenType.String, '@pop'],
],
},
};
export const conf: monacoType.languages.LanguageConfiguration = {
comments: {
blockComment: ['/*', '*/'],
},
brackets: [
['{{', '}}'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{{', close: '}}' },
{ open: '(', close: ')' },
{ open: '`', close: '`' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: '{{', close: '}}' },
{ open: '(', close: ')' },
{ open: '`', close: '`' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
};

View File

@ -0,0 +1,34 @@
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
import { Monaco } from '@grafana/ui';
export type LanguageDefinition = {
id: string;
extensions: string[];
aliases: string[];
mimetypes: string[];
loader: () => Promise<{
language: monacoType.languages.IMonarchLanguage;
conf: monacoType.languages.LanguageConfiguration;
}>;
};
export const registerLanguage = (
monaco: Monaco,
language: LanguageDefinition
// completionItemProvider: Completeable
) => {
const { id, loader } = language;
const languages = monaco.languages.getLanguages();
if (languages.find((l) => l.id === id)) {
return;
}
monaco.languages.register({ id });
loader().then((monarch) => {
monaco.languages.setMonarchTokensProvider(id, monarch.language);
monaco.languages.setLanguageConfiguration(id, monarch.conf);
// monaco.languages.registerCompletionItemProvider(id, completionItemProvider.getCompletionProvider(monaco, language));
});
};