mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add custom grouping for alert groups (#37378)
* Group alertmangaer alerts by custom grouping * Filter am groups * Style filter components * Style filter bar and add clear functionality * rename components to alert group * use query params for group filters * filter style improvements * add tests for group by * Add grouping banner to better highlight groupings * clean up hook logic
This commit is contained in:
parent
6ed60c0bec
commit
df791ae2af
@ -210,7 +210,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
{Text: "Alert rules", Id: "alert-list", Url: hs.Cfg.AppSubURL + "/alerting/list", Icon: "list-ul"},
|
{Text: "Alert rules", Id: "alert-list", Url: hs.Cfg.AppSubURL + "/alerting/list", Icon: "list-ul"},
|
||||||
}
|
}
|
||||||
if hs.Cfg.IsNgAlertEnabled() {
|
if hs.Cfg.IsNgAlertEnabled() {
|
||||||
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Notifications", Id: "notifications", Url: hs.Cfg.AppSubURL + "/alerting/alertmanager", Icon: "layer-group"})
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Alert groups", Id: "groups", Url: hs.Cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"})
|
||||||
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Silences", Id: "silences", Url: hs.Cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Silences", Id: "silences", Url: hs.Cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
|
||||||
}
|
}
|
||||||
if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR {
|
if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR {
|
||||||
|
151
public/app/features/alerting/unified/AlertGroups.test.tsx
Normal file
151
public/app/features/alerting/unified/AlertGroups.test.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
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 { byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
||||||
|
import AlertGroups from './AlertGroups';
|
||||||
|
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()}>
|
||||||
|
<AlertGroups />
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataSources = {
|
||||||
|
am: mockDataSource({
|
||||||
|
name: 'Alertmanager',
|
||||||
|
type: DataSourceType.Alertmanager,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ui = {
|
||||||
|
group: byTestId('alert-group'),
|
||||||
|
groupCollapseToggle: byTestId('alert-group-collapse-toggle'),
|
||||||
|
groupTable: byTestId('alert-group-table'),
|
||||||
|
row: byTestId('row'),
|
||||||
|
collapseToggle: byTestId('collapse-toggle'),
|
||||||
|
silenceButton: byText('Silence'),
|
||||||
|
sourceButton: byText('See source'),
|
||||||
|
matcherInput: byTestId('search-query-input'),
|
||||||
|
groupByContainer: byTestId('group-by-container'),
|
||||||
|
groupByInput: byRole('textbox', { name: /group by label keys/i }),
|
||||||
|
clearButton: byRole('button', { name: 'Clear filters' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('AlertGroups', () => {
|
||||||
|
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 () => {
|
||||||
|
renderAmNotifications();
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
userEvent.click(ui.groupCollapseToggle.get(groups[0]));
|
||||||
|
expect(ui.groupTable.get()).toBeDefined();
|
||||||
|
|
||||||
|
userEvent.click(ui.collapseToggle.get(ui.groupTable.get()));
|
||||||
|
expect(ui.silenceButton.get(ui.groupTable.get())).toBeDefined();
|
||||||
|
expect(ui.sourceButton.get(ui.groupTable.get())).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should group by custom grouping', async () => {
|
||||||
|
const regions = ['NASA', 'EMEA', 'APAC'];
|
||||||
|
mocks.api.fetchAlertGroups.mockImplementation(() => {
|
||||||
|
const groups = regions.map((region) =>
|
||||||
|
mockAlertGroup({
|
||||||
|
labels: { region },
|
||||||
|
alerts: [
|
||||||
|
mockAlertmanagerAlert({ labels: { region, appName: 'billing', env: 'production' } }),
|
||||||
|
mockAlertmanagerAlert({ labels: { region, appName: 'auth', env: 'staging', uniqueLabel: 'true' } }),
|
||||||
|
mockAlertmanagerAlert({ labels: { region, appName: 'frontend', env: 'production' } }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return Promise.resolve(groups);
|
||||||
|
});
|
||||||
|
|
||||||
|
renderAmNotifications();
|
||||||
|
await waitFor(() => expect(mocks.api.fetchAlertGroups).toHaveBeenCalled());
|
||||||
|
let groups = ui.group.getAll();
|
||||||
|
const groupByInput = ui.groupByInput.get();
|
||||||
|
const groupByWrapper = ui.groupByContainer.get();
|
||||||
|
|
||||||
|
expect(groups).toHaveLength(3);
|
||||||
|
expect(groups[0]).toHaveTextContent('region=NASA');
|
||||||
|
expect(groups[1]).toHaveTextContent('region=EMEA');
|
||||||
|
expect(groups[2]).toHaveTextContent('region=APAC');
|
||||||
|
|
||||||
|
userEvent.type(groupByInput, 'appName{enter}');
|
||||||
|
|
||||||
|
await waitFor(() => expect(groupByWrapper).toHaveTextContent('appName'));
|
||||||
|
|
||||||
|
groups = ui.group.getAll();
|
||||||
|
|
||||||
|
await waitFor(() => expect(ui.clearButton.get()).toBeInTheDocument());
|
||||||
|
expect(groups).toHaveLength(3);
|
||||||
|
expect(groups[0]).toHaveTextContent('appName=billing');
|
||||||
|
expect(groups[1]).toHaveTextContent('appName=auth');
|
||||||
|
expect(groups[2]).toHaveTextContent('appName=frontend');
|
||||||
|
|
||||||
|
userEvent.click(ui.clearButton.get());
|
||||||
|
await waitFor(() => expect(groupByWrapper).not.toHaveTextContent('appName'));
|
||||||
|
|
||||||
|
userEvent.type(groupByInput, 'env{enter}');
|
||||||
|
await waitFor(() => expect(groupByWrapper).toHaveTextContent('env'));
|
||||||
|
|
||||||
|
groups = ui.group.getAll();
|
||||||
|
|
||||||
|
expect(groups).toHaveLength(2);
|
||||||
|
expect(groups[0]).toHaveTextContent('env=production');
|
||||||
|
expect(groups[1]).toHaveTextContent('env=staging');
|
||||||
|
|
||||||
|
userEvent.click(ui.clearButton.get());
|
||||||
|
await waitFor(() => expect(groupByWrapper).not.toHaveTextContent('env'));
|
||||||
|
|
||||||
|
userEvent.type(groupByInput, 'uniqueLabel{enter}');
|
||||||
|
await waitFor(() => expect(groupByWrapper).toHaveTextContent('uniqueLabel'));
|
||||||
|
|
||||||
|
groups = ui.group.getAll();
|
||||||
|
expect(groups).toHaveLength(2);
|
||||||
|
expect(groups[0]).toHaveTextContent('No grouping');
|
||||||
|
expect(groups[1]).toHaveTextContent('uniqueLabel=true');
|
||||||
|
});
|
||||||
|
});
|
80
public/app/features/alerting/unified/AlertGroups.tsx
Normal file
80
public/app/features/alerting/unified/AlertGroups.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { Alert, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
|
||||||
|
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||||
|
import { AlertGroup } from './components/alert-groups/AlertGroup';
|
||||||
|
import { AlertGroupFilter } from './components/alert-groups/AlertGroupFilter';
|
||||||
|
import { fetchAlertGroupsAction } from './state/actions';
|
||||||
|
|
||||||
|
import { initialAsyncRequestState } from './utils/redux';
|
||||||
|
import { getFiltersFromUrlParams } from './utils/misc';
|
||||||
|
import { NOTIFICATIONS_POLL_INTERVAL_MS } from './utils/constants';
|
||||||
|
|
||||||
|
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
|
||||||
|
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||||
|
import { useGroupedAlerts } from './hooks/useGroupedAlerts';
|
||||||
|
import { useFilteredAmGroups } from './hooks/useFilteredAmGroups';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
const AlertGroups = () => {
|
||||||
|
const [alertManagerSourceName] = useAlertManagerSourceName();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [queryParams] = useQueryParams();
|
||||||
|
const { groupBy = [] } = getFiltersFromUrlParams(queryParams);
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const alertGroups = useUnifiedAlertingSelector((state) => state.amAlertGroups);
|
||||||
|
const { loading, error, result: results = [] } =
|
||||||
|
alertGroups[alertManagerSourceName || ''] ?? initialAsyncRequestState;
|
||||||
|
|
||||||
|
const groupedAlerts = useGroupedAlerts(results, groupBy);
|
||||||
|
const filteredAlertGroups = useFilteredAmGroups(groupedAlerts);
|
||||||
|
|
||||||
|
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="groups">
|
||||||
|
<AlertGroupFilter groups={results} />
|
||||||
|
{loading && <LoadingPlaceholder text="Loading notifications" />}
|
||||||
|
{error && !loading && (
|
||||||
|
<Alert title={'Error loading notifications'} severity={'error'}>
|
||||||
|
{error.message || 'Unknown error'}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{results &&
|
||||||
|
filteredAlertGroups.map((group, index) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={`${JSON.stringify(group.labels)}-group-${index}`}>
|
||||||
|
{((index === 1 && Object.keys(filteredAlertGroups[0].labels).length === 0) ||
|
||||||
|
(index === 0 && Object.keys(group.labels).length > 0)) && (
|
||||||
|
<p className={styles.groupingBanner}>Grouped by: {Object.keys(group.labels).join(', ')}</p>
|
||||||
|
)}
|
||||||
|
<AlertGroup alertManagerSourceName={alertManagerSourceName || ''} group={group} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AlertingPageWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
groupingBanner: css`
|
||||||
|
margin: ${theme.spacing(2, 0)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AlertGroups;
|
@ -1,84 +0,0 @@
|
|||||||
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: 'Alertmanager',
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,62 +0,0 @@
|
|||||||
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 '@grafana/ui';
|
|
||||||
|
|
||||||
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;
|
|
@ -13,10 +13,7 @@ interface AmNotificationsAlertDetailsProps {
|
|||||||
alert: AlertmanagerAlert;
|
alert: AlertmanagerAlert;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AmNotificationsAlertDetails: FC<AmNotificationsAlertDetailsProps> = ({
|
export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => {
|
||||||
alert,
|
|
||||||
alertManagerSourceName,
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
@ -4,27 +4,27 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { AlertLabels } from '../AlertLabels';
|
import { AlertLabels } from '../AlertLabels';
|
||||||
import { AmNotificationsAlertsTable } from './AmNotificationsAlertsTable';
|
import { AlertGroupAlertsTable } from './AlertGroupAlertsTable';
|
||||||
import { CollapseToggle } from '../CollapseToggle';
|
import { CollapseToggle } from '../CollapseToggle';
|
||||||
import { AmNotificationsGroupHeader } from './AmNotificationsGroupHeader';
|
import { AlertGroupHeader } from './AlertGroupHeader';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
group: AlertmanagerGroup;
|
group: AlertmanagerGroup;
|
||||||
alertManagerSourceName: string;
|
alertManagerSourceName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AmNotificationsGroup = ({ alertManagerSourceName, group }: Props) => {
|
export const AlertGroup = ({ alertManagerSourceName, group }: Props) => {
|
||||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(true);
|
const [isCollapsed, setIsCollapsed] = useState<boolean>(true);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.group} data-testid="notifications-group">
|
<div className={styles.group} data-testid="alert-group">
|
||||||
<CollapseToggle
|
<CollapseToggle
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
onToggle={() => setIsCollapsed(!isCollapsed)}
|
onToggle={() => setIsCollapsed(!isCollapsed)}
|
||||||
data-testid="notifications-group-collapse-toggle"
|
data-testid="alert-group-collapse-toggle"
|
||||||
/>
|
/>
|
||||||
{Object.keys(group.labels).length ? (
|
{Object.keys(group.labels).length ? (
|
||||||
<AlertLabels className={styles.headerLabels} labels={group.labels} />
|
<AlertLabels className={styles.headerLabels} labels={group.labels} />
|
||||||
@ -32,11 +32,9 @@ export const AmNotificationsGroup = ({ alertManagerSourceName, group }: Props) =
|
|||||||
<span>No grouping</span>
|
<span>No grouping</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AmNotificationsGroupHeader group={group} />
|
<AlertGroupHeader group={group} />
|
||||||
</div>
|
</div>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && <AlertGroupAlertsTable alertManagerSourceName={alertManagerSourceName} alerts={group.alerts} />}
|
||||||
<AmNotificationsAlertsTable alertManagerSourceName={alertManagerSourceName} alerts={group.alerts} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -7,21 +7,21 @@ import { DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'
|
|||||||
import { AmAlertStateTag } from '../silences/AmAlertStateTag';
|
import { AmAlertStateTag } from '../silences/AmAlertStateTag';
|
||||||
import { AlertLabels } from '../AlertLabels';
|
import { AlertLabels } from '../AlertLabels';
|
||||||
import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines';
|
import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines';
|
||||||
import { AmNotificationsAlertDetails } from './AmNotificationsAlertDetails';
|
import { AlertDetails } from './AlertDetails';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
alerts: AlertmanagerAlert[];
|
alerts: AlertmanagerAlert[];
|
||||||
alertManagerSourceName: string;
|
alertManagerSourceName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AmNotificationsAlertsTableColumnProps = DynamicTableColumnProps<AlertmanagerAlert>;
|
type AlertGroupAlertsTableColumnProps = DynamicTableColumnProps<AlertmanagerAlert>;
|
||||||
type AmNotificationsAlertsTableItemProps = DynamicTableItemProps<AlertmanagerAlert>;
|
type AlertGroupAlertsTableItemProps = DynamicTableItemProps<AlertmanagerAlert>;
|
||||||
|
|
||||||
export const AmNotificationsAlertsTable = ({ alerts, alertManagerSourceName }: Props) => {
|
export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
(): AmNotificationsAlertsTableColumnProps[] => [
|
(): AlertGroupAlertsTableColumnProps[] => [
|
||||||
{
|
{
|
||||||
id: 'state',
|
id: 'state',
|
||||||
label: 'State',
|
label: 'State',
|
||||||
@ -52,7 +52,7 @@ export const AmNotificationsAlertsTable = ({ alerts, alertManagerSourceName }: P
|
|||||||
);
|
);
|
||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
(): AmNotificationsAlertsTableItemProps[] =>
|
(): AlertGroupAlertsTableItemProps[] =>
|
||||||
alerts.map((alert) => ({
|
alerts.map((alert) => ({
|
||||||
id: alert.fingerprint,
|
id: alert.fingerprint,
|
||||||
data: alert,
|
data: alert,
|
||||||
@ -61,13 +61,13 @@ export const AmNotificationsAlertsTable = ({ alerts, alertManagerSourceName }: P
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tableWrapper} data-testid="notifications-table">
|
<div className={styles.tableWrapper} data-testid="alert-group-table">
|
||||||
<DynamicTableWithGuidelines
|
<DynamicTableWithGuidelines
|
||||||
cols={columns}
|
cols={columns}
|
||||||
items={items}
|
items={items}
|
||||||
isExpandable={true}
|
isExpandable={true}
|
||||||
renderExpandedContent={({ data: alert }) => (
|
renderExpandedContent={({ data: alert }) => (
|
||||||
<AmNotificationsAlertDetails alert={alert} alertManagerSourceName={alertManagerSourceName} />
|
<AlertDetails alert={alert} alertManagerSourceName={alertManagerSourceName} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
@ -0,0 +1,88 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { AlertManagerPicker } from '../AlertManagerPicker';
|
||||||
|
import { MatcherFilter } from './MatcherFilter';
|
||||||
|
import { AlertStateFilter } from './AlertStateFilter';
|
||||||
|
import { GroupBy } from './GroupBy';
|
||||||
|
import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Button, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { getFiltersFromUrlParams } from '../../utils/misc';
|
||||||
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
groups: AlertmanagerGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AlertGroupFilter = ({ groups }: Props) => {
|
||||||
|
const [filterKey, setFilterKey] = useState<number>(Math.floor(Math.random() * 100));
|
||||||
|
const [queryParams, setQueryParams] = useQueryParams();
|
||||||
|
const { groupBy = [], queryString, alertState } = getFiltersFromUrlParams(queryParams);
|
||||||
|
const matcherFilterKey = `matcher-${filterKey}`;
|
||||||
|
|
||||||
|
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setQueryParams({
|
||||||
|
groupBy: null,
|
||||||
|
queryString: null,
|
||||||
|
alertState: null,
|
||||||
|
});
|
||||||
|
setTimeout(() => setFilterKey(filterKey + 1), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showClearButton = !!(groupBy.length > 0 || queryString || alertState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||||
|
<div className={styles.filterSection}>
|
||||||
|
<MatcherFilter
|
||||||
|
className={styles.filterInput}
|
||||||
|
key={matcherFilterKey}
|
||||||
|
queryString={queryString}
|
||||||
|
onFilterChange={(value) => setQueryParams({ queryString: value ? value : null })}
|
||||||
|
/>
|
||||||
|
<GroupBy
|
||||||
|
className={styles.filterInput}
|
||||||
|
groups={groups}
|
||||||
|
groupBy={groupBy}
|
||||||
|
onGroupingChange={(keys) => setQueryParams({ groupBy: keys.length ? keys.join(',') : null })}
|
||||||
|
/>
|
||||||
|
<AlertStateFilter
|
||||||
|
stateFilter={alertState as AlertState}
|
||||||
|
onStateFilterChange={(value) => setQueryParams({ alertState: value ? value : null })}
|
||||||
|
/>
|
||||||
|
{showClearButton && (
|
||||||
|
<Button className={styles.clearButton} variant={'secondary'} icon="times" onClick={clearFilters}>
|
||||||
|
Clear filters
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
wrapper: css`
|
||||||
|
border-bottom: 1px solid ${theme.colors.border.medium};
|
||||||
|
margin-bottom: ${theme.spacing(3)};
|
||||||
|
`,
|
||||||
|
filterSection: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: ${theme.spacing(3)};
|
||||||
|
`,
|
||||||
|
filterInput: css`
|
||||||
|
width: 340px;
|
||||||
|
margin-left: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
clearButton: css`
|
||||||
|
margin-left: ${theme.spacing(1)};
|
||||||
|
margin-top: 19px;
|
||||||
|
`,
|
||||||
|
});
|
@ -8,7 +8,7 @@ interface Props {
|
|||||||
group: AlertmanagerGroup;
|
group: AlertmanagerGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AmNotificationsGroupHeader = ({ group }: Props) => {
|
export const AlertGroupHeader = ({ group }: Props) => {
|
||||||
const textStyles = useStyles2(getNotificationsTextColors);
|
const textStyles = useStyles2(getNotificationsTextColors);
|
||||||
const total = group.alerts.length;
|
const total = group.alerts.length;
|
||||||
const countByStatus = group.alerts.reduce((statusObj, alert) => {
|
const countByStatus = group.alerts.reduce((statusObj, alert) => {
|
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { RadioButtonGroup, Label, useStyles2 } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
|
import { AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stateFilter?: AlertState;
|
||||||
|
onStateFilterChange: (value: AlertState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AlertStateFilter = ({ onStateFilterChange, stateFilter }: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const alertStateOptions: SelectableValue[] = Object.entries(AlertState)
|
||||||
|
.sort(([labelA], [labelB]) => (labelA < labelB ? -1 : 1))
|
||||||
|
.map(([label, state]) => ({
|
||||||
|
label,
|
||||||
|
value: state,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<Label>State</Label>
|
||||||
|
<RadioButtonGroup options={alertStateOptions} value={stateFilter} onChange={onStateFilterChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
wrapper: css`
|
||||||
|
margin-left: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
});
|
@ -0,0 +1,37 @@
|
|||||||
|
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import React from 'react';
|
||||||
|
import { uniq } from 'lodash';
|
||||||
|
import { Icon, Label, MultiSelect } from '@grafana/ui';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
groups: AlertmanagerGroup[];
|
||||||
|
groupBy: string[];
|
||||||
|
onGroupingChange: (keys: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GroupBy = ({ className, groups, groupBy, onGroupingChange }: Props) => {
|
||||||
|
const labelKeyOptions = uniq(groups.flatMap((group) => group.alerts).flatMap(({ labels }) => Object.keys(labels)))
|
||||||
|
.filter((label) => !(label.startsWith('__') && label.endsWith('__'))) // Filter out private labels
|
||||||
|
.map<SelectableValue>((key) => ({
|
||||||
|
label: key,
|
||||||
|
value: key,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid={'group-by-container'} className={className}>
|
||||||
|
<Label>Custom group by</Label>
|
||||||
|
<MultiSelect
|
||||||
|
aria-label={'group by label keys'}
|
||||||
|
value={groupBy}
|
||||||
|
placeholder="Group by"
|
||||||
|
prefix={<Icon name={'tag-alt'} />}
|
||||||
|
onChange={(items) => {
|
||||||
|
onGroupingChange(items.map(({ value }) => value as string));
|
||||||
|
}}
|
||||||
|
options={labelKeyOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,47 @@
|
|||||||
|
import React, { FormEvent } from 'react';
|
||||||
|
import { Label, Tooltip, Input, Icon, useStyles2 } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
queryString?: string;
|
||||||
|
onFilterChange: (filterString: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MatcherFilter = ({ className, onFilterChange, queryString }: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const handleSearchChange = (e: FormEvent<HTMLInputElement>) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
onFilterChange(target.value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Label>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
Filter alerts using label querying, ex:
|
||||||
|
<pre>{`{severity="critical", instance=~"cluster-us-.+"}`}</pre>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon className={styles.icon} name="info-circle" size="xs" />
|
||||||
|
</Tooltip>
|
||||||
|
Search by label
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Search"
|
||||||
|
defaultValue={queryString}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
data-testid="search-query-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
icon: css`
|
||||||
|
margin-right: ${theme.spacing(0.5)};
|
||||||
|
`,
|
||||||
|
});
|
@ -61,7 +61,7 @@ const RulesFilter = () => {
|
|||||||
queryString: null,
|
queryString: null,
|
||||||
dataSource: null,
|
dataSource: null,
|
||||||
});
|
});
|
||||||
setFilterKey(filterKey + 1);
|
setTimeout(() => setFilterKey(filterKey + 1), 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchIcon = <Icon name={'search'} />;
|
const searchIcon = <Icon name={'search'} />;
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { labelsMatchMatchers, parseMatchers } from '../utils/alertmanager';
|
||||||
|
import { getFiltersFromUrlParams } from '../utils/misc';
|
||||||
|
|
||||||
|
export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => {
|
||||||
|
const [queryParams] = useQueryParams();
|
||||||
|
const filters = getFiltersFromUrlParams(queryParams);
|
||||||
|
const matchers = parseMatchers(filters.queryString || '');
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return groups.reduce((filteredGroup, group) => {
|
||||||
|
const alerts = group.alerts.filter(({ labels, status }) => {
|
||||||
|
const labelsMatch = labelsMatchMatchers(labels, matchers);
|
||||||
|
const filtersMatch = filters.alertState ? status.state === filters.alertState : true;
|
||||||
|
return labelsMatch && filtersMatch;
|
||||||
|
});
|
||||||
|
if (alerts.length > 0) {
|
||||||
|
// The ungrouped alerts should be first in the results
|
||||||
|
if (Object.keys(group.labels).length === 0) {
|
||||||
|
filteredGroup.unshift({ ...group, alerts });
|
||||||
|
} else {
|
||||||
|
filteredGroup.push({ ...group, alerts });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredGroup;
|
||||||
|
}, [] as AlertmanagerGroup[]);
|
||||||
|
}, [groups, filters, matchers]);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { CombinedRuleGroup, CombinedRuleNamespace, RuleFilterState } from 'app/types/unified-alerting';
|
import { CombinedRuleGroup, CombinedRuleNamespace, FilterState } from 'app/types/unified-alerting';
|
||||||
import { isCloudRulesSource } from '../utils/datasource';
|
import { isCloudRulesSource } from '../utils/datasource';
|
||||||
import { isAlertingRule, isGrafanaRulerRule } from '../utils/rules';
|
import { isAlertingRule, isGrafanaRulerRule } from '../utils/rules';
|
||||||
import { getFiltersFromUrlParams } from '../utils/misc';
|
import { getFiltersFromUrlParams } from '../utils/misc';
|
||||||
@ -29,7 +29,7 @@ export const useFilteredRules = (namespaces: CombinedRuleNamespace[]) => {
|
|||||||
}, [namespaces, filters]);
|
}, [namespaces, filters]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const reduceNamespaces = (filters: RuleFilterState) => {
|
const reduceNamespaces = (filters: FilterState) => {
|
||||||
return (namespaceAcc: CombinedRuleNamespace[], namespace: CombinedRuleNamespace) => {
|
return (namespaceAcc: CombinedRuleNamespace[], namespace: CombinedRuleNamespace) => {
|
||||||
const groups = namespace.groups.reduce(reduceGroups(filters), [] as CombinedRuleGroup[]);
|
const groups = namespace.groups.reduce(reduceGroups(filters), [] as CombinedRuleGroup[]);
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ const reduceNamespaces = (filters: RuleFilterState) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Reduces groups to only groups that have rules matching the filters
|
// Reduces groups to only groups that have rules matching the filters
|
||||||
const reduceGroups = (filters: RuleFilterState) => {
|
const reduceGroups = (filters: FilterState) => {
|
||||||
return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => {
|
return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => {
|
||||||
const rules = group.rules.filter((rule) => {
|
const rules = group.rules.filter((rule) => {
|
||||||
if (filters.dataSource && isGrafanaRulerRule(rule.rulerRule) && !isQueryingDataSource(rule.rulerRule, filters)) {
|
if (filters.dataSource && isGrafanaRulerRule(rule.rulerRule) && !isQueryingDataSource(rule.rulerRule, filters)) {
|
||||||
@ -87,7 +87,7 @@ const reduceGroups = (filters: RuleFilterState) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filter: RuleFilterState): boolean => {
|
const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filter: FilterState): boolean => {
|
||||||
if (!filter.dataSource) {
|
if (!filter.dataSource) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import { Labels } from '@grafana/data';
|
||||||
|
|
||||||
|
export const useGroupedAlerts = (groups: AlertmanagerGroup[], groupBy: string[]) => {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (groupBy.length === 0) {
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
const alerts = groups.flatMap(({ alerts }) => alerts);
|
||||||
|
return alerts.reduce((groupings, alert) => {
|
||||||
|
const alertContainsGroupings = groupBy.every((groupByLabel) => Object.keys(alert.labels).includes(groupByLabel));
|
||||||
|
|
||||||
|
if (alertContainsGroupings) {
|
||||||
|
const existingGrouping = groupings.find((group) => {
|
||||||
|
return groupBy.every((groupKey) => {
|
||||||
|
return group.labels[groupKey] === alert.labels[groupKey];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!existingGrouping) {
|
||||||
|
const labels = groupBy.reduce((acc, key) => {
|
||||||
|
acc = { ...acc, [key]: alert.labels[key] };
|
||||||
|
return acc;
|
||||||
|
}, {} as Labels);
|
||||||
|
groupings.push({
|
||||||
|
alerts: [alert],
|
||||||
|
labels,
|
||||||
|
receiver: {
|
||||||
|
name: 'NONE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
existingGrouping.alerts.push(alert);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const noGroupingGroup = groupings.find((group) => Object.keys(group.labels).length === 0);
|
||||||
|
if (!noGroupingGroup) {
|
||||||
|
groupings.push({ alerts: [alert], labels: {}, receiver: { name: 'NONE' } });
|
||||||
|
} else {
|
||||||
|
noGroupingGroup.alerts.push(alert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupings;
|
||||||
|
}, [] as AlertmanagerGroup[]);
|
||||||
|
}, [groups, groupBy]);
|
||||||
|
};
|
@ -177,7 +177,7 @@ export const mockAlertGroup = (partial: Partial<AlertmanagerGroup> = {}): Alertm
|
|||||||
mockAlertmanagerAlert(),
|
mockAlertmanagerAlert(),
|
||||||
mockAlertmanagerAlert({
|
mockAlertmanagerAlert({
|
||||||
status: { state: AlertState.Suppressed, silencedBy: ['123456abcdef'], inhibitedBy: [] },
|
status: { state: AlertState.Suppressed, silencedBy: ['123456abcdef'], inhibitedBy: [] },
|
||||||
labels: { severity: 'warning', region: 'US-Central', foo: 'bar' },
|
labels: { severity: 'warning', region: 'US-Central', foo: 'bar', ...partial.labels },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
...partial,
|
...partial,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { urlUtil, UrlQueryMap } from '@grafana/data';
|
import { urlUtil, UrlQueryMap } from '@grafana/data';
|
||||||
import { CombinedRule, RuleFilterState, RulesSource } from 'app/types/unified-alerting';
|
import { CombinedRule, FilterState, RulesSource } from 'app/types/unified-alerting';
|
||||||
import { ALERTMANAGER_NAME_QUERY_KEY } from './constants';
|
import { ALERTMANAGER_NAME_QUERY_KEY } from './constants';
|
||||||
import { getRulesSourceName } from './datasource';
|
import { getRulesSourceName } from './datasource';
|
||||||
import * as ruleId from './rule-id';
|
import * as ruleId from './rule-id';
|
||||||
@ -32,12 +32,12 @@ export function arrayToRecord(items: Array<{ key: string; value: string }>): Rec
|
|||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFiltersFromUrlParams = (queryParams: UrlQueryMap): RuleFilterState => {
|
export const getFiltersFromUrlParams = (queryParams: UrlQueryMap): FilterState => {
|
||||||
const queryString = queryParams['queryString'] === undefined ? undefined : String(queryParams['queryString']);
|
const queryString = queryParams['queryString'] === undefined ? undefined : String(queryParams['queryString']);
|
||||||
const alertState = queryParams['alertState'] === undefined ? undefined : String(queryParams['alertState']);
|
const alertState = queryParams['alertState'] === undefined ? undefined : String(queryParams['alertState']);
|
||||||
const dataSource = queryParams['dataSource'] === undefined ? undefined : String(queryParams['dataSource']);
|
const dataSource = queryParams['dataSource'] === undefined ? undefined : String(queryParams['dataSource']);
|
||||||
|
const groupBy = queryParams['groupBy'] === undefined ? undefined : String(queryParams['groupBy']).split(',');
|
||||||
return { queryString, alertState, dataSource };
|
return { queryString, alertState, dataSource, groupBy };
|
||||||
};
|
};
|
||||||
|
|
||||||
export function recordToArray(record: Record<string, string>): Array<{ key: string; value: string }> {
|
export function recordToArray(record: Record<string, string>): Array<{ key: string; value: string }> {
|
||||||
|
@ -5,7 +5,7 @@ import { useStyles2, LinkButton } from '@grafana/ui';
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { AlertLabels } from 'app/features/alerting/unified/components/AlertLabels';
|
import { AlertLabels } from 'app/features/alerting/unified/components/AlertLabels';
|
||||||
import { AmNotificationsGroupHeader } from 'app/features/alerting/unified/components/amnotifications/AmNotificationsGroupHeader';
|
import { AlertGroupHeader } from 'app/features/alerting/unified/components/alert-groups/AlertGroupHeader';
|
||||||
import { CollapseToggle } from 'app/features/alerting/unified/components/CollapseToggle';
|
import { CollapseToggle } from 'app/features/alerting/unified/components/CollapseToggle';
|
||||||
import { getNotificationsTextColors } from 'app/features/alerting/unified/styles/notifications';
|
import { getNotificationsTextColors } from 'app/features/alerting/unified/styles/notifications';
|
||||||
import { makeAMLink } from 'app/features/alerting/unified/utils/misc';
|
import { makeAMLink } from 'app/features/alerting/unified/utils/misc';
|
||||||
@ -33,7 +33,7 @@ export const AlertGroup = ({ alertManagerSourceName, group, expandAll }: Props)
|
|||||||
)}
|
)}
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
<CollapseToggle isCollapsed={!showAlerts} onToggle={() => setShowAlerts(!showAlerts)} />{' '}
|
<CollapseToggle isCollapsed={!showAlerts} onToggle={() => setShowAlerts(!showAlerts)} />{' '}
|
||||||
<AmNotificationsGroupHeader group={group} />
|
<AlertGroupHeader group={group} />
|
||||||
</div>
|
</div>
|
||||||
{showAlerts && (
|
{showAlerts && (
|
||||||
<div className={styles.alerts}>
|
<div className={styles.alerts}>
|
||||||
|
@ -431,10 +431,9 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/alertmanager/',
|
path: '/alerting/groups/',
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() =>
|
() => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups')
|
||||||
import(/* webpackChunkName: "AlertManagerNotifications" */ 'app/features/alerting/unified/AmNotifications')
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -127,8 +127,9 @@ export interface PrometheusRuleIdentifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RuleIdentifier = CloudRuleIdentifier | GrafanaRuleIdentifier | PrometheusRuleIdentifier;
|
export type RuleIdentifier = CloudRuleIdentifier | GrafanaRuleIdentifier | PrometheusRuleIdentifier;
|
||||||
export interface RuleFilterState {
|
export interface FilterState {
|
||||||
queryString?: string;
|
queryString?: string;
|
||||||
dataSource?: string;
|
dataSource?: string;
|
||||||
alertState?: string;
|
alertState?: string;
|
||||||
|
groupBy?: string[];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user