mirror of https://github.com/grafana/grafana
Alerting: show state history (#42362)
parent
dc57bcd458
commit
02039d7532
@ -0,0 +1,18 @@ |
||||
import '@grafana/runtime'; |
||||
import { fetchAnnotations } from './annotations'; |
||||
|
||||
const get = jest.fn(); |
||||
|
||||
jest.mock('@grafana/runtime', () => ({ |
||||
getBackendSrv: () => ({ get }), |
||||
})); |
||||
|
||||
describe('annotations', () => { |
||||
beforeEach(() => get.mockClear()); |
||||
|
||||
it('should fetch annotation for an alertId', () => { |
||||
const ALERT_ID = 'abc123'; |
||||
fetchAnnotations(ALERT_ID); |
||||
expect(get).toBeCalledWith('/api/annotations', { alertId: ALERT_ID }); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,8 @@ |
||||
import { getBackendSrv } from '@grafana/runtime'; |
||||
import { StateHistoryItem } from 'app/types/unified-alerting'; |
||||
|
||||
export function fetchAnnotations(alertId: string): Promise<StateHistoryItem[]> { |
||||
return getBackendSrv().get('/api/annotations', { |
||||
alertId, |
||||
}); |
||||
} |
||||
@ -0,0 +1,126 @@ |
||||
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 }; |
||||
@ -0,0 +1,19 @@ |
||||
import { StateHistoryItem } from 'app/types/unified-alerting'; |
||||
import { useEffect } from 'react'; |
||||
import { useDispatch } from 'react-redux'; |
||||
import { fetchGrafanaAnnotationsAction } from '../state/actions'; |
||||
import { AsyncRequestState } from '../utils/redux'; |
||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector'; |
||||
|
||||
export function useManagedAlertStateHistory(alertId: string) { |
||||
const dispatch = useDispatch(); |
||||
const history = useUnifiedAlertingSelector<AsyncRequestState<StateHistoryItem[]>>( |
||||
(state) => state.managedAlertStateHistory |
||||
); |
||||
|
||||
useEffect(() => { |
||||
dispatch(fetchGrafanaAnnotationsAction(alertId)); |
||||
}, [dispatch, alertId]); |
||||
|
||||
return history; |
||||
} |
||||
@ -0,0 +1,30 @@ |
||||
import React, { useMemo, useState } from 'react'; |
||||
import { Modal } from '@grafana/ui'; |
||||
import { StateHistory } from '../components/rules/StateHistory'; |
||||
|
||||
function useStateHistoryModal(alertId: string) { |
||||
const [showModal, setShowModal] = useState<boolean>(false); |
||||
|
||||
const StateHistoryModal = useMemo( |
||||
() => ( |
||||
<Modal |
||||
isOpen={showModal} |
||||
onDismiss={() => setShowModal(false)} |
||||
closeOnBackdropClick={true} |
||||
closeOnEscape={true} |
||||
title="State history" |
||||
> |
||||
<StateHistory alertId={alertId} /> |
||||
</Modal> |
||||
), |
||||
[alertId, showModal] |
||||
); |
||||
|
||||
return { |
||||
StateHistoryModal, |
||||
showStateHistoryModal: () => setShowModal(true), |
||||
hideStateHistoryModal: () => setShowModal(false), |
||||
}; |
||||
} |
||||
|
||||
export { useStateHistoryModal }; |
||||
Loading…
Reference in new issue