Alerting: Use new "Label" components for alert instance labels (#70997)

This commit is contained in:
Gilles De Mey
2023-07-10 13:03:35 +02:00
committed by GitHub
parent 01f6b8e90c
commit 1945f2b64e
13 changed files with 102 additions and 50 deletions

View File

@@ -78,7 +78,7 @@ describe('AlertGroups', () => {
expect(groups).toHaveLength(2); expect(groups).toHaveLength(2);
expect(groups[0]).toHaveTextContent('No grouping'); expect(groups[0]).toHaveTextContent('No grouping');
expect(groups[1]).toHaveTextContent('severity=warningregion=US-Central'); expect(groups[1]).toHaveTextContent('severitywarning regionUS-Central');
await userEvent.click(ui.groupCollapseToggle.get(groups[0])); await userEvent.click(ui.groupCollapseToggle.get(groups[0]));
expect(ui.groupTable.get()).toBeDefined(); expect(ui.groupTable.get()).toBeDefined();
@@ -111,9 +111,9 @@ describe('AlertGroups', () => {
const groupByWrapper = ui.groupByContainer.get(); const groupByWrapper = ui.groupByContainer.get();
expect(groups).toHaveLength(3); expect(groups).toHaveLength(3);
expect(groups[0]).toHaveTextContent('region=NASA'); expect(groups[0]).toHaveTextContent('regionNASA');
expect(groups[1]).toHaveTextContent('region=EMEA'); expect(groups[1]).toHaveTextContent('regionEMEA');
expect(groups[2]).toHaveTextContent('region=APAC'); expect(groups[2]).toHaveTextContent('regionAPAC');
await userEvent.type(groupByInput, 'appName{enter}'); await userEvent.type(groupByInput, 'appName{enter}');
@@ -123,9 +123,9 @@ describe('AlertGroups', () => {
await waitFor(() => expect(ui.clearButton.get()).toBeInTheDocument()); await waitFor(() => expect(ui.clearButton.get()).toBeInTheDocument());
expect(groups).toHaveLength(3); expect(groups).toHaveLength(3);
expect(groups[0]).toHaveTextContent('appName=billing'); expect(groups[0]).toHaveTextContent('appNamebilling');
expect(groups[1]).toHaveTextContent('appName=auth'); expect(groups[1]).toHaveTextContent('appNameauth');
expect(groups[2]).toHaveTextContent('appName=frontend'); expect(groups[2]).toHaveTextContent('appNamefrontend');
await userEvent.click(ui.clearButton.get()); await userEvent.click(ui.clearButton.get());
await waitFor(() => expect(groupByWrapper).not.toHaveTextContent('appName')); await waitFor(() => expect(groupByWrapper).not.toHaveTextContent('appName'));
@@ -136,8 +136,8 @@ describe('AlertGroups', () => {
groups = await ui.group.findAll(); groups = await ui.group.findAll();
expect(groups).toHaveLength(2); expect(groups).toHaveLength(2);
expect(groups[0]).toHaveTextContent('env=production'); expect(groups[0]).toHaveTextContent('envproduction');
expect(groups[1]).toHaveTextContent('env=staging'); expect(groups[1]).toHaveTextContent('envstaging');
await userEvent.click(ui.clearButton.get()); await userEvent.click(ui.clearButton.get());
await waitFor(() => expect(groupByWrapper).not.toHaveTextContent('env')); await waitFor(() => expect(groupByWrapper).not.toHaveTextContent('env'));
@@ -148,7 +148,7 @@ describe('AlertGroups', () => {
groups = await ui.group.findAll(); groups = await ui.group.findAll();
expect(groups).toHaveLength(2); expect(groups).toHaveLength(2);
expect(groups[0]).toHaveTextContent('No grouping'); expect(groups[0]).toHaveTextContent('No grouping');
expect(groups[1]).toHaveTextContent('uniqueLabel=true'); expect(groups[1]).toHaveTextContent('uniqueLabeltrue');
}); });
it('should combine multiple ungrouped groups', async () => { it('should combine multiple ungrouped groups', async () => {

View File

@@ -362,7 +362,7 @@ describe('RuleList', () => {
const ruleDetails = ui.expandedContent.get(ruleRows[1]); const ruleDetails = ui.expandedContent.get(ruleRows[1]);
expect(ruleDetails).toHaveTextContent('Labelsseverity=warningfoo=bar'); expect(ruleDetails).toHaveTextContent('Labels severitywarning foobar');
expect(ruleDetails).toHaveTextContent('Expressiontopk ( 5 , foo ) [ 5m ]'); expect(ruleDetails).toHaveTextContent('Expressiontopk ( 5 , foo ) [ 5m ]');
expect(ruleDetails).toHaveTextContent('messagegreat alert'); expect(ruleDetails).toHaveTextContent('messagegreat alert');
expect(ruleDetails).toHaveTextContent('Matching instances'); expect(ruleDetails).toHaveTextContent('Matching instances');
@@ -373,8 +373,8 @@ describe('RuleList', () => {
const instanceRows = byTestId('row').getAll(instancesTable); const instanceRows = byTestId('row').getAll(instancesTable);
expect(instanceRows).toHaveLength(2); expect(instanceRows).toHaveLength(2);
expect(instanceRows![0]).toHaveTextContent('Firing foo=barseverity=warning2021-03-18 08:47:05'); expect(instanceRows![0]).toHaveTextContent('Firing foobar severitywarning2021-03-18 08:47:05');
expect(instanceRows![1]).toHaveTextContent('Firing foo=bazseverity=error2021-03-18 08:47:05'); expect(instanceRows![1]).toHaveTextContent('Firing foobaz severityerror2021-03-18 08:47:05');
// expand details of an instance // expand details of an instance
await userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0])); await userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0]));
@@ -515,7 +515,7 @@ describe('RuleList', () => {
await userEvent.click(ui.ruleCollapseToggle.get(ruleRows[0])); await userEvent.click(ui.ruleCollapseToggle.get(ruleRows[0]));
const ruleDetails = ui.expandedContent.get(ruleRows[0]); const ruleDetails = ui.expandedContent.get(ruleRows[0]);
expect(ruleDetails).toHaveTextContent('Labelsseverity=warningfoo=bar'); expect(ruleDetails).toHaveTextContent('Labels severitywarning foobar');
// Check for different label matchers // Check for different label matchers
await userEvent.clear(filterInput); await userEvent.clear(filterInput);

View File

@@ -1,17 +1,41 @@
import { css } from '@emotion/css';
import { chain } from 'lodash';
import React from 'react'; import React from 'react';
import { TagList } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data';
import { getTagColorsFromName, useStyles2 } from '@grafana/ui';
import { Label, LabelSize } from './Label';
interface Props { interface Props {
labels: Record<string, string>; labels: Record<string, string>;
className?: string; size?: LabelSize;
} }
export const AlertLabels = ({ labels, className }: Props) => { export const AlertLabels = ({ labels, size }: Props) => {
const pairs = Object.entries(labels).filter(([key]) => !(key.startsWith('__') && key.endsWith('__'))); const styles = useStyles2((theme) => getStyles(theme, size));
const pairs = chain(labels).toPairs().reject(isPrivateKey).value();
return ( return (
<div className={className}> <div className={styles.wrapper} role="list" aria-label="Labels">
<TagList tags={Object.values(pairs).map(([label, value]) => `${label}=${value}`)} className={className} /> {pairs.map(([label, value]) => (
<Label key={label + value} size={size} label={label} value={value} color={getLabelColor(label)} />
))}
</div> </div>
); );
}; };
function getLabelColor(input: string): string {
return getTagColorsFromName(input).color;
}
const isPrivateKey = ([key, _]: [string, string]) => key.startsWith('__') && key.endsWith('__');
const getStyles = (theme: GrafanaTheme2, size?: LabelSize) => ({
wrapper: css`
display: flex;
flex-wrap: wrap;
gap: ${size === 'md' ? theme.spacing() : theme.spacing(0.5)};
`,
});

View File

@@ -1,61 +1,87 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import tinycolor2 from 'tinycolor2';
import { GrafanaTheme2, IconName } from '@grafana/data'; import { GrafanaTheme2, IconName } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Stack } from '@grafana/experimental';
import { Icon, useStyles2 } from '@grafana/ui'; import { Icon, useStyles2 } from '@grafana/ui';
export type LabelSize = 'md' | 'sm';
interface Props { interface Props {
icon?: IconName; icon?: IconName;
label?: ReactNode; label?: ReactNode;
value: ReactNode; value: ReactNode;
color?: string; color?: string;
size?: LabelSize;
} }
// TODO allow customization with color prop // TODO allow customization with color prop
const Label = ({ label, value, icon }: Props) => { const Label = ({ label, value, icon, color, size = 'md' }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2((theme) => getStyles(theme, color, size));
return ( return (
<div className={styles.meta().wrapper}> <div className={styles.wrapper} role="listitem">
<Stack direction="row" gap={0} alignItems="stretch"> <Stack direction="row" gap={0} alignItems="stretch" wrap={false}>
<div className={styles.meta().label}> <div className={styles.label}>
<Stack direction="row" gap={0.5} alignItems="center"> <Stack direction="row" gap={0.5} alignItems="center">
{icon && <Icon name={icon} />} {label ?? ''} {icon && <Icon name={icon} />} {label ?? ''}
</Stack> </Stack>
</div> </div>
<div className={styles.meta().value}>{value}</div> <div className={styles.value}>{value}</div>
</Stack> </Stack>
</div> </div>
); );
}; };
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2, color?: string, size?: string) => {
meta: (color?: string) => ({ const backgroundColor = color ?? theme.colors.secondary.main;
const borderColor = theme.isDark
? tinycolor2(backgroundColor).lighten(5).toString()
: tinycolor2(backgroundColor).darken(5).toString();
const valueBackgroundColor = theme.isDark
? tinycolor2(backgroundColor).darken(5).toString()
: tinycolor2(backgroundColor).lighten(5).toString();
const fontColor = color
? tinycolor2.mostReadable(backgroundColor, ['#000', '#fff']).toString()
: theme.colors.text.primary;
const padding =
size === 'md' ? `${theme.spacing(0.33)} ${theme.spacing(1)}` : `${theme.spacing(0.2)} ${theme.spacing(0.6)}`;
return {
wrapper: css` wrapper: css`
color: ${fontColor};
font-size: ${theme.typography.bodySmall.fontSize}; font-size: ${theme.typography.bodySmall.fontSize};
border-radius: ${theme.shape.borderRadius(2)};
`, `,
label: css` label: css`
display: flex; display: flex;
align-items: center; align-items: center;
color: inherit;
padding: ${theme.spacing(0.33)} ${theme.spacing(1)}; padding: ${padding};
background: ${theme.colors.secondary.transparent}; background: ${backgroundColor};
border: solid 1px ${theme.colors.border.medium}; border: solid 1px ${borderColor};
border-top-left-radius: ${theme.shape.borderRadius(2)}; border-top-left-radius: ${theme.shape.borderRadius(2)};
border-bottom-left-radius: ${theme.shape.borderRadius(2)}; border-bottom-left-radius: ${theme.shape.borderRadius(2)};
`, `,
value: css` value: css`
padding: ${theme.spacing(0.33)} ${theme.spacing(1)}; color: inherit;
font-weight: ${theme.typography.fontWeightBold}; padding: ${padding};
background: ${valueBackgroundColor};
border: solid 1px ${theme.colors.border.medium}; border: solid 1px ${borderColor};
border-left: none; border-left: none;
border-top-right-radius: ${theme.shape.borderRadius(2)}; border-top-right-radius: ${theme.shape.borderRadius(2)};
border-bottom-right-radius: ${theme.shape.borderRadius(2)}; border-bottom-right-radius: ${theme.shape.borderRadius(2)};
`, `,
}), };
}); };
export { Label }; export { Label };

View File

@@ -30,7 +30,11 @@ export const AlertGroup = ({ alertManagerSourceName, group }: Props) => {
onToggle={() => setIsCollapsed(!isCollapsed)} onToggle={() => setIsCollapsed(!isCollapsed)}
data-testid="alert-group-collapse-toggle" data-testid="alert-group-collapse-toggle"
/> />
{Object.keys(group.labels).length ? <AlertLabels labels={group.labels} /> : <span>No grouping</span>} {Object.keys(group.labels).length ? (
<AlertLabels labels={group.labels} size="sm" />
) : (
<span>No grouping</span>
)}
</div> </div>
<AlertGroupHeader group={group} /> <AlertGroupHeader group={group} />
</div> </div>

View File

@@ -47,7 +47,7 @@ export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props)
id: 'labels', id: 'labels',
label: 'Labels', label: 'Labels',
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: ({ data: { labels } }) => <AlertLabels labels={labels} />, renderCell: ({ data: { labels } }) => <AlertLabels labels={labels} size="sm" />,
size: 1, size: 1,
}, },
], ],

View File

@@ -171,7 +171,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
)} )}
{!!rule.labels && !!Object.keys(rule.labels).length && ( {!!rule.labels && !!Object.keys(rule.labels).length && (
<DetailsField label="Labels" horizontal={true}> <DetailsField label="Labels" horizontal={true}>
<AlertLabels labels={rule.labels} className={styles.labels} /> <AlertLabels labels={rule.labels} />
</DetailsField> </DetailsField>
)} )}
<RuleDetailsExpression rulesSource={rulesSource} rule={rule} annotations={annotations} /> <RuleDetailsExpression rulesSource={rulesSource} rule={rule} annotations={annotations} />
@@ -297,6 +297,7 @@ const getStyles = (theme: GrafanaTheme2) => {
`, `,
leftSide: css` leftSide: css`
flex: 1; flex: 1;
overflow: hidden;
`, `,
rightSide: css` rightSide: css`
padding-right: ${theme.spacing(3)}; padding-right: ${theme.spacing(3)};

View File

@@ -53,7 +53,7 @@ const columns: AlertTableColumnProps[] = [
id: 'labels', id: 'labels',
label: 'Labels', label: 'Labels',
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: ({ data: { labels } }) => <AlertLabels labels={labels} />, renderCell: ({ data: { labels } }) => <AlertLabels labels={labels} size="sm" />,
}, },
{ {
id: 'created', id: 'created',

View File

@@ -42,7 +42,7 @@ export const SilencedAlertsTableRow = ({ alert, className }: Props) => {
<tr className={className}> <tr className={className}>
<td></td> <td></td>
<td colSpan={5}> <td colSpan={5}>
<AlertLabels labels={alert.labels} /> <AlertLabels labels={alert.labels} size="sm" />
</td> </td>
</tr> </tr>
)} )}

