Alerting: Show converted alertmanager configurations in the UI (#109027)
This commit is contained in:
@@ -9,7 +9,7 @@ import { MATCHER_ALERT_RULE_UID } from 'app/features/alerting/unified/utils/cons
|
||||
import { parseQueryParamMatchers } from 'app/features/alerting/unified/utils/matchers';
|
||||
|
||||
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
|
||||
import { GrafanaAlertmanagerWarning } from './components/GrafanaAlertmanagerWarning';
|
||||
import { SilencesEditor } from './components/silences/SilencesEditor';
|
||||
import { useAlertmanager } from './state/AlertmanagerContext';
|
||||
import { withPageErrorBoundary } from './withPageErrorBoundary';
|
||||
@@ -27,7 +27,7 @@ const SilencesEditorComponent = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager} />
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager} />
|
||||
<SilencesEditor
|
||||
formValues={formValues}
|
||||
alertManagerSourceName={selectedAlertmanager}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { NotificationPoliciesList } from 'app/features/alerting/unified/componen
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities';
|
||||
|
||||
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
|
||||
import { GrafanaAlertmanagerWarning } from './components/GrafanaAlertmanagerWarning';
|
||||
import { TimeIntervalsTable } from './components/mute-timings/MuteTimingsTable';
|
||||
import { useAlertmanager } from './state/AlertmanagerContext';
|
||||
import { withPageErrorBoundary } from './withPageErrorBoundary';
|
||||
@@ -48,7 +48,7 @@ const NotificationPoliciesTabs = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager} />
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager} />
|
||||
<TabsBar>
|
||||
{policiesSupported && canSeePoliciesTab && (
|
||||
<Tab
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function fetchAlertManagerConfig(alertManagerSourceName: string): P
|
||||
alertmanager_config: result.data.alertmanager_config ?? {},
|
||||
last_applied: result.data.last_applied,
|
||||
id: result.data.id,
|
||||
extra_config: result.data.extra_config,
|
||||
};
|
||||
} catch (e) {
|
||||
// if no config has been uploaded to grafana, it returns error instead of latest config
|
||||
|
||||
@@ -191,6 +191,7 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
data: {
|
||||
alertmanager_config: status.config,
|
||||
template_files: {},
|
||||
extra_config: undefined,
|
||||
},
|
||||
}))
|
||||
);
|
||||
@@ -207,6 +208,7 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
alertmanager_config: {},
|
||||
template_files: {},
|
||||
template_file_provenances: {},
|
||||
extra_config: undefined,
|
||||
};
|
||||
|
||||
const lazyConfigInitSupported = alertmanagerFeatures?.lazyConfigInit ?? false;
|
||||
@@ -244,6 +246,7 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
template_file_provenances: result.template_file_provenances,
|
||||
last_applied: result.last_applied,
|
||||
id: result.id,
|
||||
extra_config: result.extra_config,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { t } from '@grafana/i18n';
|
||||
import { InlineField, Select, SelectMenuOptions, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useAlertmanager } from '../state/AlertmanagerContext';
|
||||
import { isExtraConfig } from '../utils/alertmanager/extraConfigs';
|
||||
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
interface Props {
|
||||
@@ -13,7 +14,11 @@ interface Props {
|
||||
}
|
||||
|
||||
function getAlertManagerLabel(alertManager: AlertManagerDataSource) {
|
||||
return alertManager.name === GRAFANA_RULES_SOURCE_NAME ? 'Grafana' : alertManager.name;
|
||||
if (alertManager.name === GRAFANA_RULES_SOURCE_NAME) {
|
||||
return 'Grafana';
|
||||
}
|
||||
|
||||
return alertManager.displayName || alertManager.name;
|
||||
}
|
||||
|
||||
export const AlertManagerPicker = ({ disabled = false }: Props) => {
|
||||
@@ -21,12 +26,50 @@ export const AlertManagerPicker = ({ disabled = false }: Props) => {
|
||||
const { selectedAlertmanager, availableAlertManagers, setSelectedAlertmanager } = useAlertmanager();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return availableAlertManagers.map<SelectableValue<string>>((ds) => ({
|
||||
label: getAlertManagerLabel(ds),
|
||||
value: ds.name,
|
||||
imgUrl: ds.imgUrl,
|
||||
meta: ds.meta,
|
||||
}));
|
||||
// Group alertmanagers
|
||||
const grafanaAM = availableAlertManagers.find((am) => am.name === GRAFANA_RULES_SOURCE_NAME);
|
||||
const extraConfig = availableAlertManagers.find((am) => isExtraConfig(am.name));
|
||||
const datasourceAMs = availableAlertManagers.filter(
|
||||
(am) => am.name !== GRAFANA_RULES_SOURCE_NAME && !isExtraConfig(am.name)
|
||||
);
|
||||
|
||||
const groupedOptions: Array<SelectableValue<string> | { label: string; options: Array<SelectableValue<string>> }> =
|
||||
[];
|
||||
|
||||
// Add Grafana alertmanager first
|
||||
if (grafanaAM) {
|
||||
groupedOptions.push({
|
||||
label: getAlertManagerLabel(grafanaAM),
|
||||
value: grafanaAM.name,
|
||||
imgUrl: grafanaAM.imgUrl,
|
||||
meta: grafanaAM.meta,
|
||||
});
|
||||
}
|
||||
|
||||
// Add extra config (single merged configuration)
|
||||
if (extraConfig) {
|
||||
groupedOptions.push({
|
||||
label: getAlertManagerLabel(extraConfig),
|
||||
value: extraConfig.name,
|
||||
imgUrl: extraConfig.imgUrl,
|
||||
meta: extraConfig.meta,
|
||||
});
|
||||
}
|
||||
|
||||
// Add external alertmanagers
|
||||
if (datasourceAMs.length > 0) {
|
||||
groupedOptions.push({
|
||||
label: t('alerting.alert-manager-picker.external-alertmanagers-group', 'External Alertmanagers'),
|
||||
options: datasourceAMs.map((ds) => ({
|
||||
label: getAlertManagerLabel(ds),
|
||||
value: ds.name,
|
||||
imgUrl: ds.imgUrl,
|
||||
meta: ds.meta,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return groupedOptions;
|
||||
}, [availableAlertManagers]);
|
||||
|
||||
const isDisabled = disabled || options.length === 1;
|
||||
@@ -61,12 +104,22 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
field: css({
|
||||
margin: 0,
|
||||
}),
|
||||
optionContent: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
whiteSpace: 'pre-line',
|
||||
}),
|
||||
});
|
||||
|
||||
// custom option that overwrites the default "white-space: nowrap" for Alertmanager names that are really long
|
||||
const CustomOption = (props: ComponentProps<typeof SelectMenuOptions>) => (
|
||||
<SelectMenuOptions
|
||||
{...props}
|
||||
renderOptionLabel={({ label }) => <div style={{ whiteSpace: 'pre-line' }}>{label}</div>}
|
||||
/>
|
||||
);
|
||||
const CustomOption = (props: ComponentProps<typeof SelectMenuOptions>) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<SelectMenuOptions
|
||||
{...props}
|
||||
renderOptionLabel={({ label }) => <div className={styles.optionContent}>{label}</div>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-9
@@ -10,10 +10,10 @@ import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/
|
||||
import { grantUserPermissions } from '../mocks';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from './GrafanaAlertmanagerDeliveryWarning';
|
||||
import { GrafanaAlertmanagerWarning } from './GrafanaAlertmanagerWarning';
|
||||
setupMswServer();
|
||||
|
||||
describe('GrafanaAlertmanagerDeliveryWarning', () => {
|
||||
describe('GrafanaAlertmanagerWarning', () => {
|
||||
beforeEach(() => {
|
||||
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
|
||||
});
|
||||
@@ -21,9 +21,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
|
||||
it('Should not render when the datasource is not Grafana', () => {
|
||||
setAlertmanagerChoices(AlertmanagerChoice.External, 0);
|
||||
|
||||
const { container } = renderWithStore(
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager="custom-alertmanager" />
|
||||
);
|
||||
const { container } = renderWithStore(<GrafanaAlertmanagerWarning currentAlertmanager="custom-alertmanager" />);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
@@ -31,7 +29,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
|
||||
it('Should render warning when the datasource is Grafana and using external AM', async () => {
|
||||
setAlertmanagerChoices(AlertmanagerChoice.External, 1);
|
||||
|
||||
renderWithStore(<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
|
||||
renderWithStore(<GrafanaAlertmanagerWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
|
||||
|
||||
expect(await screen.findByText('Grafana alerts are not delivered to Grafana Alertmanager')).toBeVisible();
|
||||
});
|
||||
@@ -39,7 +37,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
|
||||
it('Should render warning when the datasource is Grafana and using All AM', async () => {
|
||||
setAlertmanagerChoices(AlertmanagerChoice.All, 1);
|
||||
|
||||
renderWithStore(<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
|
||||
renderWithStore(<GrafanaAlertmanagerWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />);
|
||||
|
||||
expect(await screen.findByText('You have additional Alertmanagers to configure')).toBeVisible();
|
||||
});
|
||||
@@ -48,7 +46,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
|
||||
setAlertmanagerChoices(AlertmanagerChoice.Internal, 1);
|
||||
|
||||
const { container } = renderWithStore(
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -60,7 +58,7 @@ describe('GrafanaAlertmanagerDeliveryWarning', () => {
|
||||
setAlertmanagerChoices(AlertmanagerChoice.All, 0);
|
||||
|
||||
const { container } = renderWithStore(
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={GRAFANA_RULES_SOURCE_NAME} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
+29
-2
@@ -7,13 +7,23 @@ import { Alert, useStyles2 } from '@grafana/ui';
|
||||
import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types';
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities';
|
||||
import { isExtraConfig } from '../utils/alertmanager/extraConfigs';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
interface GrafanaAlertmanagerDeliveryWarningProps {
|
||||
interface GrafanaAlertmanagerWarningProps {
|
||||
currentAlertmanager: string;
|
||||
}
|
||||
|
||||
export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: GrafanaAlertmanagerDeliveryWarningProps) {
|
||||
export function GrafanaAlertmanagerWarning({ currentAlertmanager }: GrafanaAlertmanagerWarningProps) {
|
||||
return (
|
||||
<>
|
||||
<GrafanaExternalAlertmanagerConfigWarning currentAlertmanager={currentAlertmanager} />
|
||||
<GrafanaExtraConfigWarning currentAlertmanager={currentAlertmanager} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GrafanaExternalAlertmanagerConfigWarning({ currentAlertmanager }: GrafanaAlertmanagerWarningProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const externalAlertmanager = currentAlertmanager !== GRAFANA_RULES_SOURCE_NAME;
|
||||
|
||||
@@ -87,6 +97,23 @@ export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: Graf
|
||||
return null;
|
||||
}
|
||||
|
||||
function GrafanaExtraConfigWarning({ currentAlertmanager }: GrafanaAlertmanagerWarningProps) {
|
||||
const isSelectedExtraConfig = currentAlertmanager && isExtraConfig(currentAlertmanager);
|
||||
|
||||
if (!isSelectedExtraConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert title={t('alerting.alert-manager-picker.extra-config-warning.title', 'Imported configuration')}>
|
||||
<Trans i18nKey="alerting.alert-manager-picker.extra-config-warning.content">
|
||||
This shows the merged configuration of Grafana alertmanager with imported configurations. This merged view is
|
||||
read-only in the UI.
|
||||
</Trans>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
adminHint: css({
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
@@ -23,10 +23,11 @@ import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbili
|
||||
import { usePagination } from '../../hooks/usePagination';
|
||||
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { isExtraConfig } from '../../utils/alertmanager/extraConfigs';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
|
||||
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
|
||||
import { GrafanaAlertmanagerWarning } from '../GrafanaAlertmanagerWarning';
|
||||
|
||||
import { ContactPoint } from './ContactPoint';
|
||||
import { NotificationTemplates } from './NotificationTemplates';
|
||||
@@ -140,8 +141,12 @@ const ContactPointsTab = () => {
|
||||
) : (
|
||||
<ContactPointsList contactPoints={contactPoints} search={search} pageSize={DEFAULT_PAGE_SIZE} />
|
||||
)}
|
||||
|
||||
{/* Grafana manager Alertmanager does not support global config, Mimir and Cortex do */}
|
||||
{!isGrafanaManagedAlertmanager && <GlobalConfigAlert alertManagerName={selectedAlertmanager!} />}
|
||||
{/* Extra configs also don't support global config */}
|
||||
{!isGrafanaManagedAlertmanager && !isExtraConfig(selectedAlertmanager!) && (
|
||||
<GlobalConfigAlert alertManagerName={selectedAlertmanager!} />
|
||||
)}
|
||||
{ExportDrawer}
|
||||
</>
|
||||
);
|
||||
@@ -220,7 +225,7 @@ export const ContactPointsPageContents = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager!} />
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager!} />
|
||||
<Stack direction="column">
|
||||
<TabsBar>
|
||||
{showContactPointsTab && (
|
||||
|
||||
@@ -39,7 +39,7 @@ import { matcherFieldToMatcher } from '../../utils/alertmanager';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
|
||||
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
|
||||
import { GrafanaAlertmanagerWarning } from '../GrafanaAlertmanagerWarning';
|
||||
|
||||
import MatchersField from './MatchersField';
|
||||
import { SilencePeriod } from './SilencePeriod';
|
||||
@@ -118,7 +118,7 @@ const ExistingSilenceEditor = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={alertManagerSourceName} />
|
||||
<SilencesEditor ruleUid={ruleUid} formValues={defaultValues} alertManagerSourceName={alertManagerSourceName} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@ import { withPageErrorBoundary } from '../../withPageErrorBoundary';
|
||||
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
|
||||
import { Authorize } from '../Authorize';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
|
||||
import { GrafanaAlertmanagerWarning } from '../GrafanaAlertmanagerWarning';
|
||||
|
||||
import { Matchers } from './Matchers';
|
||||
import { NoSilencesSplash } from './NoSilencesCTA';
|
||||
@@ -146,7 +146,7 @@ const SilencesTable = () => {
|
||||
|
||||
return (
|
||||
<div data-testid="silences-table">
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={alertManagerSourceName} />
|
||||
{!!silences.length && (
|
||||
<Stack direction="column">
|
||||
<SilencesFilter />
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import store from 'app/core/store';
|
||||
import { AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import * as useAlertManagerSources from '../hooks/useAlertManagerSources';
|
||||
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY } from '../utils/constants';
|
||||
@@ -21,14 +23,21 @@ const externalAmMimir: AlertManagerDataSource = {
|
||||
imgUrl: '',
|
||||
};
|
||||
|
||||
function getProviderWrapper() {
|
||||
const reduxStore = configureStore();
|
||||
return ({ children }: React.PropsWithChildren) => (
|
||||
<Provider store={reduxStore}>
|
||||
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('useAlertmanager', () => {
|
||||
it('Should return undefined alert manager name when there are no available alert managers', () => {
|
||||
jest
|
||||
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
|
||||
.mockReturnValueOnce({ availableExternalDataSources: [], availableInternalDataSources: [] });
|
||||
const wrapper = ({ children }: React.PropsWithChildren) => (
|
||||
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
||||
);
|
||||
const wrapper = getProviderWrapper();
|
||||
|
||||
const { result } = renderHook(() => useAlertmanager(), { wrapper });
|
||||
expect(result.current.selectedAlertmanager).toBe(undefined);
|
||||
@@ -40,9 +49,7 @@ describe('useAlertmanager', () => {
|
||||
availableInternalDataSources: [{ name: GRAFANA_RULES_SOURCE_NAME, imgUrl: '', hasConfigurationAPI: true }],
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren) => (
|
||||
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
||||
);
|
||||
const wrapper = getProviderWrapper();
|
||||
|
||||
const { result } = renderHook(() => useAlertmanager(), { wrapper });
|
||||
expect(result.current.selectedAlertmanager).toBe(GRAFANA_RULES_SOURCE_NAME);
|
||||
@@ -55,9 +62,7 @@ describe('useAlertmanager', () => {
|
||||
|
||||
locationService.push({ search: `alertmanager=${externalAmProm.name}` });
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren) => (
|
||||
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
||||
);
|
||||
const wrapper = getProviderWrapper();
|
||||
|
||||
const { result } = renderHook(() => useAlertmanager(), { wrapper });
|
||||
expect(result.current.selectedAlertmanager).toBe(externalAmProm.name);
|
||||
@@ -70,9 +75,7 @@ describe('useAlertmanager', () => {
|
||||
|
||||
locationService.push({ search: `alertmanager=Not available external AM` });
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren) => (
|
||||
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
||||
);
|
||||
const wrapper = getProviderWrapper();
|
||||
|
||||
const { result } = renderHook(() => useAlertmanager(), { wrapper });
|
||||
expect(result.current.selectedAlertmanager).toBe(undefined);
|
||||
@@ -83,9 +86,7 @@ describe('useAlertmanager', () => {
|
||||
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
|
||||
.mockReturnValueOnce({ availableExternalDataSources: [externalAmProm], availableInternalDataSources: [] });
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren) => (
|
||||
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
||||
);
|
||||
const wrapper = getProviderWrapper();
|
||||
|
||||
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmProm.name);
|
||||
locationService.push({ search: '' });
|
||||
@@ -101,9 +102,7 @@ describe('useAlertmanager', () => {
|
||||
|
||||
locationService.push({ search: `alertmanager=${externalAmProm.name}` });
|
||||
|
||||
const wrapper = ({ children }: React.PropsWithChildren) => (
|
||||
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
||||
);
|
||||
const wrapper = getProviderWrapper();
|
||||
|
||||
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmMimir.name);
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import store from 'app/core/store';
|
||||
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
||||
import grafanaIconSvg from 'img/grafana_icon.svg';
|
||||
|
||||
import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
|
||||
import { useAlertmanagerConfig } from '../hooks/useAlertmanagerConfig';
|
||||
import { EXTRA_CONFIG_UID, isExtraConfig } from '../utils/alertmanager/extraConfigs';
|
||||
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants';
|
||||
import {
|
||||
AlertManagerDataSource,
|
||||
@@ -33,9 +36,46 @@ const AlertmanagerProvider = ({ children, accessType, alertmanagerSourceName }:
|
||||
const queryParams = locationService.getSearch();
|
||||
const updateQueryParams = locationService.partial;
|
||||
const allAvailableAlertManagers = useAlertManagersByPermission(accessType);
|
||||
const availableAlertManagers = allAvailableAlertManagers.availableInternalDataSources.concat(
|
||||
allAvailableAlertManagers.availableExternalDataSources
|
||||
);
|
||||
const isExtraConfigEnabled = config.featureToggles.alertingImportAlertmanagerUI ?? false;
|
||||
|
||||
const { data: grafanaConfig } = useAlertmanagerConfig(isExtraConfigEnabled ? GRAFANA_RULES_SOURCE_NAME : undefined, {
|
||||
refetchOnFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
const hasExtraConfigs = Boolean(grafanaConfig?.extra_config && grafanaConfig.extra_config.length > 0);
|
||||
|
||||
const availableAlertManagers = React.useMemo(() => {
|
||||
const regularAlertManagers = allAvailableAlertManagers.availableInternalDataSources.concat(
|
||||
allAvailableAlertManagers.availableExternalDataSources
|
||||
);
|
||||
|
||||
const extraConfigDataSource: AlertManagerDataSource[] =
|
||||
isExtraConfigEnabled && hasExtraConfigs
|
||||
? [
|
||||
{
|
||||
name: EXTRA_CONFIG_UID,
|
||||
displayName: 'Grafana (imported)',
|
||||
imgUrl: grafanaIconSvg,
|
||||
hasConfigurationAPI: false,
|
||||
handleGrafanaManagedAlerts: true,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
// list in order: Grafana -> Extra Config -> Extra Alertmanagers
|
||||
const grafanaAlertmanager = regularAlertManagers.find((am) => am.name === GRAFANA_RULES_SOURCE_NAME);
|
||||
const datasourceAlertmanagers = regularAlertManagers.filter((am) => am.name !== GRAFANA_RULES_SOURCE_NAME);
|
||||
const orderedAlertManagers: AlertManagerDataSource[] = [];
|
||||
|
||||
if (grafanaAlertmanager) {
|
||||
orderedAlertManagers.push(grafanaAlertmanager);
|
||||
}
|
||||
|
||||
orderedAlertManagers.push(...extraConfigDataSource);
|
||||
orderedAlertManagers.push(...datasourceAlertmanagers);
|
||||
|
||||
return orderedAlertManagers;
|
||||
}, [allAvailableAlertManagers, isExtraConfigEnabled, hasExtraConfigs]);
|
||||
|
||||
const updateSelectedAlertmanager = React.useCallback(
|
||||
(selectedAlertManager: string) => {
|
||||
@@ -73,7 +113,16 @@ const AlertmanagerProvider = ({ children, accessType, alertmanagerSourceName }:
|
||||
? desiredAlertmanager
|
||||
: undefined;
|
||||
|
||||
const selectedAlertmanagerConfig = getAlertmanagerDataSourceByName(selectedAlertmanager)?.jsonData;
|
||||
const selectedAlertmanagerConfig = React.useMemo(() => {
|
||||
if (selectedAlertmanager && isExtraConfig(selectedAlertmanager)) {
|
||||
const config: AlertManagerDataSourceJsonData = {
|
||||
implementation: AlertManagerImplementation.prometheus,
|
||||
handleGrafanaManagedAlerts: true,
|
||||
};
|
||||
return config;
|
||||
}
|
||||
return getAlertmanagerDataSourceByName(selectedAlertmanager)?.jsonData;
|
||||
}, [selectedAlertmanager]);
|
||||
|
||||
// determine if we're dealing with an Alertmanager data source that supports the ruler API
|
||||
const isGrafanaAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { EXTRA_CONFIG_UID, isExtraConfig } from './extraConfigs';
|
||||
|
||||
describe('extraConfigs utilities', () => {
|
||||
describe('isExtraConfig', () => {
|
||||
it('should return true for the exact extra config UID', () => {
|
||||
expect(isExtraConfig(EXTRA_CONFIG_UID)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-extra config names', () => {
|
||||
expect(isExtraConfig('grafana')).toBe(false);
|
||||
expect(isExtraConfig('prometheus-am')).toBe(false);
|
||||
expect(isExtraConfig('regular-alertmanager')).toBe(false);
|
||||
expect(isExtraConfig('')).toBe(false);
|
||||
expect(isExtraConfig('~grafana-converted-extra-config-test')).toBe(false); // old pattern
|
||||
expect(isExtraConfig('~grafana-with-extra-config-suffix')).toBe(false); // with suffix
|
||||
});
|
||||
});
|
||||
|
||||
describe('EXTRA_CONFIG_UID constant', () => {
|
||||
it('should have the correct UID value', () => {
|
||||
expect(EXTRA_CONFIG_UID).toBe('~grafana-with-extra-config');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
export const EXTRA_CONFIG_UID = '~grafana-with-extra-config';
|
||||
|
||||
export interface ExtraConfiguration {
|
||||
identifier: string;
|
||||
source?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface AlertingConfigResponse {
|
||||
extra_config?: ExtraConfiguration[];
|
||||
}
|
||||
|
||||
export function isExtraConfig(name: string): boolean {
|
||||
return name === EXTRA_CONFIG_UID;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
|
||||
import { isAlertManagerWithConfigAPI } from '../state/AlertmanagerContext';
|
||||
|
||||
import { instancesPermissions, notificationsPermissions, silencesPermissions } from './access-control';
|
||||
import { isExtraConfig } from './alertmanager/extraConfigs';
|
||||
import { getAllDataSources } from './config';
|
||||
import { isGrafanaRuleIdentifier } from './rules';
|
||||
|
||||
@@ -52,6 +53,7 @@ export enum DataSourceType {
|
||||
|
||||
export interface AlertManagerDataSource {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
imgUrl: string;
|
||||
meta?: DataSourceInstanceSettings['meta'];
|
||||
hasConfigurationAPI?: boolean;
|
||||
@@ -300,6 +302,11 @@ export function getDatasourceAPIUid(dataSourceName: string) {
|
||||
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
|
||||
return GRAFANA_RULES_SOURCE_NAME;
|
||||
}
|
||||
|
||||
if (isExtraConfig(dataSourceName)) {
|
||||
return dataSourceName;
|
||||
}
|
||||
|
||||
const ds = getDataSourceByName(dataSourceName);
|
||||
if (!ds) {
|
||||
throw new Error(`Datasource "${dataSourceName}" not found`);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//DOCS: https://prometheus.io/docs/alerting/latest/configuration/
|
||||
import { DataSourceJsonData, WithAccessControlMetadata } from '@grafana/data';
|
||||
import { IoK8SApimachineryPkgApisMetaV1ObjectMeta } from 'app/features/alerting/unified/openapi/receiversApi.gen';
|
||||
import { ExtraConfiguration } from 'app/features/alerting/unified/utils/alertmanager/extraConfigs';
|
||||
|
||||
export const ROUTES_META_SYMBOL = Symbol('routes_metadata');
|
||||
|
||||
@@ -11,6 +12,7 @@ export type AlertManagerCortexConfig = {
|
||||
template_file_provenances?: Record<string, string>;
|
||||
last_applied?: string;
|
||||
id?: number;
|
||||
extra_config?: ExtraConfiguration[];
|
||||
};
|
||||
|
||||
export type TLSConfig = {
|
||||
|
||||
@@ -493,6 +493,11 @@
|
||||
"title-muting-grouping-and-timings": "Muting, grouping, and timings"
|
||||
},
|
||||
"alert-manager-picker": {
|
||||
"external-alertmanagers-group": "External Alertmanagers",
|
||||
"extra-config-warning": {
|
||||
"content": "This shows the merged configuration of Grafana alertmanager with imported configurations. This merged view is read-only in the UI.",
|
||||
"title": "Imported configuration"
|
||||
},
|
||||
"noOptionsMessage-no-datasources-found": "No datasources found"
|
||||
},
|
||||
"alert-menu": {
|
||||
|
||||
Reference in New Issue
Block a user