mirror of https://github.com/grafana/grafana
Alerting: Add alertmanager notifications tab (#35759)
* Add alertmanager notifications tab * Link to silences page from am alert * Include summary for alertmanager group * Fix colors for am state * Add horizontal dividing line * PR feedback * Add basic unit test for alert notificaitons * Rename Notificaitons component file * Polling interval to groups * Add alertmanager notifications tab * Link to silences page from am alert * Include summary for alertmanager group * PR feedback * Add basic unit test for alert notificaitons * Rename Notificaitons component file * Alerting: make alertmanager notifications view responsive (#36067) * refac DynamicTableWithGuidelines * more responsiveness fixes * Add more to tests * Add loading and alert state for notifications Co-authored-by: Domas <domas.lapinskas@grafana.com>pull/36536/head^2
parent
d9e500b654
commit
a0dac9c6d9
@ -0,0 +1,84 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { locationService, setDataSourceSrv } from '@grafana/runtime'; |
||||||
|
import { render, waitFor } from '@testing-library/react'; |
||||||
|
import { Provider } from 'react-redux'; |
||||||
|
import { Router } from 'react-router-dom'; |
||||||
|
import { fetchAlertGroups } from './api/alertmanager'; |
||||||
|
import { byTestId, byText } from 'testing-library-selector'; |
||||||
|
import { configureStore } from 'app/store/configureStore'; |
||||||
|
import { typeAsJestMock } from 'test/helpers/typeAsJestMock'; |
||||||
|
import AmNotifications from './AmNotifications'; |
||||||
|
import { mockAlertGroup, mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv } from './mocks'; |
||||||
|
import { DataSourceType } from './utils/datasource'; |
||||||
|
import userEvent from '@testing-library/user-event'; |
||||||
|
|
||||||
|
jest.mock('./api/alertmanager'); |
||||||
|
|
||||||
|
const mocks = { |
||||||
|
api: { |
||||||
|
fetchAlertGroups: typeAsJestMock(fetchAlertGroups), |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const renderAmNotifications = () => { |
||||||
|
const store = configureStore(); |
||||||
|
|
||||||
|
return render( |
||||||
|
<Provider store={store}> |
||||||
|
<Router history={locationService.getHistory()}> |
||||||
|
<AmNotifications /> |
||||||
|
</Router> |
||||||
|
</Provider> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const dataSources = { |
||||||
|
am: mockDataSource({ |
||||||
|
name: 'Alert Manager', |
||||||
|
type: DataSourceType.Alertmanager, |
||||||
|
}), |
||||||
|
}; |
||||||
|
|
||||||
|
const ui = { |
||||||
|
group: byTestId('notifications-group'), |
||||||
|
groupCollapseToggle: byTestId('notifications-group-collapse-toggle'), |
||||||
|
notificationsTable: byTestId('notifications-table'), |
||||||
|
row: byTestId('row'), |
||||||
|
collapseToggle: byTestId('collapse-toggle'), |
||||||
|
silenceButton: byText('Silence'), |
||||||
|
sourceButton: byText('See source'), |
||||||
|
}; |
||||||
|
|
||||||
|
describe('AmNotifications', () => { |
||||||
|
beforeAll(() => { |
||||||
|
mocks.api.fetchAlertGroups.mockImplementation(() => { |
||||||
|
return Promise.resolve([ |
||||||
|
mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }), |
||||||
|
mockAlertGroup(), |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
setDataSourceSrv(new MockDataSourceSrv(dataSources)); |
||||||
|
}); |
||||||
|
|
||||||
|
it('loads and shows groups', async () => { |
||||||
|
await renderAmNotifications(); |
||||||
|
|
||||||
|
await waitFor(() => expect(mocks.api.fetchAlertGroups).toHaveBeenCalled()); |
||||||
|
|
||||||
|
const groups = await ui.group.getAll(); |
||||||
|
|
||||||
|
expect(groups).toHaveLength(2); |
||||||
|
expect(groups[0]).toHaveTextContent('No grouping'); |
||||||
|
expect(groups[1]).toHaveTextContent('severity=warningregion=US-Central'); |
||||||
|
|
||||||
|
userEvent.click(ui.groupCollapseToggle.get(groups[0])); |
||||||
|
expect(ui.notificationsTable.get()).toBeDefined(); |
||||||
|
|
||||||
|
userEvent.click(ui.collapseToggle.get(ui.notificationsTable.get())); |
||||||
|
expect(ui.silenceButton.get(ui.notificationsTable.get())).toBeDefined(); |
||||||
|
expect(ui.sourceButton.get(ui.notificationsTable.get())).toBeDefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,62 @@ |
|||||||
|
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types'; |
||||||
|
import React, { useEffect } from 'react'; |
||||||
|
|
||||||
|
import { useDispatch } from 'react-redux'; |
||||||
|
|
||||||
|
import { AlertingPageWrapper } from './components/AlertingPageWrapper'; |
||||||
|
import { AlertManagerPicker } from './components/AlertManagerPicker'; |
||||||
|
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; |
||||||
|
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; |
||||||
|
import { fetchAlertGroupsAction } from './state/actions'; |
||||||
|
import { initialAsyncRequestState } from './utils/redux'; |
||||||
|
|
||||||
|
import { AmNotificationsGroup } from './components/amnotifications/AmNotificationsGroup'; |
||||||
|
import { NOTIFICATIONS_POLL_INTERVAL_MS } from './utils/constants'; |
||||||
|
import { Alert, LoadingPlaceholder } from '../../../../../packages/grafana-ui/src'; |
||||||
|
|
||||||
|
const AlertManagerNotifications = () => { |
||||||
|
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); |
||||||
|
const dispatch = useDispatch(); |
||||||
|
|
||||||
|
const alertGroups = useUnifiedAlertingSelector((state) => state.amAlertGroups) || initialAsyncRequestState; |
||||||
|
const loading = alertGroups[alertManagerSourceName || '']?.loading; |
||||||
|
const error = alertGroups[alertManagerSourceName || '']?.error; |
||||||
|
const results: AlertmanagerGroup[] = alertGroups[alertManagerSourceName || '']?.result || []; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
function fetchNotifications() { |
||||||
|
if (alertManagerSourceName) { |
||||||
|
dispatch(fetchAlertGroupsAction(alertManagerSourceName)); |
||||||
|
} |
||||||
|
} |
||||||
|
fetchNotifications(); |
||||||
|
const interval = setInterval(() => fetchNotifications, NOTIFICATIONS_POLL_INTERVAL_MS); |
||||||
|
return () => { |
||||||
|
clearInterval(interval); |
||||||
|
}; |
||||||
|
}, [dispatch, alertManagerSourceName]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<AlertingPageWrapper pageId="notifications"> |
||||||
|
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} /> |
||||||
|
{loading && <LoadingPlaceholder text="Loading notifications" />} |
||||||
|
{error && !loading && ( |
||||||
|
<Alert title={'Error loading notifications'} severity={'error'}> |
||||||
|
{error.message || 'Unknown error'} |
||||||
|
</Alert> |
||||||
|
)} |
||||||
|
{results && |
||||||
|
results.map((group, index) => { |
||||||
|
return ( |
||||||
|
<AmNotificationsGroup |
||||||
|
alertManagerSourceName={alertManagerSourceName || ''} |
||||||
|
key={`${JSON.stringify(group.labels)}-group-${index}`} |
||||||
|
group={group} |
||||||
|
/> |
||||||
|
); |
||||||
|
})} |
||||||
|
</AlertingPageWrapper> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default AlertManagerNotifications; |
@ -0,0 +1,76 @@ |
|||||||
|
import { css, cx } from '@emotion/css'; |
||||||
|
import { GrafanaTheme2 } from '@grafana/data'; |
||||||
|
import { useStyles2 } from '@grafana/ui'; |
||||||
|
import React from 'react'; |
||||||
|
import { DynamicTable, DynamicTableProps } from './DynamicTable'; |
||||||
|
|
||||||
|
export type DynamicTableWithGuidelinesProps<T> = Omit<DynamicTableProps<T>, 'renderPrefixHeader, renderPrefixCell'>; |
||||||
|
|
||||||
|
// DynamicTable, but renders visual guidelines on the left, for larger screen widths
|
||||||
|
export const DynamicTableWithGuidelines = <T extends object>({ |
||||||
|
renderExpandedContent, |
||||||
|
...props |
||||||
|
}: DynamicTableWithGuidelinesProps<T>) => { |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
return ( |
||||||
|
<DynamicTable |
||||||
|
renderExpandedContent={ |
||||||
|
renderExpandedContent |
||||||
|
? (item, index, items) => ( |
||||||
|
<> |
||||||
|
{!(index === items.length - 1) && <div className={cx(styles.contentGuideline, styles.guideline)} />} |
||||||
|
{renderExpandedContent(item, index, items)} |
||||||
|
</> |
||||||
|
) |
||||||
|
: undefined |
||||||
|
} |
||||||
|
renderPrefixHeader={() => ( |
||||||
|
<div className={styles.relative}> |
||||||
|
<div className={cx(styles.headerGuideline, styles.guideline)} /> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
renderPrefixCell={(_, index, items) => ( |
||||||
|
<div className={styles.relative}> |
||||||
|
<div className={cx(styles.topGuideline, styles.guideline)} /> |
||||||
|
{!(index === items.length - 1) && <div className={cx(styles.bottomGuideline, styles.guideline)} />} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export const getStyles = (theme: GrafanaTheme2) => ({ |
||||||
|
relative: css` |
||||||
|
position: relative; |
||||||
|
height: 100%; |
||||||
|
`,
|
||||||
|
guideline: css` |
||||||
|
left: -19px; |
||||||
|
border-left: 1px solid ${theme.colors.border.medium}; |
||||||
|
position: absolute; |
||||||
|
|
||||||
|
${theme.breakpoints.down('md')} { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
`,
|
||||||
|
topGuideline: css` |
||||||
|
width: 18px; |
||||||
|
border-bottom: 1px solid ${theme.colors.border.medium}; |
||||||
|
top: 0; |
||||||
|
bottom: 50%; |
||||||
|
`,
|
||||||
|
bottomGuideline: css` |
||||||
|
top: 50%; |
||||||
|
bottom: 0; |
||||||
|
`,
|
||||||
|
contentGuideline: css` |
||||||
|
top: 0; |
||||||
|
bottom: 0; |
||||||
|
left: -49px !important; |
||||||
|
`,
|
||||||
|
headerGuideline: css` |
||||||
|
top: -25px; |
||||||
|
bottom: 0; |
||||||
|
`,
|
||||||
|
}); |
@ -0,0 +1,92 @@ |
|||||||
|
import { css } from '@emotion/css'; |
||||||
|
import { GrafanaTheme2 } from '@grafana/data'; |
||||||
|
import { LinkButton, useStyles2 } from '@grafana/ui'; |
||||||
|
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types'; |
||||||
|
import { Labels } from 'app/types/unified-alerting-dto'; |
||||||
|
import React, { FC } from 'react'; |
||||||
|
import { makeAMLink } from '../../utils/misc'; |
||||||
|
import { AnnotationDetailsField } from '../AnnotationDetailsField'; |
||||||
|
|
||||||
|
interface AmNotificationsAlertDetailsProps { |
||||||
|
alertManagerSourceName: string; |
||||||
|
alert: AlertmanagerAlert; |
||||||
|
} |
||||||
|
|
||||||
|
export const AmNotificationsAlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ |
||||||
|
alert, |
||||||
|
alertManagerSourceName, |
||||||
|
}) => { |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<div className={styles.actionsRow}> |
||||||
|
{alert.status.state === AlertState.Suppressed && ( |
||||||
|
<LinkButton |
||||||
|
href={`${makeAMLink( |
||||||
|
'/alerting/silences', |
||||||
|
alertManagerSourceName |
||||||
|
)}&silenceIds=${alert.status.silencedBy.join(',')}`}
|
||||||
|
className={styles.button} |
||||||
|
icon={'bell'} |
||||||
|
size={'sm'} |
||||||
|
> |
||||||
|
Manage silences |
||||||
|
</LinkButton> |
||||||
|
)} |
||||||
|
{alert.status.state === AlertState.Active && ( |
||||||
|
<LinkButton |
||||||
|
href={`${makeAMLink('/alerting/silence/new', alertManagerSourceName)}&${getMatcherQueryParams( |
||||||
|
alert.labels |
||||||
|
)}`}
|
||||||
|
className={styles.button} |
||||||
|
icon={'bell-slash'} |
||||||
|
size={'sm'} |
||||||
|
> |
||||||
|
Silence |
||||||
|
</LinkButton> |
||||||
|
)} |
||||||
|
{alert.generatorURL && ( |
||||||
|
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}> |
||||||
|
See source |
||||||
|
</LinkButton> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
{Object.entries(alert.annotations).map(([annotationKey, annotationValue]) => ( |
||||||
|
<AnnotationDetailsField key={annotationKey} annotationKey={annotationKey} value={annotationValue} /> |
||||||
|
))} |
||||||
|
<div className={styles.receivers}> |
||||||
|
Receivers:{' '} |
||||||
|
{alert.receivers |
||||||
|
.map(({ name }) => name) |
||||||
|
.filter((name) => !!name) |
||||||
|
.join(', ')} |
||||||
|
</div> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({ |
||||||
|
button: css` |
||||||
|
& + & { |
||||||
|
margin-left: ${theme.spacing(1)}; |
||||||
|
} |
||||||
|
`,
|
||||||
|
actionsRow: css` |
||||||
|
padding: ${theme.spacing(2, 0)} !important; |
||||||
|
border-bottom: 1px solid ${theme.colors.border.medium}; |
||||||
|
`,
|
||||||
|
receivers: css` |
||||||
|
padding: ${theme.spacing(1, 0)}; |
||||||
|
`,
|
||||||
|
}); |
||||||
|
|
||||||
|
const getMatcherQueryParams = (labels: Labels) => { |
||||||
|
return `matchers=${encodeURIComponent( |
||||||
|
Object.entries(labels) |
||||||
|
.filter(([labelKey]) => !(labelKey.startsWith('__') && labelKey.endsWith('__'))) |
||||||
|
.map(([labelKey, labelValue]) => { |
||||||
|
return `${labelKey}=${labelValue}`; |
||||||
|
}) |
||||||
|
.join(',') |
||||||
|
)}`;
|
||||||
|
}; |
@ -0,0 +1,91 @@ |
|||||||
|
import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types'; |
||||||
|
import React, { useMemo } from 'react'; |
||||||
|
import { useStyles2 } from '@grafana/ui'; |
||||||
|
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data'; |
||||||
|
import { css } from '@emotion/css'; |
||||||
|
import { DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; |
||||||
|
import { AmAlertStateTag } from '../silences/AmAlertStateTag'; |
||||||
|
import { AlertLabels } from '../AlertLabels'; |
||||||
|
import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines'; |
||||||
|
import { AmNotificationsAlertDetails } from './AmNotificationsAlertDetails'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
alerts: AlertmanagerAlert[]; |
||||||
|
alertManagerSourceName: string; |
||||||
|
} |
||||||
|
|
||||||
|
type AmNotificationsAlertsTableColumnProps = DynamicTableColumnProps<AlertmanagerAlert>; |
||||||
|
type AmNotificationsAlertsTableItemProps = DynamicTableItemProps<AlertmanagerAlert>; |
||||||
|
|
||||||
|
export const AmNotificationsAlertsTable = ({ alerts, alertManagerSourceName }: Props) => { |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
|
||||||
|
const columns = useMemo( |
||||||
|
(): AmNotificationsAlertsTableColumnProps[] => [ |
||||||
|
{ |
||||||
|
id: 'state', |
||||||
|
label: 'State', |
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
renderCell: ({ data: alert }) => ( |
||||||
|
<> |
||||||
|
<AmAlertStateTag state={alert.status.state} /> |
||||||
|
<span className={styles.duration}> |
||||||
|
for{' '} |
||||||
|
{intervalToAbbreviatedDurationString({ |
||||||
|
start: new Date(alert.startsAt), |
||||||
|
end: new Date(alert.endsAt), |
||||||
|
})} |
||||||
|
</span> |
||||||
|
</> |
||||||
|
), |
||||||
|
size: '190px', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 'labels', |
||||||
|
label: 'Labels', |
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
renderCell: ({ data: { labels } }) => <AlertLabels className={styles.labels} labels={labels} />, |
||||||
|
size: 1, |
||||||
|
}, |
||||||
|
], |
||||||
|
[styles] |
||||||
|
); |
||||||
|
|
||||||
|
const items = useMemo( |
||||||
|
(): AmNotificationsAlertsTableItemProps[] => |
||||||
|
alerts.map((alert) => ({ |
||||||
|
id: alert.fingerprint, |
||||||
|
data: alert, |
||||||
|
})), |
||||||
|
[alerts] |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.tableWrapper} data-testid="notifications-table"> |
||||||
|
<DynamicTableWithGuidelines |
||||||
|
cols={columns} |
||||||
|
items={items} |
||||||
|
isExpandable={true} |
||||||
|
renderExpandedContent={({ data: alert }) => ( |
||||||
|
<AmNotificationsAlertDetails alert={alert} alertManagerSourceName={alertManagerSourceName} /> |
||||||
|
)} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({ |
||||||
|
tableWrapper: css` |
||||||
|
margin-top: ${theme.spacing(3)}; |
||||||
|
${theme.breakpoints.up('md')} { |
||||||
|
margin-left: ${theme.spacing(4.5)}; |
||||||
|
} |
||||||
|
`,
|
||||||
|
duration: css` |
||||||
|
margin-left: ${theme.spacing(1)}; |
||||||
|
font-size: ${theme.typography.bodySmall.fontSize}; |
||||||
|
`,
|
||||||
|
labels: css` |
||||||
|
padding-bottom: 0; |
||||||
|
`,
|
||||||
|
}); |
@ -0,0 +1,82 @@ |
|||||||
|
import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types'; |
||||||
|
import React, { useState } from 'react'; |
||||||
|
import { GrafanaTheme2 } from '@grafana/data'; |
||||||
|
import { useStyles2 } from '@grafana/ui'; |
||||||
|
import { css } from '@emotion/css'; |
||||||
|
import { AlertLabels } from '../AlertLabels'; |
||||||
|
import { AmNotificationsAlertsTable } from './AmNotificationsAlertsTable'; |
||||||
|
import { CollapseToggle } from '../CollapseToggle'; |
||||||
|
import { AmNotificationsGroupHeader } from './AmNotificationsGroupHeader'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
group: AlertmanagerGroup; |
||||||
|
alertManagerSourceName: string; |
||||||
|
} |
||||||
|
|
||||||
|
export const AmNotificationsGroup = ({ alertManagerSourceName, group }: Props) => { |
||||||
|
const [isCollapsed, setIsCollapsed] = useState<boolean>(true); |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.wrapper}> |
||||||
|
<div className={styles.header}> |
||||||
|
<div className={styles.group} data-testid="notifications-group"> |
||||||
|
<CollapseToggle |
||||||
|
isCollapsed={isCollapsed} |
||||||
|
onToggle={() => setIsCollapsed(!isCollapsed)} |
||||||
|
data-testid="notifications-group-collapse-toggle" |
||||||
|
/> |
||||||
|
{Object.keys(group.labels).length ? ( |
||||||
|
<AlertLabels className={styles.headerLabels} labels={group.labels} /> |
||||||
|
) : ( |
||||||
|
<span>No grouping</span> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
<AmNotificationsGroupHeader group={group} /> |
||||||
|
</div> |
||||||
|
{!isCollapsed && ( |
||||||
|
<AmNotificationsAlertsTable alertManagerSourceName={alertManagerSourceName} alerts={group.alerts} /> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({ |
||||||
|
wrapper: css` |
||||||
|
& + & { |
||||||
|
margin-top: ${theme.spacing(2)}; |
||||||
|
} |
||||||
|
`,
|
||||||
|
headerLabels: css` |
||||||
|
padding-bottom: 0 !important; |
||||||
|
margin-bottom: -${theme.spacing(0.5)}; |
||||||
|
`,
|
||||||
|
header: css` |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
flex-wrap: wrap; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
padding: ${theme.spacing(1, 1, 1, 0)}; |
||||||
|
background-color: ${theme.colors.background.secondary}; |
||||||
|
width: 100%; |
||||||
|
`,
|
||||||
|
group: css` |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
`,
|
||||||
|
summary: css``, |
||||||
|
spanElement: css` |
||||||
|
margin-left: ${theme.spacing(0.5)}; |
||||||
|
`,
|
||||||
|
[AlertState.Active]: css` |
||||||
|
color: ${theme.colors.error.main}; |
||||||
|
`,
|
||||||
|
[AlertState.Suppressed]: css` |
||||||
|
color: ${theme.colors.primary.main}; |
||||||
|
`,
|
||||||
|
[AlertState.Unprocessed]: css` |
||||||
|
color: ${theme.colors.secondary.main}; |
||||||
|
`,
|
||||||
|
}); |
@ -0,0 +1,49 @@ |
|||||||
|
import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types'; |
||||||
|
import React from 'react'; |
||||||
|
import { GrafanaTheme2 } from '@grafana/data'; |
||||||
|
import { useStyles2 } from '@grafana/ui'; |
||||||
|
import { css } from '@emotion/css'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
group: AlertmanagerGroup; |
||||||
|
} |
||||||
|
|
||||||
|
export const AmNotificationsGroupHeader = ({ group }: Props) => { |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
const total = group.alerts.length; |
||||||
|
const countByStatus = group.alerts.reduce((statusObj, alert) => { |
||||||
|
if (statusObj[alert.status.state]) { |
||||||
|
statusObj[alert.status.state] += 1; |
||||||
|
} else { |
||||||
|
statusObj[alert.status.state] = 1; |
||||||
|
} |
||||||
|
return statusObj; |
||||||
|
}, {} as Record<AlertState, number>); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.summary}> |
||||||
|
{`${total} alerts: `} |
||||||
|
{Object.entries(countByStatus).map(([state, count], index) => { |
||||||
|
return ( |
||||||
|
<span key={`${JSON.stringify(group.labels)}-notifications-${index}`} className={styles[state as AlertState]}> |
||||||
|
{index > 0 && ', '} |
||||||
|
{`${count} ${state}`} |
||||||
|
</span> |
||||||
|
); |
||||||
|
})} |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({ |
||||||
|
summary: css``, |
||||||
|
[AlertState.Active]: css` |
||||||
|
color: ${theme.colors.error.main}; |
||||||
|
`,
|
||||||
|
[AlertState.Suppressed]: css` |
||||||
|
color: ${theme.colors.primary.main}; |
||||||
|
`,
|
||||||
|
[AlertState.Unprocessed]: css` |
||||||
|
color: ${theme.colors.secondary.main}; |
||||||
|
`,
|
||||||
|
}); |
Loading…
Reference in new issue