diff --git a/packages/grafana-ui/src/components/Forms/Field.tsx b/packages/grafana-ui/src/components/Forms/Field.tsx index c0a3814fc81..08031f40be6 100644 --- a/packages/grafana-ui/src/components/Forms/Field.tsx +++ b/packages/grafana-ui/src/components/Forms/Field.tsx @@ -12,7 +12,7 @@ export interface FieldProps extends HTMLAttributes { /** Label for the field */ label?: React.ReactNode; /** Description of the field */ - description?: string; + description?: React.ReactNode; /** Indicates if field is in invalid state */ invalid?: boolean; /** Indicates if field is in loading state */ diff --git a/public/app/core/hooks/useQueryParams.ts b/public/app/core/hooks/useQueryParams.ts index 24771d2f793..010f99fccd0 100644 --- a/public/app/core/hooks/useQueryParams.ts +++ b/public/app/core/hooks/useQueryParams.ts @@ -1,7 +1,7 @@ import { UrlQueryMap } from '@grafana/data'; import { locationSearchToObject, locationService } from '@grafana/runtime'; import { useCallback, useMemo } from 'react'; -import { useLocation } from 'react-use'; +import { useLocation } from 'react-router-dom'; export function useQueryParams(): [UrlQueryMap, (values: UrlQueryMap, replace?: boolean) => void] { const { search } = useLocation(); diff --git a/public/app/features/alerting/unified/AmRoutes.tsx b/public/app/features/alerting/unified/AmRoutes.tsx index d472a4e72eb..302324ea73a 100644 --- a/public/app/features/alerting/unified/AmRoutes.tsx +++ b/public/app/features/alerting/unified/AmRoutes.tsx @@ -1,6 +1,7 @@ import { Alert, Field, LoadingPlaceholder } from '@grafana/ui'; import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; +import { Redirect } from 'react-router-dom'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; @@ -15,10 +16,17 @@ const AmRoutes: FC = () => { const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs); useEffect(() => { - dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); + if (alertManagerSourceName) { + dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); + } }, [alertManagerSourceName, dispatch]); - const { result, loading, error } = amConfigs[alertManagerSourceName] || initialAsyncRequestState; + const { result, loading, error } = + (alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState; + + if (!alertManagerSourceName) { + return ; + } return ( diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 92c87d59467..9392a4fc44e 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -1,10 +1,12 @@ import { Field, Alert, LoadingPlaceholder } from '@grafana/ui'; import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; +import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertManagerPicker } from './components/AlertManagerPicker'; -import { ReceiversTable } from './components/receivers/ReceiversTable'; -import { TemplatesTable } from './components/receivers/TemplatesTable'; +import { EditTemplateView } from './components/receivers/EditTemplateView'; +import { NewTemplateView } from './components/receivers/NewTemplateView'; +import { ReceiversAndTemplatesView } from './components/receivers/ReceiversAndTemplatesView'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchAlertManagerConfigAction, fetchGrafanaNotifiersAction } from './state/actions'; @@ -15,12 +17,22 @@ const Receivers: FC = () => { const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); const dispatch = useDispatch(); - const config = useUnifiedAlertingSelector((state) => state.amConfigs); + const location = useLocation(); + const isRoot = location.pathname.endsWith('/alerting/notifications'); + + const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs); + + const { result: config, loading, error } = + (alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState; const receiverTypes = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); + const shouldLoadConfig = isRoot || !config; + useEffect(() => { - dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); - }, [alertManagerSourceName, dispatch]); + if (alertManagerSourceName && shouldLoadConfig) { + dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); + } + }, [alertManagerSourceName, dispatch, shouldLoadConfig]); useEffect(() => { if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && !(receiverTypes.result || receiverTypes.loading)) { @@ -28,11 +40,15 @@ const Receivers: FC = () => { } }, [alertManagerSourceName, dispatch, receiverTypes]); - const { result, loading, error } = config[alertManagerSourceName] || initialAsyncRequestState; + const disableAmSelect = !isRoot; + + if (!alertManagerSourceName) { + return ; + } return ( - + {error && !loading && ( @@ -40,12 +56,27 @@ const Receivers: FC = () => { {error.message || 'Unknown error.'} )} - {loading && } - {result && !loading && !error && ( - <> - - - + {loading && !config && } + {config && !error && ( + + + + + + + + + {({ match }: RouteChildrenProps<{ name: string }>) => + match?.params.name && ( + + ) + } + + )} ); diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index e9cca96f929..5c4f50f78d9 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -20,7 +20,8 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import { SerializedError } from '@reduxjs/toolkit'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import userEvent from '@testing-library/user-event'; -import { setDataSourceSrv } from '@grafana/runtime'; +import { locationService, setDataSourceSrv } from '@grafana/runtime'; +import { Router } from 'react-router-dom'; jest.mock('./api/prometheus'); jest.mock('./utils/config'); @@ -38,7 +39,9 @@ const renderRuleList = () => { return render( - + + + ); }; diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index 155f7802016..ea8221c3b5b 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -17,7 +17,6 @@ import RulesFilter from './components/rules/RulesFilter'; import { RuleListGroupView } from './components/rules/RuleListGroupView'; import { RuleListStateView } from './components/rules/RuleListStateView'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { config } from '@grafana/runtime'; const VIEWS = { groups: RuleListGroupView, @@ -97,15 +96,13 @@ export const RuleList: FC = () => { )} {promReqeustErrors.map(({ dataSource, error }) => (
- Failed to load rules state from{' '} - {dataSource.name}:{' '} + Failed to load rules state from {dataSource.name}:{' '} {error.message || 'Unknown error.'}
))} {rulerRequestErrors.map(({ dataSource, error }) => (
- Failed to load rules config from{' '} - {dataSource.name}:{' '} + Failed to load rules config from {dataSource.name}:{' '} {error.message || 'Unknown error.'}
))} @@ -117,19 +114,19 @@ export const RuleList: FC = () => {
- + Groups - + State diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index bbb16c5a359..6a531a540e0 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -1,6 +1,7 @@ import { Field, Alert, LoadingPlaceholder } from '@grafana/ui'; import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; +import { Redirect } from 'react-router-dom'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; @@ -15,10 +16,17 @@ const Silences: FC = () => { const silences = useUnifiedAlertingSelector((state) => state.silences); useEffect(() => { - dispatch(fetchSilencesAction(alertManagerSourceName)); + if (alertManagerSourceName) { + dispatch(fetchSilencesAction(alertManagerSourceName)); + } }, [alertManagerSourceName, dispatch]); - const { result, loading, error } = silences[alertManagerSourceName] || initialAsyncRequestState; + const { result, loading, error } = + (alertManagerSourceName && silences[alertManagerSourceName]) || initialAsyncRequestState; + + if (!alertManagerSourceName) { + return ; + } return ( diff --git a/public/app/features/alerting/unified/api/alertmanager.ts b/public/app/features/alerting/unified/api/alertmanager.ts index 90b42037b50..4cf251b3e61 100644 --- a/public/app/features/alerting/unified/api/alertmanager.ts +++ b/public/app/features/alerting/unified/api/alertmanager.ts @@ -10,11 +10,11 @@ import { import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; // "grafana" for grafana-managed, otherwise a datasource name -export async function fetchAlertManagerConfig(alertmanagerSourceName: string): Promise { +export async function fetchAlertManagerConfig(alertManagerSourceName: string): Promise { try { const result = await getBackendSrv() .fetch({ - url: `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/config/api/v1/alerts`, + url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/alerts`, showErrorAlert: false, showSuccessAlert: false, }) @@ -26,7 +26,7 @@ export async function fetchAlertManagerConfig(alertmanagerSourceName: string): P } catch (e) { // if no config has been uploaded to grafana, it returns error instead of latest config if ( - alertmanagerSourceName === GRAFANA_RULES_SOURCE_NAME && + alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && e.data?.message?.includes('failed to get latest configuration') ) { return { @@ -39,19 +39,24 @@ export async function fetchAlertManagerConfig(alertmanagerSourceName: string): P } export async function updateAlertmanagerConfig( - alertmanagerSourceName: string, + alertManagerSourceName: string, config: AlertManagerCortexConfig ): Promise { - await getBackendSrv().post( - `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/config/api/v1/alerts`, - config - ); + await getBackendSrv() + .fetch({ + method: 'POST', + url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/alerts`, + data: config, + showErrorAlert: false, + showSuccessAlert: false, + }) + .toPromise(); } -export async function fetchSilences(alertmanagerSourceName: string): Promise { +export async function fetchSilences(alertManagerSourceName: string): Promise { const result = await getBackendSrv() .fetch({ - url: `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/silences`, + url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/api/v2/silences`, showErrorAlert: false, showSuccessAlert: false, }) diff --git a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx index 1b35a1df85d..e7a437bf80b 100644 --- a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx +++ b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx @@ -7,9 +7,10 @@ import { getAllDataSources } from '../utils/config'; interface Props { onChange: (alertManagerSourceName: string) => void; current?: string; + disabled?: boolean; } -export const AlertManagerPicker: FC = ({ onChange, current }) => { +export const AlertManagerPicker: FC = ({ onChange, current, disabled = false }) => { const options: Array> = useMemo(() => { return [ { @@ -31,6 +32,7 @@ export const AlertManagerPicker: FC = ({ onChange, current }) => { return ( + + + You can use the{' '} + + Go templating language + + .{' '} + + More info about alertmanager templates + + + } + label="Content" + error={errors?.content?.message} + invalid={!!errors.content?.message} + > +