mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
|
|
import React, { FC } from 'react';
|
|||
|
|
import { uniqueId } from 'lodash';
|
|||
|
|
import { AlertState, dateTimeFormat, GrafanaTheme } from '@grafana/data';
|
|||
|
|
import { Alert, LoadingPlaceholder, useStyles } from '@grafana/ui';
|
|||
|
|
import { css } from '@emotion/css';
|
|||
|
|
import { StateHistoryItem, StateHistoryItemData } from 'app/types/unified-alerting';
|
|||
|
|
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
|||
|
|
import { AlertStateTag } from './AlertStateTag';
|
|||
|
|
import { useManagedAlertStateHistory } from '../../hooks/useManagedAlertStateHistory';
|
|||
|
|
import { AlertLabel } from '../AlertLabel';
|
|||
|
|
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
|||
|
|
|
|||
|
|
type StateHistoryRowItem = {
|
|||
|
|
id: string;
|
|||
|
|
state: PromAlertingRuleState | GrafanaAlertState | AlertState;
|
|||
|
|
text?: string;
|
|||
|
|
data?: StateHistoryItemData;
|
|||
|
|
timestamp?: number;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
type StateHistoryRow = DynamicTableItemProps<StateHistoryRowItem>;
|
|||
|
|
|
|||
|
|
interface RuleStateHistoryProps {
|
|||
|
|
alertId: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const StateHistory: FC<RuleStateHistoryProps> = ({ alertId }) => {
|
|||
|
|
const { loading, error, result = [] } = useManagedAlertStateHistory(alertId);
|
|||
|
|
|
|||
|
|
if (loading && !error) {
|
|||
|
|
return <LoadingPlaceholder text={'Loading history...'} />;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error && !loading) {
|
|||
|
|
return <Alert title={'Failed to fetch alert state history'}>{error.message}</Alert>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const columns: Array<DynamicTableColumnProps<StateHistoryRowItem>> = [
|
|||
|
|
{ id: 'state', label: 'State', size: 'max-content', renderCell: renderStateCell },
|
|||
|
|
{ id: 'value', label: '', size: 'auto', renderCell: renderValueCell },
|
|||
|
|
{ id: 'timestamp', label: 'Time', size: 'max-content', renderCell: renderTimestampCell },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const items: StateHistoryRow[] = result
|
|||
|
|
.reduce((acc: StateHistoryRowItem[], item, index) => {
|
|||
|
|
acc.push({
|
|||
|
|
id: String(item.id),
|
|||
|
|
state: item.newState,
|
|||
|
|
text: item.text,
|
|||
|
|
data: item.data,
|
|||
|
|
timestamp: item.updated,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// if the preceding state is not the same, create a separate state entry – this likely means the state was reset
|
|||
|
|
if (!hasMatchingPrecedingState(index, result)) {
|
|||
|
|
acc.push({ id: uniqueId(), state: item.prevState });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return acc;
|
|||
|
|
}, [])
|
|||
|
|
.map((historyItem) => ({
|
|||
|
|
id: historyItem.id,
|
|||
|
|
data: historyItem,
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
return <DynamicTable cols={columns} items={items} />;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function renderValueCell(item: StateHistoryRow) {
|
|||
|
|
const matches = item.data.data?.evalMatches ?? [];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<>
|
|||
|
|
{item.data.text}
|
|||
|
|
<LabelsWrapper>
|
|||
|
|
{matches.map((match) => (
|
|||
|
|
<AlertLabel key={match.metric} labelKey={match.metric} value={String(match.value)} />
|
|||
|
|
))}
|
|||
|
|
</LabelsWrapper>
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderStateCell(item: StateHistoryRow) {
|
|||
|
|
return <AlertStateTag state={item.data.state} />;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderTimestampCell(item: StateHistoryRow) {
|
|||
|
|
return (
|
|||
|
|
<div className={TimestampStyle}>{item.data.timestamp && <span>{dateTimeFormat(item.data.timestamp)}</span>}</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const LabelsWrapper: FC<{}> = ({ children }) => {
|
|||
|
|
const { wrapper } = useStyles(getStyles);
|
|||
|
|
return <div className={wrapper}>{children}</div>;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const TimestampStyle = css`
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
flex-direction: column;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const getStyles = (theme: GrafanaTheme) => ({
|
|||
|
|
wrapper: css`
|
|||
|
|
& > * {
|
|||
|
|
margin-right: ${theme.spacing.xs};
|
|||
|
|
}
|
|||
|
|
`,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// this function will figure out if a given historyItem has a preceding historyItem where the states match - in other words
|
|||
|
|
// the newState of the previous historyItem is the same as the prevState of the current historyItem
|
|||
|
|
function hasMatchingPrecedingState(index: number, items: StateHistoryItem[]): boolean {
|
|||
|
|
const currentHistoryItem = items[index];
|
|||
|
|
const previousHistoryItem = items[index + 1];
|
|||
|
|
|
|||
|
|
if (!previousHistoryItem) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return previousHistoryItem.newState === currentHistoryItem.prevState;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export { StateHistory };
|