mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Configure alert manager data source as an external AM (#52081)
Co-authored-by: Jean-Philippe Quéméner <JohnnyQQQQ@users.noreply.github.com> Co-authored-by: gotjosh <josue.abreu@gmail.com> Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com>
This commit is contained in:
@@ -3,12 +3,20 @@ import React from 'react';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import AlertmanagerConfig from './components/admin/AlertmanagerConfig';
|
||||
import { ExternalAlertmanagers } from './components/admin/ExternalAlertmanagers';
|
||||
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
|
||||
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
|
||||
export default function Admin(): JSX.Element {
|
||||
const alertManagers = useAlertManagersByPermission('notification');
|
||||
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
|
||||
|
||||
const isGrafanaAmSelected = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME;
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper pageId="alerting-admin">
|
||||
<AlertmanagerConfig test-id="admin-alertmanagerconfig" />
|
||||
<ExternalAlertmanagers test-id="admin-externalalertmanagers" />
|
||||
{isGrafanaAmSelected && <ExternalAlertmanagers test-id="admin-externalalertmanagers" />}
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { capitalize } from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Badge, CallToActionCard, Card, Icon, LinkButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { ExternalDataSourceAM } from '../../hooks/useExternalAmSelector';
|
||||
import { makeDataSourceLink } from '../../utils/misc';
|
||||
|
||||
export interface ExternalAlertManagerDataSourcesProps {
|
||||
alertmanagers: ExternalDataSourceAM[];
|
||||
inactive: boolean;
|
||||
}
|
||||
|
||||
export function ExternalAlertmanagerDataSources({ alertmanagers, inactive }: ExternalAlertManagerDataSourcesProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5>Alertmanagers data sources</h5>
|
||||
<div className={styles.muted}>
|
||||
Alertmanager data sources support a configuration setting that allows you to choose to send Grafana-managed
|
||||
alerts to that Alertmanager. <br />
|
||||
Below, you can see the list of all Alertmanager data sources that have this setting enabled.
|
||||
</div>
|
||||
{alertmanagers.length === 0 && (
|
||||
<CallToActionCard
|
||||
message={
|
||||
<div>
|
||||
There are no Alertmanager data sources configured to receive Grafana-managed alerts. <br />
|
||||
You can change this by selecting Receive Grafana Alerts in a data source configuration.
|
||||
</div>
|
||||
}
|
||||
callToActionElement={<LinkButton href="/datasources">Go to data sources</LinkButton>}
|
||||
className={styles.externalDsCTA}
|
||||
/>
|
||||
)}
|
||||
{alertmanagers.length > 0 && (
|
||||
<div className={styles.externalDs}>
|
||||
{alertmanagers.map((am) => (
|
||||
<ExternalAMdataSourceCard key={am.dataSource.uid} alertmanager={am} inactive={inactive} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExternalAMdataSourceCardProps {
|
||||
alertmanager: ExternalDataSourceAM;
|
||||
inactive: boolean;
|
||||
}
|
||||
|
||||
export function ExternalAMdataSourceCard({ alertmanager, inactive }: ExternalAMdataSourceCardProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { dataSource, status, statusInconclusive, url } = alertmanager;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Heading className={styles.externalHeading}>
|
||||
{dataSource.name}{' '}
|
||||
{statusInconclusive && (
|
||||
<Tooltip content="Multiple Alertmangers have the same URL configured. The state might be inconclusive.">
|
||||
<Icon name="exclamation-triangle" size="md" className={styles.externalWarningIcon} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Card.Heading>
|
||||
<Card.Figure>
|
||||
<img
|
||||
src="public/app/plugins/datasource/alertmanager/img/logo.svg"
|
||||
alt=""
|
||||
height="40px"
|
||||
width="40px"
|
||||
style={{ objectFit: 'contain' }}
|
||||
/>
|
||||
</Card.Figure>
|
||||
<Card.Tags>
|
||||
{inactive ? (
|
||||
<Badge
|
||||
text="Inactive"
|
||||
color="red"
|
||||
tooltip="Grafana is configured to send alerts to the built-in internal Alertmanager only. External Alertmanagers do not receive any alerts."
|
||||
/>
|
||||
) : (
|
||||
<Badge
|
||||
text={capitalize(status)}
|
||||
color={status === 'dropped' ? 'red' : status === 'active' ? 'green' : 'orange'}
|
||||
/>
|
||||
)}
|
||||
</Card.Tags>
|
||||
<Card.Meta>{url}</Card.Meta>
|
||||
<Card.Actions>
|
||||
<LinkButton href={makeDataSourceLink(dataSource)} size="sm" variant="secondary">
|
||||
Go to datasouce
|
||||
</LinkButton>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
muted: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
externalHeading: css`
|
||||
justify-content: flex-start;
|
||||
`,
|
||||
externalWarningIcon: css`
|
||||
margin: ${theme.spacing(0, 1)};
|
||||
fill: ${theme.colors.warning.main};
|
||||
`,
|
||||
externalDs: css`
|
||||
display: grid;
|
||||
gap: ${theme.spacing(1)};
|
||||
padding: ${theme.spacing(2, 0)};
|
||||
`,
|
||||
externalDsCTA: css`
|
||||
margin: ${theme.spacing(2, 0)};
|
||||
`,
|
||||
});
|
||||
@@ -2,8 +2,9 @@ import { css, cx } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
ConfirmModal,
|
||||
Field,
|
||||
@@ -15,9 +16,11 @@ import {
|
||||
useTheme2,
|
||||
} from '@grafana/ui';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { loadDataSources } from 'app/features/datasources/state/actions';
|
||||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { StoreState } from 'app/types/store';
|
||||
|
||||
import { useExternalAmSelector } from '../../hooks/useExternalAmSelector';
|
||||
import { useExternalAmSelector, useExternalDataSourceAlertmanagers } from '../../hooks/useExternalAmSelector';
|
||||
import {
|
||||
addExternalAlertmanagersAction,
|
||||
fetchExternalAlertmanagersAction,
|
||||
@@ -25,11 +28,12 @@ import {
|
||||
} from '../../state/actions';
|
||||
|
||||
import { AddAlertManagerModal } from './AddAlertManagerModal';
|
||||
import { ExternalAlertmanagerDataSources } from './ExternalAlertmanagerDataSources';
|
||||
|
||||
const alertmanagerChoices = [
|
||||
{ value: 'internal', label: 'Only Internal' },
|
||||
{ value: 'external', label: 'Only External' },
|
||||
{ value: 'all', label: 'Both internal and external' },
|
||||
const alertmanagerChoices: Array<SelectableValue<AlertmanagerChoice>> = [
|
||||
{ value: AlertmanagerChoice.Internal, label: 'Only Internal' },
|
||||
{ value: AlertmanagerChoice.External, label: 'Only External' },
|
||||
{ value: AlertmanagerChoice.All, label: 'Both internal and external' },
|
||||
];
|
||||
|
||||
export const ExternalAlertmanagers = () => {
|
||||
@@ -39,6 +43,8 @@ export const ExternalAlertmanagers = () => {
|
||||
const [deleteModalState, setDeleteModalState] = useState({ open: false, index: 0 });
|
||||
|
||||
const externalAlertManagers = useExternalAmSelector();
|
||||
const externalDsAlertManagers = useExternalDataSourceAlertmanagers();
|
||||
|
||||
const alertmanagersChoice = useSelector(
|
||||
(state: StoreState) => state.unifiedAlerting.externalAlertmanagers.alertmanagerConfig.result?.alertmanagersChoice
|
||||
);
|
||||
@@ -47,6 +53,7 @@ export const ExternalAlertmanagers = () => {
|
||||
useEffect(() => {
|
||||
dispatch(fetchExternalAlertmanagersAction());
|
||||
dispatch(fetchExternalAlertmanagersConfigAction());
|
||||
dispatch(loadDataSources());
|
||||
const interval = setInterval(() => dispatch(fetchExternalAlertmanagersAction()), 5000);
|
||||
|
||||
return () => {
|
||||
@@ -63,7 +70,10 @@ export const ExternalAlertmanagers = () => {
|
||||
return am.url;
|
||||
});
|
||||
dispatch(
|
||||
addExternalAlertmanagersAction({ alertmanagers: newList, alertmanagersChoice: alertmanagersChoice ?? 'all' })
|
||||
addExternalAlertmanagersAction({
|
||||
alertmanagers: newList,
|
||||
alertmanagersChoice: alertmanagersChoice ?? AlertmanagerChoice.All,
|
||||
})
|
||||
);
|
||||
setDeleteModalState({ open: false, index: 0 });
|
||||
},
|
||||
@@ -97,14 +107,19 @@ export const ExternalAlertmanagers = () => {
|
||||
}));
|
||||
}, [setModalState]);
|
||||
|
||||
const onChangeAlertmanagerChoice = (alertmanagersChoice: string) => {
|
||||
const onChangeAlertmanagerChoice = (alertmanagersChoice: AlertmanagerChoice) => {
|
||||
dispatch(
|
||||
addExternalAlertmanagersAction({ alertmanagers: externalAlertManagers.map((am) => am.url), alertmanagersChoice })
|
||||
);
|
||||
};
|
||||
|
||||
const onChangeAlertmanagers = (alertmanagers: string[]) => {
|
||||
dispatch(addExternalAlertmanagersAction({ alertmanagers, alertmanagersChoice: alertmanagersChoice ?? 'all' }));
|
||||
dispatch(
|
||||
addExternalAlertmanagersAction({
|
||||
alertmanagers,
|
||||
alertmanagersChoice: alertmanagersChoice ?? AlertmanagerChoice.All,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
@@ -121,10 +136,47 @@ export const ExternalAlertmanagers = () => {
|
||||
};
|
||||
|
||||
const noAlertmanagers = externalAlertManagers?.length === 0;
|
||||
const noDsAlertmanagers = externalDsAlertManagers?.length === 0;
|
||||
const hasExternalAlertmanagers = !(noAlertmanagers && noDsAlertmanagers);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>External Alertmanagers</h4>
|
||||
<Alert title="External Alertmanager changes" severity="info">
|
||||
The way you configure external Alertmanagers has changed.
|
||||
<br />
|
||||
You can now use configured Alertmanager data sources as receivers of your Grafana-managed alerts.
|
||||
<br />
|
||||
For more information, refer to our documentation.
|
||||
</Alert>
|
||||
|
||||
<ExternalAlertmanagerDataSources
|
||||
alertmanagers={externalDsAlertManagers}
|
||||
inactive={alertmanagersChoice === AlertmanagerChoice.Internal}
|
||||
/>
|
||||
|
||||
{hasExternalAlertmanagers && (
|
||||
<div className={styles.amChoice}>
|
||||
<Field
|
||||
label="Send alerts to"
|
||||
description="Configures how the Grafana alert rule evaluation engine Alertmanager handles your alerts. Internal (Grafana built-in Alertmanager), External (All Alertmanagers configured above), or both."
|
||||
>
|
||||
<RadioButtonGroup
|
||||
options={alertmanagerChoices}
|
||||
value={alertmanagersChoice}
|
||||
onChange={(value) => onChangeAlertmanagerChoice(value!)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h5>Alertmanagers by URL</h5>
|
||||
<Alert severity="warning" title="Deprecation Notice">
|
||||
The URL-based configuration of Alertmanagers is deprecated and will be removed in Grafana 9.2.0.
|
||||
<br />
|
||||
Use Alertmanager data sources to configure your external Alertmanagers.
|
||||
</Alert>
|
||||
|
||||
<div className={styles.muted}>
|
||||
You can have your Grafana managed alerts be delivered to one or many external Alertmanager(s) in addition to the
|
||||
internal Alertmanager by specifying their URLs below.
|
||||
@@ -136,6 +188,7 @@ export const ExternalAlertmanagers = () => {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{noAlertmanagers ? (
|
||||
<EmptyListCTA
|
||||
title="You have not added any external alertmanagers"
|
||||
@@ -188,20 +241,9 @@ export const ExternalAlertmanagers = () => {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<Field
|
||||
label="Send alerts to"
|
||||
description="Sets which Alertmanager will handle your alerts. Internal (Grafana built in Alertmanager), External (All Alertmanagers configured above), or both."
|
||||
>
|
||||
<RadioButtonGroup
|
||||
options={alertmanagerChoices}
|
||||
value={alertmanagersChoice}
|
||||
onChange={(value) => onChangeAlertmanagerChoice(value!)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={deleteModalState.open}
|
||||
title="Remove Alertmanager"
|
||||
@@ -221,7 +263,7 @@ export const ExternalAlertmanagers = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
url: css`
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
@@ -236,4 +278,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
table: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
amChoice: css`
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Alert, Button, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { getRulesDataSources, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { makeDataSourceLink } from '../../utils/misc';
|
||||
import { isRulerNotSupportedResponse } from '../../utils/rules';
|
||||
|
||||
export function RuleListErrors(): ReactElement {
|
||||
@@ -52,7 +53,7 @@ export function RuleListErrors(): ReactElement {
|
||||
result.push(
|
||||
<>
|
||||
Failed to load the data source configuration for{' '}
|
||||
<a href={`datasources/edit/${dataSource.uid}`}>{dataSource.name}</a>: {error.message || 'Unknown error.'}
|
||||
<a href={makeDataSourceLink(dataSource)}>{dataSource.name}</a>: {error.message || 'Unknown error.'}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -60,7 +61,7 @@ export function RuleListErrors(): ReactElement {
|
||||
promRequestErrors.forEach(({ dataSource, error }) =>
|
||||
result.push(
|
||||
<>
|
||||
Failed to load rules state from <a href={`datasources/edit/${dataSource.uid}`}>{dataSource.name}</a>:{' '}
|
||||
Failed to load rules state from <a href={makeDataSourceLink(dataSource)}>{dataSource.name}</a>:{' '}
|
||||
{error.message || 'Unknown error.'}
|
||||
</>
|
||||
)
|
||||
@@ -69,7 +70,7 @@ export function RuleListErrors(): ReactElement {
|
||||
rulerRequestErrors.forEach(({ dataSource, error }) =>
|
||||
result.push(
|
||||
<>
|
||||
Failed to load rules config from <a href={`datasources/edit/${dataSource.uid}`}>{dataSource.name}</a>:{' '}
|
||||
Failed to load rules config from <a href={makeDataSourceLink(dataSource)}>{dataSource.name}</a>:{' '}
|
||||
{error.message || 'Unknown error.'}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import * as reactRedux from 'react-redux';
|
||||
|
||||
import { useExternalAmSelector } from './useExternalAmSelector';
|
||||
|
||||
const createMockStoreState = (
|
||||
activeAlertmanagers: Array<{ url: string }>,
|
||||
droppedAlertmanagers: Array<{ url: string }>,
|
||||
alertmanagerConfig: string[]
|
||||
) => ({
|
||||
unifiedAlerting: {
|
||||
externalAlertmanagers: {
|
||||
discoveredAlertmanagers: {
|
||||
result: {
|
||||
data: {
|
||||
activeAlertManagers: activeAlertmanagers,
|
||||
droppedAlertManagers: droppedAlertmanagers,
|
||||
},
|
||||
},
|
||||
},
|
||||
alertmanagerConfig: {
|
||||
result: {
|
||||
alertmanagers: alertmanagerConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('useExternalAmSelector', () => {
|
||||
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
});
|
||||
it('should have one in pending', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(createMockStoreState([], [], ['some/url/to/am']));
|
||||
});
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
status: 'pending',
|
||||
actualUrl: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have one active, one pending', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState([{ url: 'some/url/to/am/api/v2/alerts' }], [], ['some/url/to/am', 'some/url/to/am1'])
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
actualUrl: 'some/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'some/url/to/am1',
|
||||
actualUrl: '',
|
||||
status: 'pending',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have two active', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState(
|
||||
[{ url: 'some/url/to/am/api/v2/alerts' }, { url: 'some/url/to/am1/api/v2/alerts' }],
|
||||
[],
|
||||
['some/url/to/am', 'some/url/to/am1']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
actualUrl: 'some/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'some/url/to/am1',
|
||||
actualUrl: 'some/url/to/am1/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have one active, one dropped, one pending', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState(
|
||||
[{ url: 'some/url/to/am/api/v2/alerts' }],
|
||||
[{ url: 'some/dropped/url/api/v2/alerts' }],
|
||||
['some/url/to/am', 'some/url/to/am1']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
actualUrl: 'some/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'some/url/to/am1',
|
||||
actualUrl: '',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
url: 'some/dropped/url',
|
||||
actualUrl: 'some/dropped/url/api/v2/alerts',
|
||||
status: 'dropped',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('The number of alert managers should match config entries when there are multiple entries of the same url', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState(
|
||||
[
|
||||
{ url: 'same/url/to/am/api/v2/alerts' },
|
||||
{ url: 'same/url/to/am/api/v2/alerts' },
|
||||
{ url: 'same/url/to/am/api/v2/alerts' },
|
||||
],
|
||||
[],
|
||||
['same/url/to/am', 'same/url/to/am', 'same/url/to/am']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers.length).toBe(3);
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'same/url/to/am',
|
||||
actualUrl: 'same/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'same/url/to/am',
|
||||
actualUrl: 'same/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'same/url/to/am',
|
||||
actualUrl: 'same/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,416 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
import * as reactRedux from 'react-redux';
|
||||
|
||||
import { DataSourceJsonData, DataSourceSettings } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AlertmanagerChoice, AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { mockDataSource, mockDataSourcesStore, mockStore } from '../mocks';
|
||||
|
||||
import { useExternalAmSelector, useExternalDataSourceAlertmanagers } from './useExternalAmSelector';
|
||||
|
||||
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
|
||||
|
||||
describe('useExternalAmSelector', () => {
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
});
|
||||
it('should have one in pending', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(createMockStoreState([], [], ['some/url/to/am']));
|
||||
});
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
status: 'pending',
|
||||
actualUrl: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have one active, one pending', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState([{ url: 'some/url/to/am/api/v2/alerts' }], [], ['some/url/to/am', 'some/url/to/am1'])
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
actualUrl: 'some/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'some/url/to/am1',
|
||||
actualUrl: '',
|
||||
status: 'pending',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have two active', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState(
|
||||
[{ url: 'some/url/to/am/api/v2/alerts' }, { url: 'some/url/to/am1/api/v2/alerts' }],
|
||||
[],
|
||||
['some/url/to/am', 'some/url/to/am1']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
actualUrl: 'some/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'some/url/to/am1',
|
||||
actualUrl: 'some/url/to/am1/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have one active, one dropped, one pending', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState(
|
||||
[{ url: 'some/url/to/am/api/v2/alerts' }],
|
||||
[{ url: 'some/dropped/url/api/v2/alerts' }],
|
||||
['some/url/to/am', 'some/url/to/am1']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'some/url/to/am',
|
||||
actualUrl: 'some/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'some/url/to/am1',
|
||||
actualUrl: '',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
url: 'some/dropped/url',
|
||||
actualUrl: 'some/dropped/url/api/v2/alerts',
|
||||
status: 'dropped',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('The number of alert managers should match config entries when there are multiple entries of the same url', () => {
|
||||
useSelectorMock.mockImplementation((callback) => {
|
||||
return callback(
|
||||
createMockStoreState(
|
||||
[
|
||||
{ url: 'same/url/to/am/api/v2/alerts' },
|
||||
{ url: 'same/url/to/am/api/v2/alerts' },
|
||||
{ url: 'same/url/to/am/api/v2/alerts' },
|
||||
],
|
||||
[],
|
||||
['same/url/to/am', 'same/url/to/am', 'same/url/to/am']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const alertmanagers = useExternalAmSelector();
|
||||
|
||||
expect(alertmanagers.length).toBe(3);
|
||||
expect(alertmanagers).toEqual([
|
||||
{
|
||||
url: 'same/url/to/am',
|
||||
actualUrl: 'same/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'same/url/to/am',
|
||||
actualUrl: 'same/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
url: 'same/url/to/am',
|
||||
actualUrl: 'same/url/to/am/api/v2/alerts',
|
||||
status: 'active',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useExternalDataSourceAlertmanagers', () => {
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockRestore();
|
||||
});
|
||||
|
||||
it('Should merge data sources information from config and api responses', () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockDataSourcesStore({
|
||||
dataSources: [dsSettings],
|
||||
});
|
||||
|
||||
const wrapper: React.FC = ({ children }) => <reactRedux.Provider store={store}>{children}</reactRedux.Provider>;
|
||||
|
||||
// Act
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
|
||||
// Assert
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].dataSource.uid).toBe('1');
|
||||
expect(current[0].url).toBe('http://grafana.com');
|
||||
});
|
||||
|
||||
it('Should have active state if available in the activeAlertManagers', () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
|
||||
data: {
|
||||
activeAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
|
||||
droppedAlertManagers: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper: React.FC = ({ children }) => <reactRedux.Provider store={store}>{children}</reactRedux.Provider>;
|
||||
|
||||
// Act
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
|
||||
// Assert
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('active');
|
||||
expect(current[0].statusInconclusive).toBe(false);
|
||||
});
|
||||
|
||||
it('Should have dropped state if available in the droppedAlertManagers', () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
|
||||
data: {
|
||||
activeAlertManagers: [],
|
||||
droppedAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper: React.FC = ({ children }) => <reactRedux.Provider store={store}>{children}</reactRedux.Provider>;
|
||||
|
||||
// Act
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
|
||||
// Assert
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('dropped');
|
||||
expect(current[0].statusInconclusive).toBe(false);
|
||||
});
|
||||
|
||||
it('Should have pending state if not available neither in dropped nor in active alertManagers', () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource();
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
|
||||
data: {
|
||||
activeAlertManagers: [],
|
||||
droppedAlertManagers: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper: React.FC = ({ children }) => <reactRedux.Provider store={store}>{children}</reactRedux.Provider>;
|
||||
|
||||
// Act
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
|
||||
// Assert
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('pending');
|
||||
expect(current[0].statusInconclusive).toBe(false);
|
||||
});
|
||||
|
||||
it('Should match Alertmanager url when datasource url does not have protocol specified', () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'localhost:9093' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
|
||||
data: {
|
||||
activeAlertManagers: [{ url: 'http://localhost:9093/api/v2/alerts' }],
|
||||
droppedAlertManagers: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper: React.FC = ({ children }) => <reactRedux.Provider store={store}>{children}</reactRedux.Provider>;
|
||||
|
||||
// Act
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
|
||||
// Assert
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('active');
|
||||
expect(current[0].url).toBe('localhost:9093');
|
||||
});
|
||||
|
||||
it('Should have inconclusive state when there are many Alertmanagers of the same URL', () => {
|
||||
// Arrange
|
||||
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
|
||||
|
||||
config.datasources = {
|
||||
'External Alertmanager': dsInstanceSettings,
|
||||
};
|
||||
|
||||
const store = mockStore((state) => {
|
||||
state.dataSources.dataSources = [dsSettings];
|
||||
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
|
||||
data: {
|
||||
activeAlertManagers: [
|
||||
{ url: 'http://grafana.com/api/v2/alerts' },
|
||||
{ url: 'http://grafana.com/api/v2/alerts' },
|
||||
],
|
||||
droppedAlertManagers: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper: React.FC = ({ children }) => <reactRedux.Provider store={store}>{children}</reactRedux.Provider>;
|
||||
|
||||
// Act
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
|
||||
|
||||
// Assert
|
||||
expect(current).toHaveLength(1);
|
||||
expect(current[0].status).toBe('active');
|
||||
expect(current[0].statusInconclusive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
function setupAlertmanagerDataSource(partialDsSettings?: Partial<DataSourceSettings<AlertManagerDataSourceJsonData>>) {
|
||||
const dsCommonConfig = {
|
||||
uid: '1',
|
||||
name: 'External Alertmanager',
|
||||
type: 'alertmanager',
|
||||
jsonData: { handleGrafanaManagedAlerts: true } as AlertManagerDataSourceJsonData,
|
||||
};
|
||||
|
||||
const dsInstanceSettings = mockDataSource(dsCommonConfig);
|
||||
|
||||
const dsSettings = mockApiDataSource({
|
||||
...dsCommonConfig,
|
||||
...partialDsSettings,
|
||||
});
|
||||
|
||||
return { dsSettings, dsInstanceSettings };
|
||||
}
|
||||
|
||||
function mockApiDataSource(partial: Partial<DataSourceSettings<DataSourceJsonData, {}>> = {}) {
|
||||
const dsSettings: DataSourceSettings<DataSourceJsonData, {}> = {
|
||||
uid: '1',
|
||||
id: 1,
|
||||
name: '',
|
||||
url: '',
|
||||
type: '',
|
||||
access: '',
|
||||
orgId: 1,
|
||||
typeLogoUrl: '',
|
||||
typeName: '',
|
||||
user: '',
|
||||
database: '',
|
||||
basicAuth: false,
|
||||
isDefault: false,
|
||||
basicAuthUser: '',
|
||||
jsonData: { handleGrafanaManagedAlerts: true } as AlertManagerDataSourceJsonData,
|
||||
secureJsonFields: {},
|
||||
readOnly: false,
|
||||
withCredentials: false,
|
||||
...partial,
|
||||
};
|
||||
|
||||
return dsSettings;
|
||||
}
|
||||
|
||||
const createMockStoreState = (
|
||||
activeAlertmanagers: Array<{ url: string }>,
|
||||
droppedAlertmanagers: Array<{ url: string }>,
|
||||
alertmanagerConfig: string[]
|
||||
) => {
|
||||
return {
|
||||
unifiedAlerting: {
|
||||
externalAlertmanagers: {
|
||||
discoveredAlertmanagers: {
|
||||
result: {
|
||||
data: {
|
||||
activeAlertManagers: activeAlertmanagers,
|
||||
droppedAlertManagers: droppedAlertmanagers,
|
||||
},
|
||||
},
|
||||
dispatched: false,
|
||||
loading: false,
|
||||
},
|
||||
alertmanagerConfig: {
|
||||
result: {
|
||||
alertmanagers: alertmanagerConfig,
|
||||
alertmanagersChoice: AlertmanagerChoice.All,
|
||||
},
|
||||
dispatched: false,
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,13 @@
|
||||
import { countBy, keyBy } from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceSettings } from '@grafana/data';
|
||||
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { StoreState } from '../../../../types';
|
||||
import { getAlertManagerDataSources } from '../utils/datasource';
|
||||
|
||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||
|
||||
const SUFFIX_REGEX = /\/api\/v[1|2]\/alerts/i;
|
||||
type AlertmanagerConfig = { url: string; status: string; actualUrl: string };
|
||||
@@ -51,3 +58,71 @@ export function useExternalAmSelector(): AlertmanagerConfig[] | [] {
|
||||
|
||||
return [...enabledAlertmanagers, ...droppedAlertmanagers];
|
||||
}
|
||||
|
||||
export interface ExternalDataSourceAM {
|
||||
dataSource: DataSourceInstanceSettings<AlertManagerDataSourceJsonData>;
|
||||
url?: string;
|
||||
status: 'active' | 'pending' | 'dropped';
|
||||
statusInconclusive?: boolean;
|
||||
}
|
||||
|
||||
export function useExternalDataSourceAlertmanagers(): ExternalDataSourceAM[] {
|
||||
const externalDsAlertManagers = getAlertManagerDataSources().filter((ds) => ds.jsonData.handleGrafanaManagedAlerts);
|
||||
|
||||
const alertmanagerDatasources = useSelector((state: StoreState) =>
|
||||
keyBy(
|
||||
state.dataSources.dataSources.filter((ds) => ds.type === 'alertmanager'),
|
||||
(ds) => ds.uid
|
||||
)
|
||||
);
|
||||
|
||||
const discoveredAlertmanagers = useUnifiedAlertingSelector(
|
||||
(state) => state.externalAlertmanagers.discoveredAlertmanagers.result?.data
|
||||
);
|
||||
|
||||
const droppedAMUrls = countBy(discoveredAlertmanagers?.droppedAlertManagers, (x) => x.url);
|
||||
const activeAMUrls = countBy(discoveredAlertmanagers?.activeAlertManagers, (x) => x.url);
|
||||
|
||||
return externalDsAlertManagers.map<ExternalDataSourceAM>((dsAm) => {
|
||||
const dsSettings = alertmanagerDatasources[dsAm.uid];
|
||||
|
||||
if (!dsSettings) {
|
||||
return {
|
||||
dataSource: dsAm,
|
||||
status: 'pending',
|
||||
};
|
||||
}
|
||||
|
||||
const amUrl = getDataSourceUrlWithProtocol(dsSettings);
|
||||
const amStatusUrl = `${amUrl}/api/v2/alerts`;
|
||||
|
||||
const matchingDroppedUrls = droppedAMUrls[amStatusUrl] ?? 0;
|
||||
const matchingActiveUrls = activeAMUrls[amStatusUrl] ?? 0;
|
||||
|
||||
const isDropped = matchingDroppedUrls > 0;
|
||||
const isActive = matchingActiveUrls > 0;
|
||||
|
||||
// Multiple Alertmanagers of the same URL may exist (e.g. with different credentials)
|
||||
// Alertmanager response only contains URLs, so in case of duplication, we are not able
|
||||
// to distinguish which is which, resulting in an inconclusive status.
|
||||
const isStatusInconclusive = matchingDroppedUrls + matchingActiveUrls > 1;
|
||||
|
||||
const status = isDropped ? 'dropped' : isActive ? 'active' : 'pending';
|
||||
|
||||
return {
|
||||
dataSource: dsAm,
|
||||
url: dsSettings.url,
|
||||
status,
|
||||
statusInconclusive: isStatusInconclusive,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getDataSourceUrlWithProtocol<T>(dsSettings: DataSourceSettings<T>) {
|
||||
const hasProtocol = new RegExp('^[^:]*://').test(dsSettings.url);
|
||||
if (!hasProtocol) {
|
||||
return `http://${dsSettings.url}`; // Grafana append http protocol if there is no any
|
||||
}
|
||||
|
||||
return dsSettings.url;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,16 @@ import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction, FolderDTO, StoreState } from 'app/types';
|
||||
|
||||
import { disableRBAC, enableRBAC, mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule } from '../mocks';
|
||||
import {
|
||||
disableRBAC,
|
||||
enableRBAC,
|
||||
mockFolder,
|
||||
mockRulerAlertingRule,
|
||||
mockRulerGrafanaRule,
|
||||
mockUnifiedAlertingStore,
|
||||
} from '../mocks';
|
||||
|
||||
import { useFolder } from './useFolder';
|
||||
import { useIsRuleEditable } from './useIsRuleEditable';
|
||||
@@ -166,7 +172,7 @@ function mockPermissions(grantedPermissions: AccessControlAction[]) {
|
||||
|
||||
function getProviderWrapper() {
|
||||
const dataSources = getMockedDataSources();
|
||||
const store = mockStore({ dataSources });
|
||||
const store = mockUnifiedAlertingStore({ dataSources });
|
||||
const wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>;
|
||||
return wrapper;
|
||||
}
|
||||
@@ -193,15 +199,3 @@ function getMockedDataSources(): StoreState['unifiedAlerting']['dataSources'] {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mockStore(unifiedAlerting?: Partial<StoreState['unifiedAlerting']>) {
|
||||
const defaultState = configureStore().getState();
|
||||
|
||||
return configureStore({
|
||||
...defaultState,
|
||||
unifiedAlerting: {
|
||||
...defaultState.unifiedAlerting,
|
||||
...unifiedAlerting,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import produce from 'immer';
|
||||
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
@@ -19,7 +21,8 @@ import {
|
||||
Silence,
|
||||
SilenceState,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction, FolderDTO } from 'app/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction, FolderDTO, StoreState } from 'app/types';
|
||||
import { Alert, AlertingRule, CombinedRule, RecordingRule, RuleGroup, RuleNamespace } from 'app/types/unified-alerting';
|
||||
import {
|
||||
GrafanaAlertStateDecision,
|
||||
@@ -480,3 +483,34 @@ export const grantUserPermissions = (permissions: AccessControlAction[]) => {
|
||||
.spyOn(contextSrv, 'hasPermission')
|
||||
.mockImplementation((action) => permissions.includes(action as AccessControlAction));
|
||||
};
|
||||
|
||||
export function mockDataSourcesStore(partial?: Partial<StoreState['dataSources']>) {
|
||||
const defaultState = configureStore().getState();
|
||||
const store = configureStore({
|
||||
...defaultState,
|
||||
dataSources: {
|
||||
...defaultState.dataSources,
|
||||
...partial,
|
||||
},
|
||||
});
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
export function mockUnifiedAlertingStore(unifiedAlerting?: Partial<StoreState['unifiedAlerting']>) {
|
||||
const defaultState = configureStore().getState();
|
||||
|
||||
return configureStore({
|
||||
...defaultState,
|
||||
unifiedAlerting: {
|
||||
...defaultState.unifiedAlerting,
|
||||
...unifiedAlerting,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function mockStore(recipe: (state: StoreState) => void) {
|
||||
const defaultState = configureStore().getState();
|
||||
|
||||
return configureStore(produce(defaultState, recipe));
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ export function getRulesDataSource(rulesSourceName: string) {
|
||||
|
||||
export function getAlertManagerDataSources() {
|
||||
return getAllDataSources()
|
||||
.filter((ds) => ds.type === DataSourceType.Alertmanager)
|
||||
.filter(
|
||||
(ds): ds is DataSourceInstanceSettings<AlertManagerDataSourceJsonData> => ds.type === DataSourceType.Alertmanager
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
import { urlUtil, UrlQueryMap, Labels } from '@grafana/data';
|
||||
import { urlUtil, UrlQueryMap, Labels, DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { alertInstanceKey } from 'app/features/alerting/unified/utils/rules';
|
||||
import { SortOrder } from 'app/plugins/panel/alertlist/types';
|
||||
@@ -98,6 +98,10 @@ export function makeLabelBasedSilenceLink(alertManagerSourceName: string, labels
|
||||
return `${config.appSubUrl}/alerting/silence/new?${silenceUrlParams.toString()}`;
|
||||
}
|
||||
|
||||
export function makeDataSourceLink<T>(dataSource: DataSourceInstanceSettings<T>) {
|
||||
return `${config.appSubUrl}/datasources/edit/${dataSource.uid}`;
|
||||
}
|
||||
|
||||
// keep retrying fn if it's error passes shouldRetry(error) and timeout has not elapsed yet
|
||||
export function retryWhile<T, E = Error>(
|
||||
fn: () => Promise<T>,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import produce from 'immer';
|
||||
import React from 'react';
|
||||
|
||||
import { SIGV4ConnectionConfig } from '@grafana/aws-sdk';
|
||||
import { DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { DataSourceHttpSettings, InlineFormLabel, Select } from '@grafana/ui';
|
||||
import { DataSourceHttpSettings, InlineField, InlineFormLabel, InlineSwitch, Select } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from './types';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<AlertManagerDataSourceJsonData>;
|
||||
|
||||
const IMPL_OPTIONS: SelectableValue[] = [
|
||||
const IMPL_OPTIONS: Array<SelectableValue<AlertManagerImplementation>> = [
|
||||
{
|
||||
value: AlertManagerImplementation.mimir,
|
||||
icon: 'public/img/alerting/mimir_logo.svg',
|
||||
@@ -48,13 +49,31 @@ export const ConfigEditor = (props: Props) => {
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
implementation: value.value as AlertManagerImplementation,
|
||||
implementation: value.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<InlineField
|
||||
label="Receive Grafana Alerts"
|
||||
tooltip="When enabled, Grafana-managed alerts are sent to this Alertmanager."
|
||||
labelWidth={26}
|
||||
>
|
||||
<InlineSwitch
|
||||
value={options.jsonData.handleGrafanaManagedAlerts ?? false}
|
||||
onChange={(e) => {
|
||||
onOptionsChange(
|
||||
produce(options, (draft) => {
|
||||
draft.jsonData.handleGrafanaManagedAlerts = e.currentTarget.checked;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</InlineField>
|
||||
</div>
|
||||
</div>
|
||||
<DataSourceHttpSettings
|
||||
defaultUrl={''}
|
||||
|
||||
@@ -277,12 +277,17 @@ export interface AlertmanagerUrl {
|
||||
|
||||
export interface ExternalAlertmanagersResponse {
|
||||
data: ExternalAlertmanagers;
|
||||
status: 'string';
|
||||
}
|
||||
|
||||
export enum AlertmanagerChoice {
|
||||
Internal = 'internal',
|
||||
External = 'external',
|
||||
All = 'all',
|
||||
}
|
||||
|
||||
export interface ExternalAlertmanagerConfig {
|
||||
alertmanagers: string[];
|
||||
alertmanagersChoice: string;
|
||||
alertmanagersChoice: AlertmanagerChoice;
|
||||
}
|
||||
|
||||
export enum AlertManagerImplementation {
|
||||
@@ -310,4 +315,7 @@ export type MuteTimeInterval = {
|
||||
provenance?: string;
|
||||
};
|
||||
|
||||
export type AlertManagerDataSourceJsonData = DataSourceJsonData & { implementation?: AlertManagerImplementation };
|
||||
export interface AlertManagerDataSourceJsonData extends DataSourceJsonData {
|
||||
implementation?: AlertManagerImplementation;
|
||||
handleGrafanaManagedAlerts?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user