The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/alerting/unified/components/Tokenize.tsx

153 lines
3.8 KiB

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;
/**
* 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 lines = input.split('\n');
const output: React.ReactElement[] = [];
lines.forEach((line, lineIndex) => {
const matches = Array.from(line.matchAll(regex));
matches.forEach((match, matchIndex) => {
const before = match.groups?.before;
const token = match.groups?.token?.trim();
if (before) {
output.push(<span key={`${lineIndex}-${matchIndex}-before`}>{before}</span>);
}
if (token) {
const type = tokenType(token);
const description = type === TokenType.Variable ? token : '';
const tokenContent = `${open} ${token} ${close}`;
output.push(
<Token
key={`${lineIndex}-${matchIndex}-token`}
content={tokenContent}
type={type}
description={description}
/>
);
}
});
output.push(<br key={`${lineIndex}-newline`} />);
});
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 disableCard = Boolean(type) === false;
return (
<HoverCard
placement="top-start"
disabled={disableCard}
content={
<div className={styles.hoverTokenItem}>
<Badge tabIndex={0} text={<>{type}</>} color={'blue'} /> {description && <code>{description}</code>}
</div>
}
>
<span>
<Badge tabIndex={0} className={styles.token} text={content} color={'blue'} />
</span>
</HoverCard>
);
}
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`
white-space: pre-wrap;
`,
token: css`
cursor: default;
font-family: ${theme.typography.fontFamilyMonospace};
`,
popover: css`
border-radius: ${theme.shape.radius.default};
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 };