Alerting: Adds visual tokens for templates (#51376)

pull/51564/head
Gilles De Mey 3 years ago committed by GitHub
parent 66b4a9e6a1
commit 268ee678b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      public/app/features/alerting/unified/components/AnnotationDetailsField.tsx
  2. 62
      public/app/features/alerting/unified/components/HoverCard.tsx
  3. 149
      public/app/features/alerting/unified/components/Tokenize.tsx
  4. 15
      public/app/features/alerting/unified/components/receivers/editor/language.ts
  5. 5
      public/app/features/alerting/unified/components/rules/RulesTable.tsx

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

Loading…
Cancel
Save