diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 333a5cbce42..21759efc298 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.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') ), diff --git a/public/app/features/alerting/unified/AlertGroups.tsx b/public/app/features/alerting/unified/AlertGroups.tsx index 2513f9788eb..9aee9d3c81b 100644 --- a/public/app/features/alerting/unified/AlertGroups.tsx +++ b/public/app/features/alerting/unified/AlertGroups.tsx @@ -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 ( + + + + ); + } + return ( diff --git a/public/app/features/alerting/unified/AmRoutes.tsx b/public/app/features/alerting/unified/AmRoutes.tsx index 489de5941dc..eddb4012644 100644 --- a/public/app/features/alerting/unified/AmRoutes.tsx +++ b/public/app/features/alerting/unified/AmRoutes.tsx @@ -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 ; + return ( + + + + ); } return ( - + {resultError && !resultLoading && ( {resultError.message || 'Unknown error.'} diff --git a/public/app/features/alerting/unified/MuteTimings.tsx b/public/app/features/alerting/unified/MuteTimings.tsx index 97221db287e..3bee4439115 100644 --- a/public/app/features/alerting/unified/MuteTimings.tsx +++ b/public/app/features/alerting/unified/MuteTimings.tsx @@ -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); diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 3335aa988ee..f06fdfb57a7 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -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 ; + return isRoot ? ( + + + + ) : ( + + ); } return ( @@ -63,6 +72,7 @@ const Receivers: FC = () => { current={alertManagerSourceName} disabled={disableAmSelect} onChange={setAlertManagerSourceName} + dataSources={alertManagers} /> {error && !loading && ( diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 3b29811a926..57d66fc319f 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -75,7 +75,7 @@ const RuleEditor: FC = ({ match }) => { const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess(); - if (!canCreateGrafanaRules && !canCreateCloudRules) { + if (!identifier && !canCreateGrafanaRules && !canCreateCloudRules) { return Sorry! You are not allowed to create rules.; } diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index 828a26a23df..c0c1eda8135 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -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 ; + return isRoot ? ( + + + + ) : ( + + ); } return ( - - - + {error && !loading && ( {error.message || 'Unknown error.'} diff --git a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx index e775215be28..94f30bcc38c 100644 --- a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx +++ b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx @@ -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 = ({ 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 = ({ onChange, current, dataSources, disabled = false }) => { const styles = useStyles2(getStyles); const options: Array> = 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 ( ( + + We could not find any external Alertmanagers and you may not have access to the built-in Grafana Alertmanager. + +); + +const OtherAlertManagersAvailable = () => ( + + Selected Alertmanager no longer exists or you may not have permission to access it. + +); + +export const NoAlertManagerWarning = ({ availableAlertManagers }: Props) => { + const [_, setAlertManagerSourceName] = useAlertManagerSourceName(availableAlertManagers); + const hasOtherAMs = availableAlertManagers.length > 0; + + return ( +
+ {hasOtherAMs ? ( + <> + + + + ) : ( + + )} +
+ ); +}; diff --git a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx index aaa09860c15..fdf286df090 100644 --- a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx +++ b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx @@ -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 (
- + {loadingError && !loading && ( {loadingError.message || 'Unknown error.'} diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx index 7af9efe0958..2950864ac2b 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx @@ -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 (
- +
{ 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 ( - + {result && !loading && (
diff --git a/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx index 1b9bebabbb3..2f07e7dd0e8 100644 --- a/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx @@ -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 = ({ existing, alertManagerSourceName, oldConfig: config, alertManagerSourceName, successMessage: existing ? 'Contact point updated.' : 'Contact point created.', - redirectPath: makeAMLink('/alerting/notifications', alertManagerSourceName), + redirectPath: '/alerting/notifications', }) ); }; diff --git a/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx new file mode 100644 index 00000000000..aaf5a8a774c --- /dev/null +++ b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx @@ -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 }) => {children}; + 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 }) => {children}; + + 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 }) => {children}; + + 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 }) => {children}; + + 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 }) => {children}; + + 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 }) => {children}; + + 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); + }); +}); diff --git a/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.ts b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.ts index 8fe2c34a600..e453588911c 100644 --- a/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.ts +++ b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.ts @@ -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]; } diff --git a/public/app/features/alerting/unified/hooks/useAlertManagerSources.ts b/public/app/features/alerting/unified/hooks/useAlertManagerSources.ts new file mode 100644 index 00000000000..25177b0c1ae --- /dev/null +++ b/public/app/features/alerting/unified/hooks/useAlertManagerSources.ts @@ -0,0 +1,7 @@ +import { useMemo } from 'react'; + +import { getAlertManagerDataSourcesByPermission } from '../utils/datasource'; + +export function useAlertManagersByPermission(accessType: 'instance' | 'notification') { + return useMemo(() => getAlertManagerDataSourcesByPermission(accessType), [accessType]); +} diff --git a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts index 843a7ec3197..2f702ad3836 100644 --- a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts +++ b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts @@ -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> { - const [alertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('notification'); + const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers); const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs); return useMemo(() => { diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts index 9b736179118..57d785df5ec 100644 --- a/public/app/features/alerting/unified/utils/access-control.ts +++ b/public/app/features/alerting/unified/utils/access-control.ts @@ -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, diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts index b13997c4d78..216105023c5 100644 --- a/public/app/features/alerting/unified/utils/datasource.ts +++ b/public/app/features/alerting/unified/utils/datasource.ts @@ -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((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((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) { diff --git a/public/app/plugins/panel/alertGroups/module.tsx b/public/app/plugins/panel/alertGroups/module.tsx index 94ccb5c7e26..b2e37a74ff9 100644 --- a/public/app/plugins/panel/alertGroups/module.tsx +++ b/public/app/plugins/panel/alertGroups/module.tsx @@ -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(AlertGroupsPanel). defaultValue: GRAFANA_RULES_SOURCE_NAME, category: ['Options'], editor: function RenderAlertmanagerPicker(props) { + const alertManagers = useMemo(getAllAlertManagerDataSources, []); + return ( { return props.onChange(alertManagerSourceName); }} + dataSources={alertManagers} /> ); },