mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
696405ba7b
commit
65d7d466d7
@ -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…
Reference in New Issue
Block a user