Alerting: Display Error Message in Alert History View (#110123)

* add error message to alert history view

* fix lint and typing

* resolve design comments- add show/hide button

* resolve design comments- make error box grey

* run yarn i18n-extract

* separate PR

* resolve PR comments

* fix lint

---------

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
Lauren
2025-08-27 15:13:52 +01:00
committed by GitHub
co-authored by Gilles De Mey
parent a25116dbd5
commit edd59d634e
5 changed files with 106 additions and 24 deletions
@@ -0,0 +1,36 @@
import { t } from '@grafana/i18n';
import { Box, Stack, Text } from '@grafana/ui';
interface ErrorMessageRowProps {
message: string;
}
export function ErrorMessageRow({ message }: ErrorMessageRowProps) {
return (
<div data-testid="state-history-error">
<Box
display="block"
backgroundColor="secondary"
borderStyle="solid"
borderColor="weak"
borderRadius="default"
paddingY={1}
paddingX={2}
marginTop={0.5}
>
<Stack direction="row" alignItems="center" gap={2} wrap={false}>
<Box shrink={0}>
<Text variant="bodySmall" weight="medium" element="span">
{t('alerting.state-history.error-message-prefix', 'Error message:')}
</Text>
</Box>
<Box grow={1} shrink={1} minWidth={0}>
<Text variant="bodySmall" truncate element="p">
{message}
</Text>
</Box>
</Stack>
</Box>
</div>
);
}
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import { byRole } from 'testing-library-selector';
import { LogRecordViewerByTimestamp } from './LogRecordViewer';
@@ -31,4 +31,33 @@ describe('LogRecordViewerByTimestamp', () => {
expect(entry2).toHaveTextContent('foo=bar');
expect(entry2).toHaveTextContent('severity=warning');
});
it('renders error row only when current state is Error and shows message', () => {
const ts = 1681739700000;
const records: LogRecord[] = [
{
timestamp: ts,
line: { current: 'Error (timeout)', previous: 'Pending', labels: { foo: 'bar' }, error: 'timeout' },
},
{ timestamp: ts, line: { current: 'Normal', previous: 'Alerting', labels: { foo: 'baz' } } },
{
timestamp: ts,
line: {
current: 'Error',
previous: 'Pending',
labels: { error: 'explicit message' },
error: 'explicit message',
},
},
];
render(<LogRecordViewerByTimestamp records={records} commonLabels={[]} />);
const errorRows = screen.getAllByTestId('state-history-error');
expect(errorRows).toHaveLength(2);
expect(within(errorRows[0]).getByText(/Error message:/)).toBeInTheDocument();
expect(within(errorRows[0]).getByText(/timeout/)).toBeInTheDocument();
expect(within(errorRows[1]).getByText(/Error message:/)).toBeInTheDocument();
expect(within(errorRows[1]).getByText(/explicit message/)).toBeInTheDocument();
});
});
@@ -1,15 +1,17 @@
import { css } from '@emotion/css';
import { formatDistanceToNowStrict } from 'date-fns';
import { groupBy, uniqueId } from 'lodash';
import { Fragment, memo, useEffect } from 'react';
import { Fragment, memo, useEffect, useRef } from 'react';
import { GrafanaTheme2, dateTimeFormat } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { Icon, Stack, TagList, useStyles2 } from '@grafana/ui';
import { GrafanaAlertState, mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
import { Label } from '../../Label';
import { AlertStateTag } from '../AlertStateTag';
import { ErrorMessageRow } from './ErrorMessageRow';
import { LogRecord, omitLabels } from './common';
type LogRecordViewerProps = {
@@ -49,10 +51,10 @@ export const LogRecordViewerByTimestamp = memo(
const groupedLines = groupRecordsByTimestamp(records);
const timestampRefs = new Map<number, HTMLElement>();
const timestampRefs = useRef<Map<number, HTMLElement>>(new Map());
useEffect(() => {
onRecordsRendered && onRecordsRendered(timestampRefs);
});
onRecordsRendered && onRecordsRendered(timestampRefs.current);
}, [onRecordsRendered, records]);
return (
<ul
@@ -68,30 +70,43 @@ export const LogRecordViewerByTimestamp = memo(
id={key.toString(10)}
key={key}
data-testid={key}
ref={(element) => element && timestampRefs.set(key, element)}
ref={(element) => {
if (element) {
timestampRefs.current.set(key, element);
} else {
timestampRefs.current.delete(key);
}
}}
className={styles.listItemWrapper}
>
<Timestamp time={key} />
<div className={styles.logsContainer}>
{records.map(({ line }) => (
<Fragment key={uniqueId()}>
<AlertStateTag state={line.previous} size="sm" muted />
<Icon name="arrow-right" size="sm" />
<AlertStateTag state={line.current} />
<Stack>{line.values && <AlertInstanceValues record={line.values} />}</Stack>
<div>
{line.labels && (
<TagList
tags={omitLabels(Object.entries(line.labels), commonLabels).map(
([key, value]) => `${key}=${value}`
)}
onClick={onLabelClick}
/>
)}
{records.map(({ line }, idx) => {
const id = line.fingerprint ?? `${key}-${idx}`;
const isErrorRow =
mapStateWithReasonToBaseState(line.current) === GrafanaAlertState.Error && Boolean(line.error);
return (
<Fragment key={id}>
<div className={styles.logsContainer}>
<AlertStateTag state={line.previous} size="sm" muted />
<Icon name="arrow-right" size="sm" />
<AlertStateTag state={line.current} />
<Stack>{line.values && <AlertInstanceValues record={line.values} />}</Stack>
<div>
{line.labels && (
<TagList
tags={omitLabels(Object.entries(line.labels), commonLabels).map(
([key, value]) => `${key}=${value}`
)}
onClick={onLabelClick}
/>
)}
</div>
</div>
{isErrorRow && line.error && <ErrorMessageRow message={line.error} />}
</Fragment>
))}
</div>
);
})}
</li>
);
})}
@@ -9,6 +9,7 @@ export interface Line {
labels?: Record<string, string>;
fingerprint?: string;
ruleUID?: string;
error?: string;
}
export interface LogRecord {
+1
View File
@@ -2781,6 +2781,7 @@
"time": "Time"
}
},
"error-message-prefix": "Error message:",
"filter-group": "Filter group",
"filter-group-tooltip": "Filter each state history group either by exact match or a regular expression, for example:",
"placeholder-search": "Search",