diff --git a/public/app/features/alerting/unified/components/rules/state-history/ErrorMessageRow.tsx b/public/app/features/alerting/unified/components/rules/state-history/ErrorMessageRow.tsx new file mode 100644 index 00000000000..35c5a6b50da --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/state-history/ErrorMessageRow.tsx @@ -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 ( +
+ + + + + {t('alerting.state-history.error-message-prefix', 'Error message:')} + + + + + {message} + + + + +
+ ); +} diff --git a/public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.test.tsx b/public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.test.tsx index b94e535c70c..a90e9dc52a8 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.test.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.test.tsx @@ -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(); + + 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(); + }); }); diff --git a/public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.tsx b/public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.tsx index 6337b89bb0f..288af1dd2f9 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.tsx @@ -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(); + const timestampRefs = useRef>(new Map()); useEffect(() => { - onRecordsRendered && onRecordsRendered(timestampRefs); - }); + onRecordsRendered && onRecordsRendered(timestampRefs.current); + }, [onRecordsRendered, records]); return (
    element && timestampRefs.set(key, element)} + ref={(element) => { + if (element) { + timestampRefs.current.set(key, element); + } else { + timestampRefs.current.delete(key); + } + }} className={styles.listItemWrapper} > -
    - {records.map(({ line }) => ( - - - - - {line.values && } -
    - {line.labels && ( - `${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 ( + +
    + + + + {line.values && } +
    + {line.labels && ( + `${key}=${value}` + )} + onClick={onLabelClick} + /> + )} +
    + {isErrorRow && line.error && }
    - ))} -
    + ); + })} ); })} diff --git a/public/app/features/alerting/unified/components/rules/state-history/common.ts b/public/app/features/alerting/unified/components/rules/state-history/common.ts index a3f486752da..4076dde37ea 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/common.ts +++ b/public/app/features/alerting/unified/components/rules/state-history/common.ts @@ -9,6 +9,7 @@ export interface Line { labels?: Record; fingerprint?: string; ruleUID?: string; + error?: string; } export interface LogRecord { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 1111a31316f..cedc39df80e 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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",