mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Adds contact point template syntax highlighting (#51559)
This commit is contained in:
@@ -4271,9 +4271,9 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx:3093815801": [
|
"public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx:3093815801": [
|
||||||
[126, 10, 1271, "Do not use any type assertions.", "441563991"]
|
[126, 10, 1271, "Do not use any type assertions.", "441563991"]
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/receivers/TemplateForm.tsx:312681720": [
|
"public/app/features/alerting/unified/components/receivers/TemplateForm.tsx:2509382344": [
|
||||||
[101, 29, 12, "Do not use any type assertions.", "3304544793"],
|
[106, 29, 12, "Do not use any type assertions.", "3304544793"],
|
||||||
[101, 38, 3, "Unexpected any. Specify a different type.", "193409811"]
|
[106, 38, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx:2192936556": [
|
"public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx:2192936556": [
|
||||||
[31, 28, 30, "Do not use any type assertions.", "1841535999"],
|
[31, 28, 30, "Do not use any type assertions.", "1841535999"],
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -2,9 +2,10 @@ import { css } from '@emotion/css';
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useForm, Validate } from 'react-hook-form';
|
import { useForm, Validate } from 'react-hook-form';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
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 { useCleanup } from 'app/core/hooks/useCleanup';
|
||||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ import { makeAMLink } from '../../utils/misc';
|
|||||||
import { ensureDefine } from '../../utils/templates';
|
import { ensureDefine } from '../../utils/templates';
|
||||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||||
|
|
||||||
|
import { TemplateEditor } from './TemplateEditor';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
@@ -83,6 +86,8 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
} = useForm<Values>({
|
} = useForm<Values>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
defaultValues: existing ?? defaults,
|
defaultValues: existing ?? defaults,
|
||||||
@@ -143,12 +148,18 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
|
|||||||
invalid={!!errors.content?.message}
|
invalid={!!errors.content?.message}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<TextArea
|
<div className={styles.editWrapper}>
|
||||||
{...register('content', { required: { value: true, message: 'Required.' } })}
|
<AutoSizer>
|
||||||
className={styles.textarea}
|
{({ width, height }) => (
|
||||||
placeholder="Message"
|
<TemplateEditor
|
||||||
rows={12}
|
value={getValues('content')}
|
||||||
/>
|
width={width}
|
||||||
|
height={height}
|
||||||
|
onBlur={(value) => setValue('content', value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -189,4 +200,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
textarea: css`
|
textarea: css`
|
||||||
max-width: 758px;
|
max-width: 758px;
|
||||||
`,
|
`,
|
||||||
|
editWrapper: css`
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 640px;
|
||||||
|
height: 320px;
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { ProvisioningBadge } from '../Provisioning';
|
|||||||
import { ActionIcon } from '../rules/ActionIcon';
|
import { ActionIcon } from '../rules/ActionIcon';
|
||||||
|
|
||||||
import { ReceiversSection } from './ReceiversSection';
|
import { ReceiversSection } from './ReceiversSection';
|
||||||
|
import { TemplateEditor } from './TemplateEditor';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
config: AlertManagerCortexConfig;
|
config: AlertManagerCortexConfig;
|
||||||
@@ -128,7 +129,17 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
|||||||
<td></td>
|
<td></td>
|
||||||
<td colSpan={2}>
|
<td colSpan={2}>
|
||||||
<DetailsField label="Description" horizontal={true}>
|
<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>
|
</DetailsField>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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: "'" },
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -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));
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user