Files
Virginia Cepeda 471a03328b Alerting: Allow to tab onto elements for a11y (#67684)
* Make search popover be tabbed onto

Co-Authored-By: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com>

* Allow tokens in rule description to be tabbed onto

Co-Authored-By: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com>

---------

Co-authored-by: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com>
2023-05-03 09:37:42 -03:00

154 lines
3.8 KiB
TypeScript

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.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 };