mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Use new "Label" components for alert instance labels (#70997)
This commit is contained in:
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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)};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user