View File

@@ -90,7 +90,7 @@ function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
id: 'labels', id: 'labels',
label: 'Labels', label: 'Labels',
renderCell: function renderName({ data }) { renderCell: function renderName({ data }) {
return <AlertLabels labels={data.labels} className={styles.alertLabels} />; return <AlertLabels labels={data.labels} size="sm" />;
}, },
size: 'auto', size: 'auto',
}, },
@@ -123,7 +123,4 @@ const getStyles = (theme: GrafanaTheme2) => ({
display: flex; display: flex;
align-items: center; align-items: center;
`, `,
alertLabels: css`
justify-content: flex-start;
`,
}); });

View File

@@ -26,7 +26,7 @@ export const AlertGroup = ({ alertManagerSourceName, group, expandAll }: Props)
return ( return (
<div className={styles.group} data-testid="alert-group"> <div className={styles.group} data-testid="alert-group">
{Object.keys(group.labels).length > 0 ? ( {Object.keys(group.labels).length > 0 ? (
<AlertLabels labels={group.labels} /> <AlertLabels labels={group.labels} size="sm" />
) : ( ) : (
<div className={styles.noGroupingText}>No grouping</div> <div className={styles.noGroupingText}>No grouping</div>
)} )}
@@ -49,7 +49,7 @@ export const AlertGroup = ({ alertManagerSourceName, group, expandAll }: Props)
<span className={textStyles[alert.status.state]}>{state} </span>for {interval} <span className={textStyles[alert.status.state]}>{state} </span>for {interval}
</div> </div>
<div> <div>
<AlertLabels labels={alert.labels} /> <AlertLabels labels={alert.labels} size="sm" />
</div> </div>
<div className={styles.actionsRow}> <div className={styles.actionsRow}>
{alert.status.state === AlertState.Suppressed && ( {alert.status.state === AlertState.Suppressed && (

View File

@@ -123,7 +123,7 @@ describe('AlertGroupsPanel', () => {
expect(groups).toHaveLength(2); expect(groups).toHaveLength(2);
expect(groups[0]).toHaveTextContent('No grouping'); expect(groups[0]).toHaveTextContent('No grouping');
expect(groups[1]).toHaveTextContent('severity=warningregion=US-Central'); expect(groups[1]).toHaveTextContent('severitywarning regionUS-Central');
const alerts = ui.alert.queryAll(); const alerts = ui.alert.queryAll();
expect(alerts).toHaveLength(0); expect(alerts).toHaveLength(0);

View File

@@ -188,8 +188,8 @@ describe('UnifiedAlertList', () => {
await user.click(expandElement); await user.click(expandElement);
const tagsElement = await byRole('list', { name: 'Tags' }).find(); const labelsElement = await byRole('list', { name: 'Labels' }).find();
expect(await byRole('listitem').find(tagsElement)).toHaveTextContent('severity=critical'); expect(await byRole('listitem').find(labelsElement)).toHaveTextContent('severitycritical');
expect(replaceVarsSpy).toHaveBeenLastCalledWith('$label'); expect(replaceVarsSpy).toHaveBeenLastCalledWith('$label');
expect(filterAlertsSpy).toHaveBeenLastCalledWith( expect(filterAlertsSpy).toHaveBeenLastCalledWith(