mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add templates autocomplete (#53655)
Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com>
This commit is contained in:
parent
43a1d1484d
commit
4c4b758646
@ -0,0 +1,149 @@
|
||||
export interface TemplateDataItem {
|
||||
name: string;
|
||||
type: 'string' | '[]Alert' | 'KeyValue' | 'time.Time';
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface TemplateFunctionItem {
|
||||
name: string;
|
||||
args?: '[]string';
|
||||
returns: 'KeyValue' | '[]string';
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export const GlobalTemplateData: TemplateDataItem[] = [
|
||||
{
|
||||
name: 'Receiver',
|
||||
type: 'string',
|
||||
notes: 'Name of the contact point that the notification is being sent to.',
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
type: 'string',
|
||||
notes: 'firing if at least one alert is firing, otherwise resolved',
|
||||
},
|
||||
{
|
||||
name: 'Alerts',
|
||||
type: '[]Alert',
|
||||
notes: 'List of alert objects that are included in this notification.',
|
||||
},
|
||||
{
|
||||
name: 'Alerts.Firing',
|
||||
type: '[]Alert',
|
||||
notes: 'List of firing alerts',
|
||||
},
|
||||
{
|
||||
name: 'Alerts.Resolved',
|
||||
type: '[]Alert',
|
||||
notes: 'List of resolved alerts',
|
||||
},
|
||||
{
|
||||
name: 'GroupLabels',
|
||||
type: 'KeyValue',
|
||||
notes: 'Labels these alerts were grouped by.',
|
||||
},
|
||||
{
|
||||
name: 'CommonLabels',
|
||||
type: 'KeyValue',
|
||||
notes: 'Labels common to all the alerts included in this notification.',
|
||||
},
|
||||
{
|
||||
name: 'CommonAnnotations',
|
||||
type: 'KeyValue',
|
||||
notes: 'Annotations common to all the alerts included in this notification.',
|
||||
},
|
||||
{
|
||||
name: 'ExternalURL',
|
||||
type: 'string',
|
||||
notes: 'Back link to the Grafana that sent the notification.',
|
||||
},
|
||||
];
|
||||
|
||||
export const AlertTemplateData: TemplateDataItem[] = [
|
||||
{
|
||||
name: 'Status',
|
||||
type: 'string',
|
||||
notes: 'firing or resolved.',
|
||||
},
|
||||
{
|
||||
name: 'Labels',
|
||||
type: 'KeyValue',
|
||||
notes: 'Set of labels attached to the alert.',
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
type: 'KeyValue',
|
||||
notes: 'Set of annotations attached to the alert.',
|
||||
},
|
||||
{
|
||||
name: 'StartsAt',
|
||||
type: 'time.Time',
|
||||
notes: 'Time the alert started firing.',
|
||||
},
|
||||
{
|
||||
name: 'EndsAt',
|
||||
type: 'time.Time',
|
||||
notes:
|
||||
'Only set if the end time of an alert is known. Otherwise set to a configurable timeout period from the time since the last alert was received.',
|
||||
},
|
||||
{
|
||||
name: 'GeneratorURL',
|
||||
type: 'string',
|
||||
notes: 'A back link to Grafana or external Alertmanager.',
|
||||
},
|
||||
{
|
||||
name: 'SilenceURL',
|
||||
type: 'string',
|
||||
notes: 'Link to Grafana silence for with labels for this alert pre-filled. Only for Grafana managed alerts.',
|
||||
},
|
||||
{
|
||||
name: 'DashboardURL',
|
||||
type: 'string',
|
||||
notes: 'Link to Grafana dashboard, if alert rule belongs to one. Only for Grafana managed alerts.',
|
||||
},
|
||||
{
|
||||
name: 'PanelURL',
|
||||
type: 'string',
|
||||
notes: 'Link to Grafana dashboard panel, if alert rule belongs to one. Only for Grafana managed alerts.',
|
||||
},
|
||||
{
|
||||
name: 'Fingerprint',
|
||||
type: 'string',
|
||||
notes: 'Fingerprint that can be used to identify the alert.',
|
||||
},
|
||||
{
|
||||
name: 'ValueString',
|
||||
type: 'string',
|
||||
notes: 'String that contains the labels and value of each reduced expression in the alert.',
|
||||
},
|
||||
];
|
||||
|
||||
export const KeyValueTemplateFunctions: TemplateFunctionItem[] = [
|
||||
{
|
||||
name: 'SortedPairs',
|
||||
returns: 'KeyValue',
|
||||
notes: 'Returns sorted list of key & value string pairs',
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
args: '[]string',
|
||||
returns: 'KeyValue',
|
||||
notes: 'Returns a copy of the Key/Value map without the given keys.',
|
||||
},
|
||||
{
|
||||
name: 'Names',
|
||||
returns: '[]string',
|
||||
notes: 'List of label names',
|
||||
},
|
||||
{
|
||||
name: 'Values',
|
||||
returns: '[]string',
|
||||
notes: 'List of label values',
|
||||
},
|
||||
];
|
||||
|
||||
export const KeyValueCodeSnippet = `{
|
||||
"summary": "alert summary",
|
||||
"description": "alert description"
|
||||
}
|
||||
`;
|
@ -0,0 +1,164 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { HoverCard } from '../HoverCard';
|
||||
|
||||
import {
|
||||
AlertTemplateData,
|
||||
GlobalTemplateData,
|
||||
KeyValueCodeSnippet,
|
||||
KeyValueTemplateFunctions,
|
||||
TemplateDataItem,
|
||||
} from './TemplateData';
|
||||
|
||||
export function TemplateDataDocs() {
|
||||
const styles = useStyles2(getTemplateDataDocsStyles);
|
||||
|
||||
const AlertTemplateDataTable = (
|
||||
<TemplateDataTable
|
||||
caption={
|
||||
<h4 className={styles.header}>
|
||||
Alert template data <span>Available only when in the context of an Alert (e.g. inside .Alerts loop)</span>
|
||||
</h4>
|
||||
}
|
||||
dataItems={AlertTemplateData}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack gap={2} flexGrow={1}>
|
||||
<TemplateDataTable
|
||||
caption={<h4 className={styles.header}>Template Data</h4>}
|
||||
dataItems={GlobalTemplateData}
|
||||
typeRenderer={(type) =>
|
||||
type === '[]Alert' ? (
|
||||
<HoverCard content={AlertTemplateDataTable}>
|
||||
<div className={styles.interactiveType}>{type}</div>
|
||||
</HoverCard>
|
||||
) : type === 'KeyValue' ? (
|
||||
<HoverCard content={<KeyValueTemplateDataTable />}>
|
||||
<div className={styles.interactiveType}>{type}</div>
|
||||
</HoverCard>
|
||||
) : (
|
||||
type
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const getTemplateDataDocsStyles = (theme: GrafanaTheme2) => ({
|
||||
header: css`
|
||||
color: ${theme.colors.text.primary};
|
||||
|
||||
span {
|
||||
color: ${theme.colors.text.secondary};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
}
|
||||
`,
|
||||
interactiveType: css`
|
||||
color: ${theme.colors.text.link};
|
||||
`,
|
||||
});
|
||||
|
||||
interface TemplateDataTableProps {
|
||||
dataItems: TemplateDataItem[];
|
||||
caption: JSX.Element | string;
|
||||
typeRenderer?: (type: TemplateDataItem['type']) => React.ReactNode;
|
||||
}
|
||||
|
||||
function TemplateDataTable({ dataItems, caption, typeRenderer }: TemplateDataTableProps) {
|
||||
const styles = useStyles2(getTemplateDataTableStyles);
|
||||
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<caption>{caption}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dataItems.map(({ name, type, notes }, index) => (
|
||||
<tr key={index}>
|
||||
<td>{name}</td>
|
||||
<td>{typeRenderer ? typeRenderer(type) : type}</td>
|
||||
<td>{notes}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyValueTemplateDataTable() {
|
||||
const tableStyles = useStyles2(getTemplateDataTableStyles);
|
||||
|
||||
return (
|
||||
<div>
|
||||
KeyValue is a set of key/value string pairs that represent labels and annotations.
|
||||
<pre>
|
||||
<code>{KeyValueCodeSnippet}</code>
|
||||
</pre>
|
||||
<table className={tableStyles.table}>
|
||||
<caption>Key-value methods</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Arguments</th>
|
||||
<th>Returns</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{KeyValueTemplateFunctions.map(({ name, args, returns, notes }) => (
|
||||
<tr key={name}>
|
||||
<td>{name}</td>
|
||||
<td>{args}</td>
|
||||
<td>{returns}</td>
|
||||
<td>{notes}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getTemplateDataTableStyles = (theme: GrafanaTheme2) => ({
|
||||
table: css`
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
caption {
|
||||
caption-side: top;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: ${theme.spacing(1, 1)};
|
||||
}
|
||||
|
||||
thead {
|
||||
font-weight: ${theme.typography.fontWeightBold};
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n + 1) {
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
}
|
||||
|
||||
tbody td:nth-child(1) {
|
||||
font-weight: ${theme.typography.fontWeightBold};
|
||||
}
|
||||
|
||||
tbody td:nth-child(2) {
|
||||
font-style: italic;
|
||||
}
|
||||
`,
|
||||
});
|
@ -3,25 +3,23 @@
|
||||
*
|
||||
* It includes auto-complete for template data and syntax highlighting
|
||||
*/
|
||||
import { editor } from 'monaco-editor';
|
||||
import React, { FC } from 'react';
|
||||
import { editor, IDisposable } from 'monaco-editor';
|
||||
import React, { FC, useEffect, useRef } from 'react';
|
||||
|
||||
import { CodeEditor } from '@grafana/ui';
|
||||
import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types';
|
||||
|
||||
import { registerGoTemplateAutocomplete } from './editor/autocomplete';
|
||||
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 disposeSuggestions = useRef<IDisposable | null>(null);
|
||||
|
||||
const onEditorDidMount = (editor: editor.IStandaloneCodeEditor) => {
|
||||
if (shouldAutoHeight) {
|
||||
@ -35,15 +33,21 @@ const TemplateEditor: FC<TemplateEditorProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
disposeSuggestions.current?.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
showLineNumbers={true}
|
||||
getSuggestions={getSuggestions}
|
||||
showMiniMap={false}
|
||||
{...props}
|
||||
onEditorDidMount={onEditorDidMount}
|
||||
onBeforeEditorMount={(monaco) => {
|
||||
registerLanguage(monaco, goTemplateLanguageDefinition);
|
||||
disposeSuggestions.current = registerGoTemplateAutocomplete(monaco);
|
||||
}}
|
||||
language={GO_TEMPLATE_LANGUAGE_ID}
|
||||
/>
|
||||
|
@ -4,7 +4,7 @@ import { useForm, Validate } from 'react-hook-form';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Button, Field, FieldSet, Input, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Button, Field, FieldSet, Input, LinkButton, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
@ -16,7 +16,9 @@ import { initialAsyncRequestState } from '../../utils/redux';
|
||||
import { ensureDefine } from '../../utils/templates';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
|
||||
import { TemplateDataDocs } from './TemplateDataDocs';
|
||||
import { TemplateEditor } from './TemplateEditor';
|
||||
import { snippets } from './editor/templateDataSuggestions';
|
||||
|
||||
interface Values {
|
||||
name: string;
|
||||
@ -121,77 +123,104 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
|
||||
autoFocus={true}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
description={
|
||||
<>
|
||||
You can use the{' '}
|
||||
<a
|
||||
href="https://pkg.go.dev/text/template?utm_source=godoc"
|
||||
target="__blank"
|
||||
rel="noreferrer"
|
||||
className={styles.externalLink}
|
||||
>
|
||||
Go templating language
|
||||
</a>
|
||||
.{' '}
|
||||
<a
|
||||
href="https://prometheus.io/blog/2016/03/03/custom-alertmanager-templates/"
|
||||
target="__blank"
|
||||
rel="noreferrer"
|
||||
className={styles.externalLink}
|
||||
>
|
||||
More info about alertmanager templates
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
label="Content"
|
||||
error={errors?.content?.message}
|
||||
invalid={!!errors.content?.message}
|
||||
required
|
||||
>
|
||||
<div className={styles.editWrapper}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<TemplateEditor
|
||||
value={getValues('content')}
|
||||
width={width}
|
||||
height={height}
|
||||
onBlur={(value) => setValue('content', value)}
|
||||
/>
|
||||
<TemplatingGuideline />
|
||||
<div className={styles.contentContainer}>
|
||||
<div>
|
||||
<Field label="Content" error={errors?.content?.message} invalid={!!errors.content?.message} required>
|
||||
<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 && (
|
||||
<Button disabled={true} icon="fa fa-spinner" variant="primary">
|
||||
Saving...
|
||||
</Button>
|
||||
)}
|
||||
</AutoSizer>
|
||||
{!loading && (
|
||||
<Button type="submit" variant="primary">
|
||||
Save template
|
||||
</Button>
|
||||
)}
|
||||
<LinkButton
|
||||
disabled={loading}
|
||||
href={makeAMLink('alerting/notifications', alertManagerSourceName)}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
fill="outline"
|
||||
>
|
||||
Cancel
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
<div className={styles.buttons}>
|
||||
{loading && (
|
||||
<Button disabled={true} icon="fa fa-spinner" variant="primary">
|
||||
Saving...
|
||||
</Button>
|
||||
)}
|
||||
{!loading && (
|
||||
<Button type="submit" variant="primary">
|
||||
Save template
|
||||
</Button>
|
||||
)}
|
||||
<LinkButton
|
||||
disabled={loading}
|
||||
href={makeAMLink('alerting/notifications', alertManagerSourceName)}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
fill="outline"
|
||||
>
|
||||
Cancel
|
||||
</LinkButton>
|
||||
<TemplateDataDocs />
|
||||
</div>
|
||||
</FieldSet>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
function TemplatingGuideline() {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Alert title="Templating guideline" severity="info">
|
||||
<Stack direction="row">
|
||||
<div>
|
||||
Grafana uses Go templating language to create notification messages.
|
||||
<br />
|
||||
To find out more about templating please visit our documentation.
|
||||
</div>
|
||||
<div>
|
||||
<LinkButton
|
||||
href="https://grafana.com/docs/grafana/latest/alerting/contact-points/message-templating"
|
||||
target="_blank"
|
||||
icon="external-link-alt"
|
||||
>
|
||||
Templating documentation
|
||||
</LinkButton>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<div className={styles.snippets}>
|
||||
To make templating easier, we provide a few snippets in the content editor to help you speed up your workflow.
|
||||
<div className={styles.code}>
|
||||
{Object.values(snippets)
|
||||
.map((s) => s.label)
|
||||
.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
externalLink: css`
|
||||
contentContainer: css`
|
||||
display: flex;
|
||||
gap: ${theme.spacing(2)};
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
${theme.breakpoints.up('xxl')} {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
`,
|
||||
snippets: css`
|
||||
margin-top: ${theme.spacing(2)};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
`,
|
||||
code: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
text-decoration: underline;
|
||||
font-weight: ${theme.typography.fontWeightBold};
|
||||
`,
|
||||
buttons: css`
|
||||
& > * + * {
|
||||
|
@ -0,0 +1,52 @@
|
||||
import type { Monaco } from '@grafana/ui';
|
||||
|
||||
import { AlertmanagerTemplateFunction } from './language';
|
||||
import { SuggestionDefinition } from './suggestionDefinition';
|
||||
|
||||
export function getAlertManagerSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const kind = monaco.languages.CompletionItemKind.Function;
|
||||
return [
|
||||
{
|
||||
label: AlertmanagerTemplateFunction.toUpper,
|
||||
detail: 'function(s string)',
|
||||
kind,
|
||||
},
|
||||
{
|
||||
label: AlertmanagerTemplateFunction.toLower,
|
||||
detail: 'function(s string)',
|
||||
kind,
|
||||
},
|
||||
{
|
||||
label: AlertmanagerTemplateFunction.title,
|
||||
documentation: 'Capitalizes the first letter of each word',
|
||||
detail: 'function(s string)',
|
||||
kind,
|
||||
},
|
||||
{
|
||||
label: AlertmanagerTemplateFunction.join,
|
||||
documentation: { value: 'Joins an array of strings using the separator provided.' },
|
||||
detail: 'function(separator string, s []string)',
|
||||
kind,
|
||||
},
|
||||
{
|
||||
label: AlertmanagerTemplateFunction.match,
|
||||
detail: 'function',
|
||||
kind,
|
||||
},
|
||||
{
|
||||
label: AlertmanagerTemplateFunction.safeHtml,
|
||||
detail: 'function(pattern, repl, text)',
|
||||
kind,
|
||||
},
|
||||
{
|
||||
label: AlertmanagerTemplateFunction.reReplaceAll,
|
||||
detail: 'function(pattern, repl, text)',
|
||||
kind,
|
||||
},
|
||||
{
|
||||
label: AlertmanagerTemplateFunction.stringSlice,
|
||||
detail: 'function(s ...string)',
|
||||
kind,
|
||||
},
|
||||
];
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
import { concat } from 'lodash';
|
||||
import type { languages, editor, Position, IRange, IDisposable } from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
import type { Monaco } from '@grafana/ui';
|
||||
|
||||
import { getAlertManagerSuggestions } from './alertManagerSuggestions';
|
||||
import { SuggestionDefinition } from './suggestionDefinition';
|
||||
import {
|
||||
getAlertsSuggestions,
|
||||
getAlertSuggestions,
|
||||
getGlobalSuggestions,
|
||||
getKeyValueSuggestions,
|
||||
getSnippetsSuggestions,
|
||||
} from './templateDataSuggestions';
|
||||
|
||||
export function registerGoTemplateAutocomplete(monaco: Monaco): IDisposable {
|
||||
const goTemplateAutocompleteProvider: languages.CompletionItemProvider = {
|
||||
triggerCharacters: ['.'],
|
||||
provideCompletionItems(model, position, context): languages.ProviderResult<languages.CompletionList> {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn,
|
||||
};
|
||||
|
||||
const completionProvider = new CompletionProvider(monaco, range);
|
||||
|
||||
const insideExpression = isInsideGoExpression(model, position);
|
||||
if (!insideExpression) {
|
||||
return completionProvider.getSnippetsSuggestions();
|
||||
}
|
||||
|
||||
if (context.triggerKind === monaco.languages.CompletionTriggerKind.Invoke && !context.triggerCharacter) {
|
||||
return completionProvider.getFunctionsSuggestions();
|
||||
}
|
||||
|
||||
const wordBeforeDot = model.getWordUntilPosition({
|
||||
lineNumber: position.lineNumber,
|
||||
column: position.column - 1,
|
||||
});
|
||||
|
||||
return completionProvider.getTemplateDataSuggestions(wordBeforeDot.word);
|
||||
},
|
||||
};
|
||||
|
||||
return monaco.languages.registerCompletionItemProvider('go-template', goTemplateAutocompleteProvider);
|
||||
}
|
||||
|
||||
function isInsideGoExpression(model: editor.ITextModel, position: Position) {
|
||||
const searchRange = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: model.getLineMinColumn(position.lineNumber),
|
||||
endColumn: model.getLineMaxColumn(position.lineNumber),
|
||||
};
|
||||
|
||||
const goSyntaxRegex = '\\{\\{[a-zA-Z0-9._() "]+\\}\\}';
|
||||
const matches = model.findMatches(goSyntaxRegex, searchRange, true, false, null, true);
|
||||
|
||||
return matches.some((match) => match.range.containsPosition(position));
|
||||
}
|
||||
|
||||
export class CompletionProvider {
|
||||
constructor(private readonly monaco: Monaco, private readonly range: IRange) {}
|
||||
|
||||
getSnippetsSuggestions = (): languages.ProviderResult<languages.CompletionList> => {
|
||||
return this.getCompletionsFromDefinitions(getSnippetsSuggestions(this.monaco));
|
||||
};
|
||||
|
||||
getFunctionsSuggestions = (): languages.ProviderResult<languages.CompletionList> => {
|
||||
return this.getCompletionsFromDefinitions(getAlertManagerSuggestions(this.monaco));
|
||||
};
|
||||
|
||||
getTemplateDataSuggestions = (wordContext: string): languages.ProviderResult<languages.CompletionList> => {
|
||||
switch (wordContext) {
|
||||
case '':
|
||||
return this.getCompletionsFromDefinitions(getGlobalSuggestions(this.monaco), getAlertSuggestions(this.monaco));
|
||||
case 'Alerts':
|
||||
return this.getCompletionsFromDefinitions(getAlertsSuggestions(this.monaco));
|
||||
case 'GroupLabels':
|
||||
case 'CommonLabels':
|
||||
case 'CommonAnnotations':
|
||||
case 'Labels':
|
||||
case 'Annotations':
|
||||
return this.getCompletionsFromDefinitions(getKeyValueSuggestions(this.monaco));
|
||||
default:
|
||||
return { suggestions: [] };
|
||||
}
|
||||
};
|
||||
|
||||
private getCompletionsFromDefinitions = (...args: SuggestionDefinition[][]): languages.CompletionList => {
|
||||
const allDefinitions = concat(...args);
|
||||
|
||||
return {
|
||||
suggestions: allDefinitions.map((definition) => buildAutocompleteSuggestion(definition, this.range)),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function buildAutocompleteSuggestion(
|
||||
{ label, detail, documentation, kind, insertText }: SuggestionDefinition,
|
||||
range: IRange
|
||||
): languages.CompletionItem {
|
||||
const insertFallback = typeof label === 'string' ? label : label.label;
|
||||
const labelObject = typeof label === 'string' ? { label: label, description: detail } : { ...label };
|
||||
|
||||
labelObject.description ??= detail;
|
||||
|
||||
return {
|
||||
label: labelObject,
|
||||
kind: kind,
|
||||
insertText: insertText ?? insertFallback,
|
||||
range,
|
||||
documentation: documentation,
|
||||
detail: detail,
|
||||
};
|
||||
}
|
@ -15,16 +15,18 @@ enum TokenType {
|
||||
|
||||
// list of available functions in Alertmanager templates
|
||||
// see https://cs.github.com/prometheus/alertmanager/blob/805e505288ce82c3e2b625a3ca63aaf2b0aa9cea/template/template.go?q=join#L132-L151
|
||||
export const availableAlertManagerFunctions = [
|
||||
'toUpper',
|
||||
'toLower',
|
||||
'title',
|
||||
'join',
|
||||
'match',
|
||||
'safeHtml',
|
||||
'reReplaceAll',
|
||||
'stringSlice',
|
||||
];
|
||||
export enum AlertmanagerTemplateFunction {
|
||||
toUpper = 'toUpper',
|
||||
toLower = 'toLower',
|
||||
title = 'title',
|
||||
join = 'join',
|
||||
match = 'match',
|
||||
safeHtml = 'safeHtml',
|
||||
reReplaceAll = 'reReplaceAll',
|
||||
stringSlice = 'stringSlice',
|
||||
}
|
||||
|
||||
export const availableAlertManagerFunctions = Object.values(AlertmanagerTemplateFunction);
|
||||
|
||||
// boolean functions
|
||||
const booleanFunctions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge'];
|
||||
@ -77,7 +79,7 @@ export const language: monacoType.languages.IMonarchLanguage = {
|
||||
[/{{-?/, TokenType.Delimiter],
|
||||
[/-?}}/, TokenType.Delimiter],
|
||||
// variables
|
||||
// [/\.([A-Za-z]+)?/, TokenType.Variable],
|
||||
[/\.([A-Za-z]+)?/, TokenType.Variable],
|
||||
// identifiers and keywords
|
||||
[
|
||||
/[a-zA-Z_]\w*/,
|
||||
|
@ -0,0 +1,42 @@
|
||||
export const alertsLoopSnippet = `
|
||||
{{ range .Alerts }}
|
||||
Status: {{ .Status }}
|
||||
Starts at: {{ .StartsAt }}
|
||||
{{ end }}
|
||||
`;
|
||||
|
||||
export const alertDetailsSnippet = `
|
||||
[{{.Status}}] {{ .Labels.alertname }}
|
||||
|
||||
Labels:
|
||||
{{ range .Labels.SortedPairs }}
|
||||
{{ .Name }}: {{ .Value }}
|
||||
{{ end }}
|
||||
|
||||
{{ if gt (len .Annotations) 0 }}
|
||||
Annotations:
|
||||
{{ range .Annotations.SortedPairs }}
|
||||
{{ .Name }}: {{ .Value }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if gt (len .SilenceURL ) 0 }}
|
||||
Silence alert: {{ .SilenceURL }}
|
||||
{{ end }}
|
||||
{{ if gt (len .DashboardURL ) 0 }}
|
||||
Go to dashboard: {{ .DashboardURL }}
|
||||
{{ end }}
|
||||
`;
|
||||
|
||||
export const groupLabelsLoopSnippet = getKeyValueTemplate('GroupLabels');
|
||||
export const commonLabelsLoopSnippet = getKeyValueTemplate('CommonLabels');
|
||||
export const commonAnnotationsLoopSnippet = getKeyValueTemplate('CommonAnnotations');
|
||||
export const labelsLoopSnippet = getKeyValueTemplate('Labels');
|
||||
export const annotationsLoopSnippet = getKeyValueTemplate('Annotations');
|
||||
|
||||
function getKeyValueTemplate(arrayName: string) {
|
||||
return `
|
||||
{{ range .${arrayName} }}
|
||||
{{ .Name }} = {{ .Value }}
|
||||
{{ end }}`;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { languages } from 'monaco-editor';
|
||||
|
||||
export interface SuggestionDefinition extends Omit<languages.CompletionItem, 'range' | 'insertText'> {
|
||||
insertText?: languages.CompletionItem['insertText'];
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
import type { Monaco } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
alertDetailsSnippet,
|
||||
alertsLoopSnippet,
|
||||
annotationsLoopSnippet,
|
||||
commonAnnotationsLoopSnippet,
|
||||
commonLabelsLoopSnippet,
|
||||
groupLabelsLoopSnippet,
|
||||
labelsLoopSnippet,
|
||||
} from './snippets';
|
||||
import { SuggestionDefinition } from './suggestionDefinition';
|
||||
|
||||
// Suggestions available at the top level of a template
|
||||
export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const kind = monaco.languages.CompletionItemKind.Field;
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Alerts',
|
||||
kind,
|
||||
detail: 'Alert[]',
|
||||
documentation: { value: 'An Array containing all alerts' },
|
||||
},
|
||||
{ label: 'Receiver', kind, detail: 'string' },
|
||||
{ label: 'Status', kind, detail: 'string' },
|
||||
{ label: 'GroupLabels', kind, detail: '[]KeyValue' },
|
||||
{ label: 'CommonLabels', kind, detail: '[]KeyValue' },
|
||||
{ label: 'CommonAnnotations', kind, detail: '[]KeyValue' },
|
||||
{ label: 'ExternalURL', kind, detail: 'string' },
|
||||
];
|
||||
}
|
||||
|
||||
// Suggestions that are valid only in the scope of an alert (e.g. in the .Alerts loop)
|
||||
export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const kind = monaco.languages.CompletionItemKind.Field;
|
||||
|
||||
return [
|
||||
{
|
||||
label: { label: 'Status', detail: '(Alert)', description: 'string' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation: { value: 'Status of the alert. It can be `firing` or `resolved`' },
|
||||
},
|
||||
{
|
||||
label: { label: 'Labels', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: '[]KeyValue',
|
||||
documentation: { value: 'A set of labels attached to the alert.' },
|
||||
},
|
||||
{
|
||||
label: { label: 'Annotations', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: '[]KeyValue',
|
||||
documentation: 'A set of annotations attached to the alert.',
|
||||
},
|
||||
{
|
||||
label: { label: 'StartsAt', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'time.Time',
|
||||
documentation: 'Time the alert started firing.',
|
||||
},
|
||||
{
|
||||
label: { label: 'EndsAt', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'time.Time',
|
||||
documentation:
|
||||
'Only set if the end time of an alert is known. Otherwise set to a configurable timeout period from the time since the last alert was received.',
|
||||
},
|
||||
{
|
||||
label: { label: 'GeneratorURL', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation: 'Back link to Grafana or external Alertmanager.',
|
||||
},
|
||||
{
|
||||
label: { label: 'SilenceURL', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation:
|
||||
'Link to Grafana silence for with labels for this alert pre-filled. Only for Grafana managed alerts.',
|
||||
},
|
||||
{
|
||||
label: { label: 'DashboardURL', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation: 'Link to Grafana dashboard, if alert rule belongs to one. Only for Grafana managed alerts.',
|
||||
},
|
||||
{
|
||||
label: { label: 'PanelURL', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation: 'Link to Grafana dashboard panel, if alert rule belongs to one. Only for Grafana managed alerts.',
|
||||
},
|
||||
{
|
||||
label: { label: 'Fingerprint', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation: 'Fingerprint that can be used to identify the alert.',
|
||||
},
|
||||
{
|
||||
label: { label: 'ValueString', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation: 'String that contains labels and values of each reduced expression in the alert.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Suggestions for .Alerts
|
||||
export function getAlertsSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const kind = monaco.languages.CompletionItemKind.Field;
|
||||
|
||||
return [
|
||||
{ label: 'Firing', kind, detail: 'Alert[]' },
|
||||
{ label: 'Resolved', kind, detail: 'Alert[]' },
|
||||
];
|
||||
}
|
||||
|
||||
// Suggestions for the KeyValue types
|
||||
export function getKeyValueSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const kind = monaco.languages.CompletionItemKind.Field;
|
||||
|
||||
return [
|
||||
{ label: 'SortedPairs', kind, detail: '[]KeyValue' },
|
||||
{ label: 'Names', kind, detail: '[]string' },
|
||||
{ label: 'Values', kind, detail: '[]string' },
|
||||
{
|
||||
label: 'Remove',
|
||||
detail: 'KeyValue[] function(keys []string)',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const snippets = {
|
||||
alerts: {
|
||||
label: 'alertsloop',
|
||||
description: 'Renders a loop through alerts',
|
||||
snippet: alertsLoopSnippet,
|
||||
},
|
||||
alertDetails: {
|
||||
label: 'alertdetails',
|
||||
description: 'Renders all information available about the alert',
|
||||
snippet: alertDetailsSnippet,
|
||||
},
|
||||
groupLabels: {
|
||||
label: 'grouplabelsloop',
|
||||
description: 'Renders a loop through group labels',
|
||||
snippet: groupLabelsLoopSnippet,
|
||||
},
|
||||
commonLabels: {
|
||||
label: 'commonlabelsloop',
|
||||
description: 'Renders a loop through common labels',
|
||||
snippet: commonLabelsLoopSnippet,
|
||||
},
|
||||
commonAnnotations: {
|
||||
label: 'commonannotationsloop',
|
||||
description: 'Renders a loop through common annotations',
|
||||
snippet: commonAnnotationsLoopSnippet,
|
||||
},
|
||||
labels: {
|
||||
label: 'labelsloop',
|
||||
description: 'Renders a loop through labels',
|
||||
snippet: labelsLoopSnippet,
|
||||
},
|
||||
annotations: {
|
||||
label: 'annotationsloop',
|
||||
description: 'Renders a loop through annotations',
|
||||
snippet: annotationsLoopSnippet,
|
||||
},
|
||||
};
|
||||
|
||||
// Snippets
|
||||
export function getSnippetsSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const snippetKind = monaco.languages.CompletionItemKind.Snippet;
|
||||
const snippetInsertRule = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet;
|
||||
|
||||
const { alerts, alertDetails, groupLabels, commonLabels, commonAnnotations, labels, annotations } = snippets;
|
||||
|
||||
return [
|
||||
{
|
||||
label: alerts.label,
|
||||
documentation: alerts.description,
|
||||
kind: snippetKind,
|
||||
insertText: alerts.snippet,
|
||||
insertTextRules: snippetInsertRule,
|
||||
},
|
||||
{
|
||||
label: {
|
||||
label: alertDetails.label,
|
||||
detail: '(Alert)',
|
||||
},
|
||||
documentation: alertDetails.description,
|
||||
kind: snippetKind,
|
||||
insertText: alertDetails.snippet,
|
||||
insertTextRules: snippetInsertRule,
|
||||
},
|
||||
{
|
||||
label: groupLabels.label,
|
||||
documentation: groupLabels.description,
|
||||
kind: snippetKind,
|
||||
insertText: groupLabels.snippet,
|
||||
insertTextRules: snippetInsertRule,
|
||||
},
|
||||
{
|
||||
label: commonLabels.label,
|
||||
documentation: commonLabels.description,
|
||||
kind: snippetKind,
|
||||
insertText: commonLabels.snippet,
|
||||
insertTextRules: snippetInsertRule,
|
||||
},
|
||||
{
|
||||
label: commonAnnotations.label,
|
||||
documentation: commonAnnotations.description,
|
||||
kind: snippetKind,
|
||||
insertText: commonAnnotations.snippet,
|
||||
insertTextRules: snippetInsertRule,
|
||||
},
|
||||
{
|
||||
label: { label: labels.label, detail: '(Alert)' },
|
||||
documentation: labels.description,
|
||||
kind: snippetKind,
|
||||
insertText: labels.snippet,
|
||||
insertTextRules: snippetInsertRule,
|
||||
},
|
||||
{
|
||||
label: { label: annotations.label, detail: '(Alert)' },
|
||||
documentation: annotations.description,
|
||||
kind: snippetKind,
|
||||
insertText: annotations.snippet,
|
||||
insertTextRules: snippetInsertRule,
|
||||
},
|
||||
];
|
||||
}
|
Loading…
Reference in New Issue
Block a user