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
|
* It includes auto-complete for template data and syntax highlighting
|
||||||
*/
|
*/
|
||||||
import { editor } from 'monaco-editor';
|
import { editor, IDisposable } from 'monaco-editor';
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { CodeEditor } from '@grafana/ui';
|
import { CodeEditor } from '@grafana/ui';
|
||||||
import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types';
|
import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types';
|
||||||
|
|
||||||
|
import { registerGoTemplateAutocomplete } from './editor/autocomplete';
|
||||||
import goTemplateLanguageDefinition, { GO_TEMPLATE_LANGUAGE_ID } from './editor/definition';
|
import goTemplateLanguageDefinition, { GO_TEMPLATE_LANGUAGE_ID } from './editor/definition';
|
||||||
import { registerLanguage } from './editor/register';
|
import { registerLanguage } from './editor/register';
|
||||||
|
|
||||||
const getSuggestions = () => {
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
type TemplateEditorProps = Omit<CodeEditorProps, 'language' | 'theme'> & {
|
type TemplateEditorProps = Omit<CodeEditorProps, 'language' | 'theme'> & {
|
||||||
autoHeight?: boolean;
|
autoHeight?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TemplateEditor: FC<TemplateEditorProps> = (props) => {
|
const TemplateEditor: FC<TemplateEditorProps> = (props) => {
|
||||||
const shouldAutoHeight = Boolean(props.autoHeight);
|
const shouldAutoHeight = Boolean(props.autoHeight);
|
||||||
|
const disposeSuggestions = useRef<IDisposable | null>(null);
|
||||||
|
|
||||||
const onEditorDidMount = (editor: editor.IStandaloneCodeEditor) => {
|
const onEditorDidMount = (editor: editor.IStandaloneCodeEditor) => {
|
||||||
if (shouldAutoHeight) {
|
if (shouldAutoHeight) {
|
||||||
@ -35,15 +33,21 @@ const TemplateEditor: FC<TemplateEditorProps> = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
disposeSuggestions.current?.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
showLineNumbers={true}
|
showLineNumbers={true}
|
||||||
getSuggestions={getSuggestions}
|
|
||||||
showMiniMap={false}
|
showMiniMap={false}
|
||||||
{...props}
|
{...props}
|
||||||
onEditorDidMount={onEditorDidMount}
|
onEditorDidMount={onEditorDidMount}
|
||||||
onBeforeEditorMount={(monaco) => {
|
onBeforeEditorMount={(monaco) => {
|
||||||
registerLanguage(monaco, goTemplateLanguageDefinition);
|
registerLanguage(monaco, goTemplateLanguageDefinition);
|
||||||
|
disposeSuggestions.current = registerGoTemplateAutocomplete(monaco);
|
||||||
}}
|
}}
|
||||||
language={GO_TEMPLATE_LANGUAGE_ID}
|
language={GO_TEMPLATE_LANGUAGE_ID}
|
||||||
/>
|
/>
|
||||||
|
@ -4,7 +4,7 @@ import { useForm, Validate } from 'react-hook-form';
|
|||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
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 { useCleanup } from 'app/core/hooks/useCleanup';
|
||||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
@ -16,7 +16,9 @@ import { initialAsyncRequestState } from '../../utils/redux';
|
|||||||
import { ensureDefine } from '../../utils/templates';
|
import { ensureDefine } from '../../utils/templates';
|
||||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||||
|
|
||||||
|
import { TemplateDataDocs } from './TemplateDataDocs';
|
||||||
import { TemplateEditor } from './TemplateEditor';
|
import { TemplateEditor } from './TemplateEditor';
|
||||||
|
import { snippets } from './editor/templateDataSuggestions';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
name: string;
|
name: string;
|
||||||
@ -121,77 +123,104 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<TemplatingGuideline />
|
||||||
description={
|
<div className={styles.contentContainer}>
|
||||||
<>
|
<div>
|
||||||
You can use the{' '}
|
<Field label="Content" error={errors?.content?.message} invalid={!!errors.content?.message} required>
|
||||||
<a
|
<div className={styles.editWrapper}>
|
||||||
href="https://pkg.go.dev/text/template?utm_source=godoc"
|
<AutoSizer>
|
||||||
target="__blank"
|
{({ width, height }) => (
|
||||||
rel="noreferrer"
|
<TemplateEditor
|
||||||
className={styles.externalLink}
|
value={getValues('content')}
|
||||||
>
|
width={width}
|
||||||
Go templating language
|
height={height}
|
||||||
</a>
|
onBlur={(value) => setValue('content', value)}
|
||||||
.{' '}
|
/>
|
||||||
<a
|
)}
|
||||||
href="https://prometheus.io/blog/2016/03/03/custom-alertmanager-templates/"
|
</AutoSizer>
|
||||||
target="__blank"
|
</div>
|
||||||
rel="noreferrer"
|
</Field>
|
||||||
className={styles.externalLink}
|
<div className={styles.buttons}>
|
||||||
>
|
{loading && (
|
||||||
More info about alertmanager templates
|
<Button disabled={true} icon="fa fa-spinner" variant="primary">
|
||||||
</a>
|
Saving...
|
||||||
</>
|
</Button>
|
||||||
}
|
|
||||||
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>
|
{!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>
|
</div>
|
||||||
</Field>
|
<TemplateDataDocs />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
</form>
|
</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) => ({
|
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};
|
color: ${theme.colors.text.secondary};
|
||||||
text-decoration: underline;
|
font-weight: ${theme.typography.fontWeightBold};
|
||||||
`,
|
`,
|
||||||
buttons: css`
|
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
|
// list of available functions in Alertmanager templates
|
||||||
// see https://cs.github.com/prometheus/alertmanager/blob/805e505288ce82c3e2b625a3ca63aaf2b0aa9cea/template/template.go?q=join#L132-L151
|
// see https://cs.github.com/prometheus/alertmanager/blob/805e505288ce82c3e2b625a3ca63aaf2b0aa9cea/template/template.go?q=join#L132-L151
|
||||||
export const availableAlertManagerFunctions = [
|
export enum AlertmanagerTemplateFunction {
|
||||||
'toUpper',
|
toUpper = 'toUpper',
|
||||||
'toLower',
|
toLower = 'toLower',
|
||||||
'title',
|
title = 'title',
|
||||||
'join',
|
join = 'join',
|
||||||
'match',
|
match = 'match',
|
||||||
'safeHtml',
|
safeHtml = 'safeHtml',
|
||||||
'reReplaceAll',
|
reReplaceAll = 'reReplaceAll',
|
||||||
'stringSlice',
|
stringSlice = 'stringSlice',
|
||||||
];
|
}
|
||||||
|
|
||||||
|
export const availableAlertManagerFunctions = Object.values(AlertmanagerTemplateFunction);
|
||||||
|
|
||||||
// boolean functions
|
// boolean functions
|
||||||
const booleanFunctions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge'];
|
const booleanFunctions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge'];
|
||||||
@ -77,7 +79,7 @@ export const language: monacoType.languages.IMonarchLanguage = {
|
|||||||
[/{{-?/, TokenType.Delimiter],
|
[/{{-?/, TokenType.Delimiter],
|
||||||
[/-?}}/, TokenType.Delimiter],
|
[/-?}}/, TokenType.Delimiter],
|
||||||
// variables
|
// variables
|
||||||
// [/\.([A-Za-z]+)?/, TokenType.Variable],
|
[/\.([A-Za-z]+)?/, TokenType.Variable],
|
||||||
// identifiers and keywords
|
// identifiers and keywords
|
||||||
[
|
[
|
||||||
/[a-zA-Z_]\w*/,
|
/[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