Alerting: Adds visual tokens for templates (#51376)
This commit is contained in:
@@ -7,6 +7,7 @@ import { Tooltip, useStyles } from '@grafana/ui';
|
||||
import { Annotation, annotationLabels } from '../utils/constants';
|
||||
|
||||
import { DetailsField } from './DetailsField';
|
||||
import { Tokenize } from './Tokenize';
|
||||
import { Well } from './Well';
|
||||
|
||||
const wellableAnnotationKeys = ['message', 'description'];
|
||||
@@ -38,8 +39,10 @@ const AnnotationValue: FC<Props> = ({ annotationKey, value }) => {
|
||||
const needsWell = wellableAnnotationKeys.includes(annotationKey);
|
||||
const needsLink = value && value.startsWith('http');
|
||||
|
||||
const tokenizeValue = <Tokenize input={value} delimiter={['{{', '}}']} />;
|
||||
|
||||
if (needsWell) {
|
||||
return <Well className={styles.well}>{value}</Well>;
|
||||
return <Well className={styles.well}>{tokenizeValue}</Well>;
|
||||
}
|
||||
|
||||
if (needsLink) {
|
||||
@@ -50,7 +53,7 @@ const AnnotationValue: FC<Props> = ({ annotationKey, value }) => {
|
||||
);
|
||||
}
|
||||
|
||||
return <>{value}</>;
|
||||
return <>{tokenizeValue}</>;
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme) => ({
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Placement } from '@popperjs/core';
|
||||
import classnames from 'classnames';
|
||||
import React, { FC, ReactElement, useRef } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Popover as GrafanaPopover, PopoverController, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface HoverCardProps {
|
||||
children: ReactElement;
|
||||
content: ReactElement;
|
||||
wrapperClassName?: string;
|
||||
placement?: Placement;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const HoverCard: FC<HoverCardProps> = ({ children, content, wrapperClassName, disabled = false, ...rest }) => {
|
||||
const popoverRef = useRef<HTMLElement>(null);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (disabled) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverController content={content} hideAfter={100}>
|
||||
{(showPopper, hidePopper, popperProps) => {
|
||||
return (
|
||||
<>
|
||||
{popoverRef.current && (
|
||||
<GrafanaPopover
|
||||
{...popperProps}
|
||||
{...rest}
|
||||
wrapperClassName={classnames(styles.popover, wrapperClassName)}
|
||||
onMouseLeave={hidePopper}
|
||||
onMouseEnter={showPopper}
|
||||
referenceElement={popoverRef.current}
|
||||
/>
|
||||
)}
|
||||
|
||||
{React.cloneElement(children, {
|
||||
ref: popoverRef,
|
||||
onMouseEnter: showPopper,
|
||||
onMouseLeave: hidePopper,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</PopoverController>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
popover: css`
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
box-shadow: ${theme.shadows.z3};
|
||||
background: ${theme.colors.background.primary};
|
||||
border: 1px solid ${theme.colors.border.medium};
|
||||
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Badge, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { HoverCard } from './HoverCard';
|
||||
import { keywords as KEYWORDS, builtinFunctions as FUNCTIONS } from './receivers/editor/language';
|
||||
|
||||
const VARIABLES = ['$', '.', '"'];
|
||||
|
||||
interface TokenizerProps {
|
||||
input: string;
|
||||
delimiter?: [string, string];
|
||||
}
|
||||
|
||||
function Tokenize({ input, delimiter = ['{{', '}}'] }: TokenizerProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [open, close] = delimiter;
|
||||
const normalizedIput = normalizeInput(input);
|
||||
|
||||
/**
|
||||
* This RegExp uses 2 named capture groups, text that comes before the token and the token itself
|
||||
*
|
||||
* <before> open <token> close
|
||||
* ───────── ── ─────────── ──
|
||||
* Some text {{ $labels.foo }}
|
||||
*/
|
||||
const regex = new RegExp(`(?<before>.*?)(${open}(?<token>.*?)${close}|$)`, 'gm');
|
||||
|
||||
const matches = Array.from(normalizedIput.matchAll(regex));
|
||||
|
||||
const output: React.ReactElement[] = [];
|
||||
|
||||
matches.forEach((match, index) => {
|
||||
const before = match.groups?.before;
|
||||
const token = match.groups?.token?.trim();
|
||||
|
||||
if (before) {
|
||||
output.push(<span key={`${index}-before`}>{before}</span>);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
const type = tokenType(token);
|
||||
const description = type === TokenType.Variable ? token : '';
|
||||
const tokenContent = `${open} ${token} ${close}`;
|
||||
|
||||
output.push(<Token key={`${index}-token`} content={tokenContent} type={type} description={description} />);
|
||||
}
|
||||
});
|
||||
|
||||
return <span className={styles.wrapper}>{output}</span>;
|
||||
}
|
||||
|
||||
enum TokenType {
|
||||
Variable = 'variable',
|
||||
Function = 'function',
|
||||
Keyword = 'keyword',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
interface TokenProps {
|
||||
content: string;
|
||||
type?: TokenType;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
function Token({ content, description, type }: TokenProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const varName = content.trim();
|
||||
|
||||
const disableCard = Boolean(type) === false;
|
||||
|
||||
return (
|
||||
<HoverCard
|
||||
placement="top-start"
|
||||
disabled={disableCard}
|
||||
content={
|
||||
<div className={styles.hoverTokenItem}>
|
||||
<Badge text={<>{type}</>} color={'blue'} /> {description && <code>{description}</code>}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Badge className={styles.token} text={varName} color={'blue'} />
|
||||
</span>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeInput(input: string) {
|
||||
return input.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isVariable(input: string) {
|
||||
return VARIABLES.some((character) => input.startsWith(character));
|
||||
}
|
||||
|
||||
function isKeyword(input: string) {
|
||||
return KEYWORDS.some((keyword) => input.startsWith(keyword));
|
||||
}
|
||||
|
||||
function isFunction(input: string) {
|
||||
return FUNCTIONS.some((functionName) => input.startsWith(functionName));
|
||||
}
|
||||
|
||||
function tokenType(input: string) {
|
||||
let tokenType;
|
||||
if (isVariable(input)) {
|
||||
tokenType = TokenType.Variable;
|
||||
} else if (isKeyword(input)) {
|
||||
tokenType = TokenType.Keyword;
|
||||
} else if (isFunction(input)) {
|
||||
tokenType = TokenType.Function;
|
||||
} else {
|
||||
tokenType = TokenType.Unknown;
|
||||
}
|
||||
|
||||
return tokenType;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
`,
|
||||
token: css`
|
||||
cursor: default;
|
||||
font-family: ${theme.typography.fontFamilyMonospace};
|
||||
`,
|
||||
popover: css`
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
box-shadow: ${theme.shadows.z3};
|
||||
background: ${theme.colors.background.primary};
|
||||
border: 1px solid ${theme.colors.border.medium};
|
||||
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
hoverTokenItem: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
});
|
||||
|
||||
export { Tokenize, Token };
|
||||
@@ -15,7 +15,7 @@ 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
|
||||
const availableAlertManagerFunctions = [
|
||||
export const availableAlertManagerFunctions = [
|
||||
'toUpper',
|
||||
'toLower',
|
||||
'title',
|
||||
@@ -26,8 +26,11 @@ const availableAlertManagerFunctions = [
|
||||
'stringSlice',
|
||||
];
|
||||
|
||||
// boolean functions
|
||||
const booleanFunctions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge'];
|
||||
|
||||
// built-in functions for Go templates
|
||||
const builtinFunctions = [
|
||||
export const builtinFunctions = [
|
||||
'and',
|
||||
'call',
|
||||
'html',
|
||||
@@ -41,13 +44,11 @@ const builtinFunctions = [
|
||||
'printf',
|
||||
'println',
|
||||
'urlquery',
|
||||
...booleanFunctions,
|
||||
];
|
||||
|
||||
// boolean functions
|
||||
const booleanFunctions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge'];
|
||||
|
||||
// Go template keywords
|
||||
const keywords = ['define', 'if', 'else', 'end', 'range', 'break', 'continue', 'template', 'block', 'with'];
|
||||
export const keywords = ['define', 'if', 'else', 'end', 'range', 'break', 'continue', 'template', 'block', 'with'];
|
||||
|
||||
// Monarch language definition, see https://microsoft.github.io/monaco-editor/monarch.html
|
||||
// check https://github.com/microsoft/monaco-editor/blob/main/src/basic-languages/go/go.ts for an example
|
||||
@@ -55,7 +56,7 @@ const keywords = ['define', 'if', 'else', 'end', 'range', 'break', 'continue', '
|
||||
export const language: monacoType.languages.IMonarchLanguage = {
|
||||
defaultToken: '', // change this to "invalid" to find tokens that were never matched
|
||||
keywords: keywords,
|
||||
functions: [...builtinFunctions, ...booleanFunctions, ...availableAlertManagerFunctions],
|
||||
functions: [...builtinFunctions, ...availableAlertManagerFunctions],
|
||||
operators: ['|'],
|
||||
tokenizer: {
|
||||
root: [
|
||||
|
||||
@@ -13,6 +13,7 @@ import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '..
|
||||
import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
import { RuleLocation } from '../RuleLocation';
|
||||
import { Tokenize } from '../Tokenize';
|
||||
|
||||
import { RuleDetails } from './RuleDetails';
|
||||
import { RuleHealth } from './RuleHealth';
|
||||
@@ -160,7 +161,9 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
|
||||
id: 'summary',
|
||||
label: 'Summary',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: rule }) => rule.annotations[Annotation.summary] ?? '',
|
||||
renderCell: ({ data: rule }) => {
|
||||
return <Tokenize input={rule.annotations[Annotation.summary] ?? ''} />;
|
||||
},
|
||||
size: 5,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user