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

View File

@@ -362,7 +362,7 @@ describe('RuleList', () => {
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('messagegreat alert');
expect(ruleDetails).toHaveTextContent('Matching instances');
@@ -373,8 +373,8 @@ describe('RuleList', () => {
const instanceRows = byTestId('row').getAll(instancesTable);
expect(instanceRows).toHaveLength(2);
expect(instanceRows![0]).toHaveTextContent('Firing foo=barseverity=warning2021-03-18 08:47:05');
expect(instanceRows![1]).toHaveTextContent('Firing foo=bazseverity=error2021-03-18 08:47:05');
expect(instanceRows![0]).toHaveTextContent('Firing foobar severitywarning2021-03-18 08:47:05');
expect(instanceRows![1]).toHaveTextContent('Firing foobaz severityerror2021-03-18 08:47:05');
// expand details of an instance
await userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0]));
@@ -515,7 +515,7 @@ describe('RuleList', () => {
await userEvent.click(ui.ruleCollapseToggle.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
await userEvent.clear(filterInput);

View File

@@ -1,17 +1,41 @@
import { css } from '@emotion/css';
import { chain } from 'lodash';
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 {
labels: Record<string, string>;
className?: string;
size?: LabelSize;
}
export const AlertLabels = ({ labels, className }: Props) => {
const pairs = Object.entries(labels).filter(([key]) => !(key.startsWith('__') && key.endsWith('__')));
export const AlertLabels = ({ labels, size }: Props) => {
const styles = useStyles2((theme) => getStyles(theme, size));
const pairs = chain(labels).toPairs().reject(isPrivateKey).value();
return (
<div className={className}>
<TagList tags={Object.values(pairs).map(([label, value]) => `${label}=${value}`)} className={className} />
<div className={styles.wrapper} role="list" aria-label="Labels">
{pairs.map(([label, value]) => (
<Label key={label + value} size={size} label={label} value={value} color={getLabelColor(label)} />
))}
</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 React, { ReactNode } from 'react';
import tinycolor2 from 'tinycolor2';
import { GrafanaTheme2, IconName } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Icon, useStyles2 } from '@grafana/ui';
export type LabelSize = 'md' | 'sm';
interface Props {
icon?: IconName;
label?: ReactNode;
value: ReactNode;
color?: string;
size?: LabelSize;
}
// TODO allow customization with color prop
const Label = ({ label, value, icon }: Props) => {
const styles = useStyles2(getStyles);
const Label = ({ label, value, icon, color, size = 'md' }: Props) => {
const styles = useStyles2((theme) => getStyles(theme, color, size));
return (
<div className={styles.meta().wrapper}>
<Stack direction="row" gap={0} alignItems="stretch">
<div className={styles.meta().label}>
<div className={styles.wrapper} role="listitem">
<Stack direction="row" gap={0} alignItems="stretch" wrap={false}>
<div className={styles.label}>
<Stack direction="row" gap={0.5} alignItems="center">
{icon && <Icon name={icon} />} {label ?? ''}
</Stack>
</div>
<div className={styles.meta().value}>{value}</div>
<div className={styles.value}>{value}</div>
</Stack>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
meta: (color?: string) => ({
const getStyles = (theme: GrafanaTheme2, color?: string, size?: 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`
color: ${fontColor};
font-size: ${theme.typography.bodySmall.fontSize};
border-radius: ${theme.shape.borderRadius(2)};
`,
label: css`
display: flex;
align-items: center;
color: inherit;
padding: ${theme.spacing(0.33)} ${theme.spacing(1)};
background: ${theme.colors.secondary.transparent};
padding: ${padding};
background: ${backgroundColor};
border: solid 1px ${theme.colors.border.medium};
border: solid 1px ${borderColor};
border-top-left-radius: ${theme.shape.borderRadius(2)};
border-bottom-left-radius: ${theme.shape.borderRadius(2)};
`,
value: css`
padding: ${theme.spacing(0.33)} ${theme.spacing(1)};
font-weight: ${theme.typography.fontWeightBold};
color: inherit;
padding: ${padding};
background: ${valueBackgroundColor};
border: solid 1px ${theme.colors.border.medium};
border: solid 1px ${borderColor};
border-left: none;
border-top-right-radius: ${theme.shape.borderRadius(2)};
border-bottom-right-radius: ${theme.shape.borderRadius(2)};
`,
}),
});
};
};
export { Label };

View File

@@ -30,7 +30,11 @@ export const AlertGroup = ({ alertManagerSourceName, group }: Props) => {
onToggle={() => setIsCollapsed(!isCollapsed)}
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>
<AlertGroupHeader group={group} />
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ export const AlertGroup = ({ alertManagerSourceName, group, expandAll }: Props)
return (
<div className={styles.group} data-testid="alert-group">
{Object.keys(group.labels).length > 0 ? (
<AlertLabels labels={group.labels} />
<AlertLabels labels={group.labels} size="sm" />
) : (
<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}
</div>
<div>
<AlertLabels labels={alert.labels} />
<AlertLabels labels={alert.labels} size="sm" />
</div>
<div className={styles.actionsRow}>
{alert.status.state === AlertState.Suppressed && (

View File

@@ -123,7 +123,7 @@ describe('AlertGroupsPanel', () => {
expect(groups).toHaveLength(2);
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();
expect(alerts).toHaveLength(0);

View File

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