mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add alertmanager notifications panel (#37078)
* Add filter parsing to rule list filters * Add unit tests for label parsing * Make label operators an enum * add example for parsing function * Update labels operator regex * Use tooltip for query syntax example * refactor to use Matchers for filtering * wip: initial alertmanager notifications panel * Panel for alertmanager notificaitons * add filtering for notifications list * remove icon * rename am notifications to alert groups * naming fixes * Feature toggle * Add toggle for expand all * add pluralize * add action buttons * test work in progress * Tests for alert groups panel * Add useEffect for expandAll prop change * Set panel to alpha state * Fix colors * fix polling interval callback Co-authored-by: Domas <domas.lapinskas@grafana.com> Co-authored-by: Domas <domas.lapinskas@grafana.com>
This commit is contained in:
@@ -2,10 +2,11 @@ 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';
|
||||
import { getMatcherQueryParams } from '../../utils/matchers';
|
||||
|
||||
interface AmNotificationsAlertDetailsProps {
|
||||
alertManagerSourceName: string;
|
||||
@@ -79,14 +80,3 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
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(',')
|
||||
)}`;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
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';
|
||||
import { getNotificationsTextColors } from '../../styles/notifications';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
interface Props {
|
||||
group: AlertmanagerGroup;
|
||||
}
|
||||
|
||||
export const AmNotificationsGroupHeader = ({ group }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const textStyles = useStyles2(getNotificationsTextColors);
|
||||
const total = group.alerts.length;
|
||||
const countByStatus = group.alerts.reduce((statusObj, alert) => {
|
||||
if (statusObj[alert.status.state]) {
|
||||
@@ -21,11 +21,14 @@ export const AmNotificationsGroupHeader = ({ group }: Props) => {
|
||||
}, {} as Record<AlertState, number>);
|
||||
|
||||
return (
|
||||
<div className={styles.summary}>
|
||||
{`${total} alerts: `}
|
||||
<div>
|
||||
{`${total} ${pluralize('alert', total)}: `}
|
||||
{Object.entries(countByStatus).map(([state, count], index) => {
|
||||
return (
|
||||
<span key={`${JSON.stringify(group.labels)}-notifications-${index}`} className={styles[state as AlertState]}>
|
||||
<span
|
||||
key={`${JSON.stringify(group.labels)}-notifications-${index}`}
|
||||
className={textStyles[state as AlertState]}
|
||||
>
|
||||
{index > 0 && ', '}
|
||||
{`${count} ${state}`}
|
||||
</span>
|
||||
@@ -34,16 +37,3 @@ export const AmNotificationsGroupHeader = ({ group }: Props) => {
|
||||
</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};
|
||||
`,
|
||||
});
|
||||
|
||||
15
public/app/features/alerting/unified/styles/notifications.ts
Normal file
15
public/app/features/alerting/unified/styles/notifications.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
import { AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
export const getNotificationsTextColors = (theme: GrafanaTheme2) => ({
|
||||
[AlertState.Active]: css`
|
||||
color: ${theme.colors.error.text};
|
||||
`,
|
||||
[AlertState.Suppressed]: css`
|
||||
color: ${theme.colors.primary.text};
|
||||
`,
|
||||
[AlertState.Unprocessed]: css`
|
||||
color: ${theme.colors.secondary.text};
|
||||
`,
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { Labels } from '@grafana/data';
|
||||
import { parseMatcher } from './alertmanager';
|
||||
|
||||
// parses comma separated matchers like "foo=bar,baz=~bad*" into SilenceMatcher[]
|
||||
@@ -8,3 +9,14 @@ export function parseQueryParamMatchers(paramValue: string): Matcher[] {
|
||||
.filter((x) => !!x.trim())
|
||||
.map((x) => parseMatcher(x.trim()));
|
||||
}
|
||||
|
||||
export const getMatcherQueryParams = (labels: Labels) => {
|
||||
return `matchers=${encodeURIComponent(
|
||||
Object.entries(labels)
|
||||
.filter(([labelKey]) => !(labelKey.startsWith('__') && labelKey.endsWith('__')))
|
||||
.map(([labelKey, labelValue]) => {
|
||||
return `${labelKey}=${labelValue}`;
|
||||
})
|
||||
.join(',')
|
||||
)}`;
|
||||
};
|
||||
|
||||
@@ -66,6 +66,7 @@ import * as debugPanel from 'app/plugins/panel/debug/module';
|
||||
import * as welcomeBanner from 'app/plugins/panel/welcome/module';
|
||||
import * as nodeGraph from 'app/plugins/panel/nodeGraph/module';
|
||||
import * as histogramPanel from 'app/plugins/panel/histogram/module';
|
||||
import * as alertGroupsPanel from 'app/plugins/panel/alertGroups/module';
|
||||
|
||||
// Async loaded panels
|
||||
const geomapPanel = async () => await import(/* webpackChunkName: "geomapPanel" */ 'app/plugins/panel/geomap/module');
|
||||
@@ -119,6 +120,7 @@ const builtInPlugins: any = {
|
||||
'app/plugins/panel/welcome/module': welcomeBanner,
|
||||
'app/plugins/panel/nodeGraph/module': nodeGraph,
|
||||
'app/plugins/panel/histogram/module': histogramPanel,
|
||||
'app/plugins/panel/alertGroups/module': alertGroupsPanel,
|
||||
};
|
||||
|
||||
export default builtInPlugins;
|
||||
|
||||
127
public/app/plugins/panel/alertGroups/AlertGroup.tsx
Normal file
127
public/app/plugins/panel/alertGroups/AlertGroup.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
|
||||
import { useStyles2, LinkButton } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { AlertLabels } from 'app/features/alerting/unified/components/AlertLabels';
|
||||
import { AmNotificationsGroupHeader } from 'app/features/alerting/unified/components/amnotifications/AmNotificationsGroupHeader';
|
||||
import { CollapseToggle } from 'app/features/alerting/unified/components/CollapseToggle';
|
||||
import { getNotificationsTextColors } from 'app/features/alerting/unified/styles/notifications';
|
||||
import { makeAMLink } from 'app/features/alerting/unified/utils/misc';
|
||||
import { getMatcherQueryParams } from 'app/features/alerting/unified/utils/matchers';
|
||||
|
||||
type Props = {
|
||||
alertManagerSourceName: string;
|
||||
group: AlertmanagerGroup;
|
||||
expandAll: boolean;
|
||||
};
|
||||
|
||||
export const AlertGroup = ({ alertManagerSourceName, group, expandAll }: Props) => {
|
||||
const [showAlerts, setShowAlerts] = useState(expandAll);
|
||||
const styles = useStyles2(getStyles);
|
||||
const textStyles = useStyles2(getNotificationsTextColors);
|
||||
|
||||
useEffect(() => setShowAlerts(expandAll), [expandAll]);
|
||||
|
||||
return (
|
||||
<div className={styles.group} data-testid="alert-group">
|
||||
{Object.keys(group.labels).length > 0 ? (
|
||||
<AlertLabels labels={group.labels} />
|
||||
) : (
|
||||
<div className={styles.noGroupingText}>No grouping</div>
|
||||
)}
|
||||
<div className={styles.row}>
|
||||
<CollapseToggle isCollapsed={!showAlerts} onToggle={() => setShowAlerts(!showAlerts)} />{' '}
|
||||
<AmNotificationsGroupHeader group={group} />
|
||||
</div>
|
||||
{showAlerts && (
|
||||
<div className={styles.alerts}>
|
||||
{group.alerts.map((alert, index) => {
|
||||
const state = alert.status.state.toUpperCase();
|
||||
const interval = intervalToAbbreviatedDurationString({
|
||||
start: new Date(alert.startsAt),
|
||||
end: Date.now(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-testid={'alert-group-alert'} className={styles.alert} key={`${alert.fingerprint}-${index}`}>
|
||||
<div>
|
||||
<span className={textStyles[alert.status.state]}>{state} </span>for {interval}
|
||||
</div>
|
||||
<div>
|
||||
<AlertLabels labels={alert.labels} />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
noGroupingText: css`
|
||||
height: ${theme.spacing(4)};
|
||||
`,
|
||||
group: css`
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
margin: ${theme.spacing(0.5, 1, 0.5, 1)};
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
row: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`,
|
||||
alerts: css`
|
||||
margin: ${theme.spacing(0, 2, 0, 4)};
|
||||
`,
|
||||
alert: css`
|
||||
padding: ${theme.spacing(1, 0)};
|
||||
& + & {
|
||||
border-top: 1px solid ${theme.colors.border.medium};
|
||||
}
|
||||
`,
|
||||
button: css`
|
||||
& + & {
|
||||
margin-left: ${theme.spacing(1)};
|
||||
}
|
||||
`,
|
||||
actionsRow: css`
|
||||
padding: ${theme.spacing(1, 0)};
|
||||
`,
|
||||
});
|
||||
148
public/app/plugins/panel/alertGroups/AlertGroupsPanel.test.tsx
Normal file
148
public/app/plugins/panel/alertGroups/AlertGroupsPanel.test.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { AlertGroupsPanel } from './AlertGroupsPanel';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { byTestId } from 'testing-library-selector';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AlertGroupPanelOptions } from './types';
|
||||
import { getDefaultTimeRange, LoadingState, PanelProps, FieldConfigSource } from '@grafana/data';
|
||||
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
||||
import { fetchAlertGroups } from 'app/features/alerting/unified/api/alertmanager';
|
||||
import {
|
||||
mockAlertGroup,
|
||||
mockAlertmanagerAlert,
|
||||
mockDataSource,
|
||||
MockDataSourceSrv,
|
||||
} from 'app/features/alerting/unified/mocks';
|
||||
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
|
||||
jest.mock('app/features/alerting/unified/api/alertmanager');
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
buildInfo: {},
|
||||
panels: {},
|
||||
featureToggles: {
|
||||
ngalert: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mocks = {
|
||||
api: {
|
||||
fetchAlertGroups: typeAsJestMock(fetchAlertGroups),
|
||||
},
|
||||
};
|
||||
|
||||
const dataSources = {
|
||||
am: mockDataSource({
|
||||
name: 'Alertmanager',
|
||||
type: DataSourceType.Alertmanager,
|
||||
}),
|
||||
};
|
||||
|
||||
const defaultOptions: AlertGroupPanelOptions = {
|
||||
labels: '',
|
||||
alertmanager: 'Alertmanager',
|
||||
expandAll: false,
|
||||
};
|
||||
|
||||
const defaultProps: PanelProps<AlertGroupPanelOptions> = {
|
||||
data: { state: LoadingState.Done, series: [], timeRange: getDefaultTimeRange() },
|
||||
id: 1,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
timeZone: 'utc',
|
||||
options: defaultOptions,
|
||||
eventBus: {
|
||||
subscribe: jest.fn(),
|
||||
getStream: () =>
|
||||
({
|
||||
subscribe: jest.fn(),
|
||||
} as any),
|
||||
publish: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
newScopedBus: jest.fn(),
|
||||
},
|
||||
fieldConfig: ({} as unknown) as FieldConfigSource,
|
||||
height: 400,
|
||||
onChangeTimeRange: jest.fn(),
|
||||
onFieldConfigChange: jest.fn(),
|
||||
onOptionsChange: jest.fn(),
|
||||
renderCounter: 1,
|
||||
replaceVariables: jest.fn(),
|
||||
title: 'Alert groups test',
|
||||
transparent: false,
|
||||
width: 320,
|
||||
};
|
||||
|
||||
const renderPanel = (options: AlertGroupPanelOptions = defaultOptions) => {
|
||||
const store = configureStore();
|
||||
const dash: any = { id: 1, formatDate: (time: number) => new Date(time).toISOString() };
|
||||
const dashSrv: any = { getCurrent: () => dash };
|
||||
setDashboardSrv(dashSrv);
|
||||
|
||||
defaultProps.options = options;
|
||||
const props = { ...defaultProps };
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<AlertGroupsPanel {...props} />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const ui = {
|
||||
group: byTestId('alert-group'),
|
||||
alert: byTestId('alert-group-alert'),
|
||||
};
|
||||
|
||||
describe('AlertGroupsPanel', () => {
|
||||
beforeAll(() => {
|
||||
mocks.api.fetchAlertGroups.mockImplementation(() => {
|
||||
return Promise.resolve([
|
||||
mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }),
|
||||
mockAlertGroup(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
});
|
||||
|
||||
it('renders the panel with the groups', async () => {
|
||||
await renderPanel();
|
||||
|
||||
await waitFor(() => expect(mocks.api.fetchAlertGroups).toHaveBeenCalled());
|
||||
const groups = ui.group.getAll();
|
||||
|
||||
expect(groups).toHaveLength(2);
|
||||
|
||||
expect(groups[0]).toHaveTextContent('No grouping');
|
||||
expect(groups[1]).toHaveTextContent('severity=warningregion=US-Central');
|
||||
|
||||
const alerts = ui.alert.queryAll();
|
||||
expect(alerts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders panel with groups expanded', async () => {
|
||||
await renderPanel({ labels: '', alertmanager: 'Alertmanager', expandAll: true });
|
||||
|
||||
await waitFor(() => expect(mocks.api.fetchAlertGroups).toHaveBeenCalled());
|
||||
const alerts = ui.alert.queryAll();
|
||||
expect(alerts).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('filters alerts by label filter', async () => {
|
||||
await renderPanel({ labels: 'region=US-Central', alertmanager: 'Alertmanager', expandAll: true });
|
||||
|
||||
await waitFor(() => expect(mocks.api.fetchAlertGroups).toHaveBeenCalled());
|
||||
const alerts = ui.alert.queryAll();
|
||||
|
||||
expect(alerts).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
66
public/app/plugins/panel/alertGroups/AlertGroupsPanel.tsx
Normal file
66
public/app/plugins/panel/alertGroups/AlertGroupsPanel.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { AlertmanagerGroup, Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { fetchAlertGroupsAction } from 'app/features/alerting/unified/state/actions';
|
||||
import { initialAsyncRequestState } from 'app/features/alerting/unified/utils/redux';
|
||||
import { NOTIFICATIONS_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants';
|
||||
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
|
||||
|
||||
import { AlertGroup } from './AlertGroup';
|
||||
import { AlertGroupPanelOptions } from './types';
|
||||
import { parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
|
||||
import { useFilteredGroups } from './useFilteredGroups';
|
||||
|
||||
export const AlertGroupsPanel = (props: PanelProps<AlertGroupPanelOptions>) => {
|
||||
const dispatch = useDispatch();
|
||||
const isAlertingEnabled = config.featureToggles.ngalert;
|
||||
|
||||
const expandAll = props.options.expandAll;
|
||||
const alertManagerSourceName = props.options.alertmanager;
|
||||
|
||||
const alertGroups = useUnifiedAlertingSelector((state) => state.amAlertGroups) || initialAsyncRequestState;
|
||||
const results: AlertmanagerGroup[] = alertGroups[alertManagerSourceName || '']?.result || [];
|
||||
const matchers: Matcher[] = props.options.labels ? parseMatchers(props.options.labels) : [];
|
||||
|
||||
const filteredResults = useFilteredGroups(results, matchers);
|
||||
|
||||
useEffect(() => {
|
||||
function fetchNotifications() {
|
||||
if (alertManagerSourceName) {
|
||||
dispatch(fetchAlertGroupsAction(alertManagerSourceName));
|
||||
}
|
||||
}
|
||||
fetchNotifications();
|
||||
const interval = setInterval(fetchNotifications, NOTIFICATIONS_POLL_INTERVAL_MS);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [dispatch, alertManagerSourceName]);
|
||||
|
||||
const hasResults = filteredResults.length > 0;
|
||||
|
||||
return (
|
||||
<CustomScrollbar autoHeightMax="100%" autoHeightMin="100%">
|
||||
{isAlertingEnabled && (
|
||||
<div>
|
||||
{hasResults &&
|
||||
filteredResults.map((group) => {
|
||||
return (
|
||||
<AlertGroup
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
key={JSON.stringify(group.labels)}
|
||||
group={group}
|
||||
expandAll={expandAll}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!hasResults && 'No alerts'}
|
||||
</div>
|
||||
)}
|
||||
</CustomScrollbar>
|
||||
);
|
||||
};
|
||||
39
public/app/plugins/panel/alertGroups/module.tsx
Normal file
39
public/app/plugins/panel/alertGroups/module.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { AlertGroupPanelOptions } from './types';
|
||||
import { AlertGroupsPanel } from './AlertGroupsPanel';
|
||||
import { AlertManagerPicker } from 'app/features/alerting/unified/components/AlertManagerPicker';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
export const plugin = new PanelPlugin<AlertGroupPanelOptions>(AlertGroupsPanel).setPanelOptions((builder) => {
|
||||
return builder
|
||||
.addCustomEditor({
|
||||
name: 'Alertmanager',
|
||||
path: 'alertmanager',
|
||||
id: 'alertmanager',
|
||||
defaultValue: GRAFANA_RULES_SOURCE_NAME,
|
||||
category: ['Options'],
|
||||
editor: function RenderAlertmanagerPicker(props) {
|
||||
return (
|
||||
<AlertManagerPicker
|
||||
current={props.value}
|
||||
onChange={(alertManagerSourceName) => {
|
||||
return props.onChange(alertManagerSourceName);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
name: 'Expand all by default',
|
||||
path: 'expandAll',
|
||||
defaultValue: false,
|
||||
category: ['Options'],
|
||||
})
|
||||
.addTextInput({
|
||||
description: 'Filter results by matching labels, ex: env=production,severity=~critical|warning',
|
||||
name: 'Labels',
|
||||
path: 'labels',
|
||||
category: ['Filter'],
|
||||
});
|
||||
});
|
||||
15
public/app/plugins/panel/alertGroups/plugin.json
Normal file
15
public/app/plugins/panel/alertGroups/plugin.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Alert groups",
|
||||
"id": "alertGroups",
|
||||
"state": "alpha",
|
||||
|
||||
"skipDataQuery": true,
|
||||
"info": {
|
||||
"description": "Shows alertmanager alerts grouped by labels",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
public/app/plugins/panel/alertGroups/types.ts
Normal file
5
public/app/plugins/panel/alertGroups/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface AlertGroupPanelOptions {
|
||||
labels: string;
|
||||
alertmanager: string;
|
||||
expandAll: boolean;
|
||||
}
|
||||
14
public/app/plugins/panel/alertGroups/useFilteredGroups.ts
Normal file
14
public/app/plugins/panel/alertGroups/useFilteredGroups.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { labelsMatchMatchers } from 'app/features/alerting/unified/utils/alertmanager';
|
||||
import { AlertmanagerGroup, Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useFilteredGroups = (groups: AlertmanagerGroup[], matchers: Matcher[]): AlertmanagerGroup[] => {
|
||||
return useMemo(() => {
|
||||
return groups.filter((group) => {
|
||||
return (
|
||||
labelsMatchMatchers(group.labels, matchers) ||
|
||||
group.alerts.some((alert) => labelsMatchMatchers(alert.labels, matchers))
|
||||
);
|
||||
});
|
||||
}, [groups, matchers]);
|
||||
};
|
||||
Reference in New Issue
Block a user