mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 16:15:42 -06:00
Alerting: Adds contact point template syntax highlighting (#51559)
This commit is contained in:
parent
cfa877b33a
commit
a1fb73c503
@ -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"],
|
||||
|
@ -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 { 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;
|
||||
`,
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user