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:
+36
@@ -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>
|
||||
);
|
||||
}
|
||||
+30
-1
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+38
-23
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user