Alerting: Add templates autocomplete (#53655)

Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com>
This commit is contained in:
Konrad Lalik 2022-09-20 13:07:17 +02:00 committed by GitHub
parent 43a1d1484d
commit 4c4b758646
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 881 additions and 80 deletions

View File

@ -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"
}
`;

View File

@ -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;
}
`,
});

View File

@ -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}
/>

View File

@ -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`
& > * + * {

View File

@ -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,
},
];
}

View File

@ -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,
};
}

View File

@ -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*/,

View File

@ -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 }}`;
}

View File

@ -0,0 +1,5 @@
import { languages } from 'monaco-editor';
export interface SuggestionDefinition extends Omit<languages.CompletionItem, 'range' | 'insertText'> {
insertText?: languages.CompletionItem['insertText'];
}

View File

@ -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,
},
];
}