Alerting: Improved RBAC for Alert managers (#48344)

* Initial support for grafana or cloud only alert managers

* Handle missing alert manager

* Refactor code, fix tests

* Fix redirect url

* Bring back the test

* Improve missing alert manager warning, add useAlertManagerSourceName tests

* Fix lint errors

* Rename alert manager hook

* Refactor alert manager label creation

* Improve warnings' messages

* Fix linter

* Fix warning condition in RuleEditor
pull/48758/head
Konrad Lalik 3 years ago committed by GitHub
parent 696405ba7b
commit 65d7d466d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      public/app/features/alerting/routes.tsx
  2. 13
      public/app/features/alerting/unified/AlertGroups.tsx
  3. 18
      public/app/features/alerting/unified/AmRoutes.tsx
  4. 4
      public/app/features/alerting/unified/MuteTimings.tsx
  5. 14
      public/app/features/alerting/unified/Receivers.tsx
  6. 2
      public/app/features/alerting/unified/RuleEditor.tsx
  7. 27
      public/app/features/alerting/unified/Silences.tsx
  8. 36
      public/app/features/alerting/unified/components/AlertManagerPicker.tsx
  9. 42
      public/app/features/alerting/unified/components/NoAlertManagerWarning.tsx
  10. 11
      public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx
  11. 10
      public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx
  12. 11
      public/app/features/alerting/unified/components/amroutes/MuteTimingForm.tsx
  13. 3
      public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx
  14. 103
      public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx
  15. 39
      public/app/features/alerting/unified/hooks/useAlertManagerSourceName.ts
  16. 7
      public/app/features/alerting/unified/hooks/useAlertManagerSources.ts
  17. 4
      public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts
  18. 4
      public/app/features/alerting/unified/utils/access-control.ts
  19. 53
      public/app/features/alerting/unified/utils/datasource.ts
  20. 10
      public/app/plugins/panel/alertGroups/module.tsx

@ -132,7 +132,10 @@ const unifiedRoutes: RouteDescriptor[] = [
},
{
path: '/alerting/silences',
roles: evaluateAccess([AccessControlAction.AlertingInstanceRead], ['Editor', 'Admin']),
roles: evaluateAccess(
[AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead],
['Editor', 'Admin']
),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
),

@ -7,9 +7,11 @@ import { Alert, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import { AlertGroup } from './components/alert-groups/AlertGroup';
import { AlertGroupFilter } from './components/alert-groups/AlertGroupFilter';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useFilteredAmGroups } from './hooks/useFilteredAmGroups';
import { useGroupedAlerts } from './hooks/useGroupedAlerts';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
@ -19,7 +21,8 @@ import { getFiltersFromUrlParams } from './utils/misc';
import { initialAsyncRequestState } from './utils/redux';
const AlertGroups = () => {
const [alertManagerSourceName] = useAlertManagerSourceName();
const alertManagers = useAlertManagersByPermission('instance');
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const dispatch = useDispatch();
const [queryParams] = useQueryParams();
const { groupBy = [] } = getFiltersFromUrlParams(queryParams);
@ -48,6 +51,14 @@ const AlertGroups = () => {
};
}, [dispatch, alertManagerSourceName]);
if (!alertManagerSourceName) {
return (
<AlertingPageWrapper pageId="groups">
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
</AlertingPageWrapper>
);
}
return (
<AlertingPageWrapper pageId="groups">
<AlertGroupFilter groups={results} />

@ -1,7 +1,6 @@
import { css } from '@emotion/css';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui';
@ -11,10 +10,12 @@ import { useCleanup } from '../../../core/hooks/useCleanup';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import { AmRootRoute } from './components/amroutes/AmRootRoute';
import { AmSpecificRouting } from './components/amroutes/AmSpecificRouting';
import { MuteTimingsTable } from './components/amroutes/MuteTimingsTable';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAlertManagerConfigAction, updateAlertManagerConfigAction } from './state/actions';
import { AmRouteReceiver, FormAmRoute } from './types/amroutes';
@ -26,7 +27,8 @@ const AmRoutes: FC = () => {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const [isRootRouteEditMode, setIsRootRouteEditMode] = useState(false);
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const readOnly = alertManagerSourceName ? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) : true;
@ -100,12 +102,20 @@ const AmRoutes: FC = () => {
};
if (!alertManagerSourceName) {
return <Redirect to="/alerting/routes" />;
return (
<AlertingPageWrapper pageId="am-routes">
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
</AlertingPageWrapper>
);
}
return (
<AlertingPageWrapper pageId="am-routes">
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
<AlertManagerPicker
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
{resultError && !resultLoading && (
<Alert severity="error" title="Error loading Alertmanager config">
{resultError.message || 'Unknown error.'}

@ -8,6 +8,7 @@ import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import MuteTimingForm from './components/amroutes/MuteTimingForm';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAlertManagerConfigAction } from './state/actions';
import { initialAsyncRequestState } from './utils/redux';
@ -15,7 +16,8 @@ import { initialAsyncRequestState } from './utils/redux';
const MuteTimings = () => {
const [queryParams] = useQueryParams();
const dispatch = useDispatch();
const [alertManagerSourceName] = useAlertManagerSourceName();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);

@ -6,6 +6,7 @@ import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import { EditReceiverView } from './components/receivers/EditReceiverView';
import { EditTemplateView } from './components/receivers/EditTemplateView';
import { GlobalConfigForm } from './components/receivers/GlobalConfigForm';
@ -13,13 +14,15 @@ import { NewReceiverView } from './components/receivers/NewReceiverView';
import { NewTemplateView } from './components/receivers/NewTemplateView';
import { ReceiversAndTemplatesView } from './components/receivers/ReceiversAndTemplatesView';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAlertManagerConfigAction, fetchGrafanaNotifiersAction } from './state/actions';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import { initialAsyncRequestState } from './utils/redux';
const Receivers: FC = () => {
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const dispatch = useDispatch();
const location = useLocation();
@ -54,7 +57,13 @@ const Receivers: FC = () => {
const disableAmSelect = !isRoot;
if (!alertManagerSourceName) {
return <Redirect to="/alerting/notifications" />;
return isRoot ? (
<AlertingPageWrapper pageId="receivers">
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
</AlertingPageWrapper>
) : (
<Redirect to="/alerting/notifications" />
);
}
return (
@ -63,6 +72,7 @@ const Receivers: FC = () => {
current={alertManagerSourceName}
disabled={disableAmSelect}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
{error && !loading && (
<Alert severity="error" title="Error loading Alertmanager config">

@ -75,7 +75,7 @@ const RuleEditor: FC<RuleEditorProps> = ({ match }) => {
const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess();
if (!canCreateGrafanaRules && !canCreateCloudRules) {
if (!identifier && !canCreateGrafanaRules && !canCreateCloudRules) {
return <AlertWarning title="Cannot create rules">Sorry! You are not allowed to create rules.</AlertWarning>;
}

@ -1,24 +1,26 @@
import React, { FC, useEffect, useCallback } from 'react';
import React, { FC, useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui';
import { Silence } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { Authorize } from './components/Authorize';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import SilencesEditor from './components/silences/SilencesEditor';
import SilencesTable from './components/silences/SilencesTable';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAmAlertsAction, fetchSilencesAction } from './state/actions';
import { SILENCES_POLL_INTERVAL_MS } from './utils/constants';
import { AsyncRequestState, initialAsyncRequestState } from './utils/redux';
const Silences: FC = () => {
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
const alertManagers = useAlertManagersByPermission('instance');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const dispatch = useDispatch();
const silences = useUnifiedAlertingSelector((state) => state.silences);
const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts);
@ -49,14 +51,23 @@ const Silences: FC = () => {
const getSilenceById = useCallback((id: string) => result && result.find((silence) => silence.id === id), [result]);
if (!alertManagerSourceName) {
return <Redirect to="/alerting/silences" />;
return isRoot ? (
<AlertingPageWrapper pageId="silences">
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
</AlertingPageWrapper>
) : (
<Redirect to="/alerting/silences" />
);
}
return (
<AlertingPageWrapper pageId="silences">
<Authorize actions={[AccessControlAction.AlertingInstancesExternalRead]}>
<AlertManagerPicker disabled={!isRoot} current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
</Authorize>
<AlertManagerPicker
disabled={!isRoot}
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
{error && !loading && (
<Alert severity="error" title="Error loading silences">
{error.message || 'Unknown error.'}

@ -1,39 +1,33 @@
import { css } from '@emotion/css';
import React, { FC, useMemo } from 'react';
import { SelectableValue, GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Field, Select, useStyles2 } from '@grafana/ui';
import { getAllDataSources } from '../utils/config';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
interface Props {
onChange: (alertManagerSourceName: string) => void;
current?: string;
disabled?: boolean;
dataSources: AlertManagerDataSource[];
}
export const AlertManagerPicker: FC<Props> = ({ onChange, current, disabled = false }) => {
function getAlertManagerLabel(alertManager: AlertManagerDataSource) {
return alertManager.name === GRAFANA_RULES_SOURCE_NAME ? 'Grafana' : alertManager.name.slice(0, 37);
}
export const AlertManagerPicker: FC<Props> = ({ onChange, current, dataSources, disabled = false }) => {
const styles = useStyles2(getStyles);
const options: Array<SelectableValue<string>> = useMemo(() => {
return [
{
label: 'Grafana',
value: GRAFANA_RULES_SOURCE_NAME,
imgUrl: 'public/img/grafana_icon.svg',
meta: {},
},
...getAllDataSources()
.filter((ds) => ds.type === DataSourceType.Alertmanager)
.map((ds) => ({
label: ds.name.slice(0, 37),
value: ds.name,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
})),
];
}, []);
return dataSources.map((ds) => ({
label: getAlertManagerLabel(ds),
value: ds.name,
imgUrl: ds.imgUrl,
meta: ds.meta,
}));
}, [dataSources]);
return (
<Field

@ -0,0 +1,42 @@
import React from 'react';
import { Alert } from '@grafana/ui';
import { useAlertManagerSourceName } from '../hooks/useAlertManagerSourceName';
import { AlertManagerDataSource } from '../utils/datasource';
import { AlertManagerPicker } from './AlertManagerPicker';
interface Props {
availableAlertManagers: AlertManagerDataSource[];
}
const NoAlertManagersAvailable = () => (
<Alert title="No Alertmanager found" severity="warning">
We could not find any external Alertmanagers and you may not have access to the built-in Grafana Alertmanager.
</Alert>
);
const OtherAlertManagersAvailable = () => (
<Alert title="Selected Alertmanager not found. Select a different Alertmanager." severity="warning">
Selected Alertmanager no longer exists or you may not have permission to access it.
</Alert>
);
export const NoAlertManagerWarning = ({ availableAlertManagers }: Props) => {
const [_, setAlertManagerSourceName] = useAlertManagerSourceName(availableAlertManagers);
const hasOtherAMs = availableAlertManagers.length > 0;
return (
<div>
{hasOtherAMs ? (
<>
<AlertManagerPicker onChange={setAlertManagerSourceName} dataSources={availableAlertManagers} />
<OtherAlertManagersAvailable />
</>
) : (
<NoAlertManagersAvailable />
)}
</div>
);
};

@ -6,6 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Button, ConfirmModal, TextArea, HorizontalGroup, Field, Form, useStyles2 } from '@grafana/ui';
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from '../../hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import {
deleteAlertManagerConfigAction,
@ -22,7 +23,9 @@ interface FormValues {
export default function AlertmanagerConfig(): JSX.Element {
const dispatch = useDispatch();
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const [showConfirmDeleteAMConfig, setShowConfirmDeleteAMConfig] = useState(false);
const { loading: isDeleting } = useUnifiedAlertingSelector((state) => state.deleteAMConfig);
const { loading: isSaving } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
@ -75,7 +78,11 @@ export default function AlertmanagerConfig(): JSX.Element {
return (
<div className={styles.container}>
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
<AlertManagerPicker
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
{loadingError && !loading && (
<Alert severity="error" title="Error loading Alertmanager configuration">
{loadingError.message || 'Unknown error.'}

@ -7,6 +7,7 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types';
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from '../../hooks/useAlertManagerSources';
import { getFiltersFromUrlParams } from '../../utils/misc';
import { AlertManagerPicker } from '../AlertManagerPicker';
@ -24,7 +25,8 @@ export const AlertGroupFilter = ({ groups }: Props) => {
const { groupBy = [], queryString, alertState } = getFiltersFromUrlParams(queryParams);
const matcherFilterKey = `matcher-${filterKey}`;
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
const alertManagers = useAlertManagersByPermission('instance');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const styles = useStyles2(getStyles);
const clearFilters = () => {
@ -40,7 +42,11 @@ export const AlertGroupFilter = ({ groups }: Props) => {
return (
<div className={styles.wrapper}>
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
<AlertManagerPicker
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
<div className={styles.filterSection}>
<MatcherFilter
className={styles.filterInput}

@ -12,6 +12,7 @@ import {
} from 'app/plugins/datasource/alertmanager/types';
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from '../../hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { updateAlertManagerConfigAction } from '../../state/actions';
import { MuteTimingFields } from '../../types/mute-timing-form';
@ -57,7 +58,8 @@ const useDefaultValues = (muteTiming?: MuteTimeInterval): MuteTimingFields => {
const MuteTimingForm = ({ muteTiming, showError }: Props) => {
const dispatch = useDispatch();
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const styles = useStyles2(getStyles);
const defaultAmCortexConfig = { alertmanager_config: {}, template_files: {} };
@ -101,7 +103,12 @@ const MuteTimingForm = ({ muteTiming, showError }: Props) => {
return (
<AlertingPageWrapper pageId="am-routes">
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} disabled />
<AlertManagerPicker
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
disabled
dataSources={alertManagers}
/>
{result && !loading && (
<FormProvider {...formApi}>
<form onSubmit={formApi.handleSubmit(onSubmit)} data-testid="mute-timing-form">

@ -8,7 +8,6 @@ import { updateAlertManagerConfigAction } from '../../../state/actions';
import { CloudChannelValues, ReceiverFormValues, CloudChannelMap } from '../../../types/receiver-form';
import { cloudNotifierTypes } from '../../../utils/cloud-alertmanager-notifier-types';
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
import { makeAMLink } from '../../../utils/misc';
import {
cloudReceiverToFormValues,
formValuesToCloudReceiver,
@ -53,7 +52,7 @@ export const CloudReceiverForm: FC<Props> = ({ existing, alertManagerSourceName,
oldConfig: config,
alertManagerSourceName,
successMessage: existing ? 'Contact point updated.' : 'Contact point created.',
redirectPath: makeAMLink('/alerting/notifications', alertManagerSourceName),
redirectPath: '/alerting/notifications',
})
);
};

@ -0,0 +1,103 @@
import { renderHook } from '@testing-library/react-hooks';
import { createMemoryHistory } from 'history';
import React from 'react';
import { MemoryRouter, Router } from 'react-router-dom';
import store from 'app/core/store';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY } from '../utils/constants';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { useAlertManagerSourceName } from './useAlertManagerSourceName';
const grafanaAm: AlertManagerDataSource = {
name: GRAFANA_RULES_SOURCE_NAME,
imgUrl: '',
};
const externalAmProm: AlertManagerDataSource = {
name: 'PrometheusAm',
imgUrl: '',
};
const externalAmMimir: AlertManagerDataSource = {
name: 'MimirAm',
imgUrl: '',
};
describe('useAlertManagerSourceName', () => {
it('Should return undefined alert manager name when there are no available alert managers', () => {
const wrapper: React.FC = ({ children }) => <MemoryRouter>{children}</MemoryRouter>;
const { result } = renderHook(() => useAlertManagerSourceName([]), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBeUndefined();
});
it('Should return Grafana AM when it is available and no alert manager query param exists', () => {
const wrapper: React.FC = ({ children }) => <MemoryRouter>{children}</MemoryRouter>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(grafanaAm.name);
});
it('Should return alert manager included in the query param when available', () => {
const history = createMemoryHistory();
history.push({ search: `alertmanager=${externalAmProm.name}` });
const wrapper: React.FC = ({ children }) => <Router history={history}>{children}</Router>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(externalAmProm.name);
});
it('Should return undefined if alert manager included in the query is not available', () => {
const history = createMemoryHistory();
history.push({ search: `alertmanager=Not available external AM` });
const wrapper: React.FC = ({ children }) => <Router history={history}>{children}</Router>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(undefined);
});
it('Should return alert manager from store if available and query is empty', () => {
const wrapper: React.FC = ({ children }) => <MemoryRouter>{children}</MemoryRouter>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmProm.name);
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(externalAmProm.name);
});
it('Should prioritize the alert manager from query over store', () => {
const history = createMemoryHistory();
history.push({ search: `alertmanager=${externalAmProm.name}` });
const wrapper: React.FC = ({ children }) => <Router history={history}>{children}</Router>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmMimir.name);
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(externalAmProm.name);
});
});

@ -4,25 +4,31 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams';
import store from 'app/core/store';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants';
import { getAlertManagerDataSources, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
function isAlertManagerSource(alertManagerSourceName: string): boolean {
return (
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME ||
!!getAlertManagerDataSources().find((ds) => ds.name === alertManagerSourceName)
function useIsAlertManagerAvailable(availableAlertManagers: AlertManagerDataSource[]) {
return useCallback(
(alertManagerName: string) => {
const availableAlertManagersNames = availableAlertManagers.map((am) => am.name);
return availableAlertManagersNames.includes(alertManagerName);
},
[availableAlertManagers]
);
}
/* this will return am name either from query params or from local storage or a default (grafana).
*
* fallbackUrl - if provided, will redirect to this url if alertmanager provided in query no longer
/* This will return am name either from query params or from local storage or a default (grafana).
* Due to RBAC permissions Grafana Managed Alert manager or external alert managers may not be available
* In the worst case neihter GMA nor external alert manager is available
*/
export function useAlertManagerSourceName(): [string | undefined, (alertManagerSourceName: string) => void] {
export function useAlertManagerSourceName(
availableAlertManagers: AlertManagerDataSource[]
): [string | undefined, (alertManagerSourceName: string) => void] {
const [queryParams, updateQueryParams] = useQueryParams();
const isAlertManagerAvailable = useIsAlertManagerAvailable(availableAlertManagers);
const update = useCallback(
(alertManagerSourceName: string) => {
if (!isAlertManagerSource(alertManagerSourceName)) {
if (!isAlertManagerAvailable(alertManagerSourceName)) {
return;
}
if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME) {
@ -33,24 +39,29 @@ export function useAlertManagerSourceName(): [string | undefined, (alertManagerS
updateQueryParams({ [ALERTMANAGER_NAME_QUERY_KEY]: alertManagerSourceName });
}
},
[updateQueryParams]
[updateQueryParams, isAlertManagerAvailable]
);
const querySource = queryParams[ALERTMANAGER_NAME_QUERY_KEY];
if (querySource && typeof querySource === 'string') {
if (isAlertManagerSource(querySource)) {
if (isAlertManagerAvailable(querySource)) {
return [querySource, update];
} else {
// non existing alertmanager
return [undefined, update];
}
}
const storeSource = store.get(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
if (storeSource && typeof storeSource === 'string' && isAlertManagerSource(storeSource)) {
if (storeSource && typeof storeSource === 'string' && isAlertManagerAvailable(storeSource)) {
update(storeSource);
return [storeSource, update];
}
return [GRAFANA_RULES_SOURCE_NAME, update];
if (isAlertManagerAvailable(GRAFANA_RULES_SOURCE_NAME)) {
return [GRAFANA_RULES_SOURCE_NAME, update];
}
return [undefined, update];
}

@ -0,0 +1,7 @@
import { useMemo } from 'react';
import { getAlertManagerDataSourcesByPermission } from '../utils/datasource';
export function useAlertManagersByPermission(accessType: 'instance' | 'notification') {
return useMemo(() => getAlertManagerDataSourcesByPermission(accessType), [accessType]);
}

@ -7,10 +7,12 @@ import { timeIntervalToString } from '../utils/alertmanager';
import { initialAsyncRequestState } from '../utils/redux';
import { useAlertManagerSourceName } from './useAlertManagerSourceName';
import { useAlertManagersByPermission } from './useAlertManagerSources';
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
export function useMuteTimingOptions(): Array<SelectableValue<string>> {
const [alertManagerSourceName] = useAlertManagerSourceName();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
return useMemo(() => {

@ -9,7 +9,7 @@ function getRulesSourceType(alertManagerSourceName: string): RulesSourceType {
return isGrafanaRulesSource(alertManagerSourceName) ? 'grafana' : 'external';
}
const instancesPermissions = {
export const instancesPermissions = {
read: {
grafana: AccessControlAction.AlertingInstanceRead,
external: AccessControlAction.AlertingInstancesExternalRead,
@ -28,7 +28,7 @@ const instancesPermissions = {
},
};
const notificationsPermissions = {
export const notificationsPermissions = {
read: {
grafana: AccessControlAction.AlertingNotificationsRead,
external: AccessControlAction.AlertingNotificationsExternalRead,

@ -1,9 +1,10 @@
import { DataSourceJsonData, DataSourceInstanceSettings } from '@grafana/data';
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
import { contextSrv } from 'app/core/services/context_srv';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { RulesSource } from 'app/types/unified-alerting';
import { instancesPermissions, notificationsPermissions } from './access-control';
import { getAllDataSources } from './config';
export const GRAFANA_RULES_SOURCE_NAME = 'grafana';
@ -15,6 +16,12 @@ export enum DataSourceType {
Prometheus = 'prometheus',
}
export interface AlertManagerDataSource {
name: string;
imgUrl: string;
meta?: DataSourceInstanceSettings['meta'];
}
export const RulesDataSourceTypes: string[] = [DataSourceType.Loki, DataSourceType.Prometheus];
export function getRulesDataSources() {
@ -37,6 +44,50 @@ export function getAlertManagerDataSources() {
.sort((a, b) => a.name.localeCompare(b.name));
}
const grafanaAlertManagerDataSource: AlertManagerDataSource = {
name: GRAFANA_RULES_SOURCE_NAME,
imgUrl: 'public/img/grafana_icon.svg',
};
// Used only as a fallback for Alert Group plugin
export function getAllAlertManagerDataSources(): AlertManagerDataSource[] {
return [
grafanaAlertManagerDataSource,
...getAlertManagerDataSources().map<AlertManagerDataSource>((ds) => ({
name: ds.name,
displayName: ds.name,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
})),
];
}
export function getAlertManagerDataSourcesByPermission(
permission: 'instance' | 'notification'
): AlertManagerDataSource[] {
const availableDataSources: AlertManagerDataSource[] = [];
const permissions = {
instance: instancesPermissions.read,
notification: notificationsPermissions.read,
};
if (contextSrv.hasPermission(permissions[permission].grafana)) {
availableDataSources.push(grafanaAlertManagerDataSource);
}
if (contextSrv.hasPermission(permissions[permission].external)) {
const cloudSources = getAlertManagerDataSources().map<AlertManagerDataSource>((ds) => ({
name: ds.name,
displayName: ds.name,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
}));
availableDataSources.push(...cloudSources);
}
return availableDataSources;
}
export function getLotexDataSourceByName(dataSourceName: string): DataSourceInstanceSettings {
const dataSource = getDataSourceByName(dataSourceName);
if (!dataSource) {

@ -1,8 +1,11 @@
import React from 'react';
import React, { useMemo } from 'react';
import { PanelPlugin } from '@grafana/data';
import { AlertManagerPicker } from 'app/features/alerting/unified/components/AlertManagerPicker';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import {
getAllAlertManagerDataSources,
GRAFANA_RULES_SOURCE_NAME,
} from 'app/features/alerting/unified/utils/datasource';
import { AlertGroupsPanel } from './AlertGroupsPanel';
import { AlertGroupPanelOptions } from './types';
@ -16,12 +19,15 @@ export const plugin = new PanelPlugin<AlertGroupPanelOptions>(AlertGroupsPanel).
defaultValue: GRAFANA_RULES_SOURCE_NAME,
category: ['Options'],
editor: function RenderAlertmanagerPicker(props) {
const alertManagers = useMemo(getAllAlertManagerDataSources, []);
return (
<AlertManagerPicker
current={props.value}
onChange={(alertManagerSourceName) => {
return props.onChange(alertManagerSourceName);
}}
dataSources={alertManagers}
/>
);
},

Loading…
Cancel
Save