From 69b115320517f2ec3f65224563c388b6edf80e81 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 16 Jun 2023 16:19:44 +0200 Subject: [PATCH] Alerting: Contact Points v2 feature flag (#70165) --- .../features/alerting/unified/Receivers.tsx | 240 ++---------------- .../contact-points/ContactPoints.v1.test.tsx} | 35 +-- .../contact-points/ContactPoints.v1.tsx | 181 +++++++++++++ .../contact-points/ContactPoints.v2.tsx | 5 + .../app/features/alerting/unified/features.ts | 5 + 5 files changed, 230 insertions(+), 236 deletions(-) rename public/app/features/alerting/unified/{Receivers.test.tsx => components/contact-points/ContactPoints.v1.test.tsx} (96%) create mode 100644 public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx create mode 100644 public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 68cebae6705..0b5b6979e16 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -1,222 +1,24 @@ -import { css } from '@emotion/css'; -import pluralize from 'pluralize'; -import React, { useEffect } from 'react'; -import { Redirect, Route, RouteChildrenProps, Switch, useLocation, useParams } from 'react-router-dom'; +import React from 'react'; +import { Disable, Enable } from 'react-enable'; -import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { Stack } from '@grafana/experimental'; -import { Alert, Icon, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui'; -import { useDispatch } from 'app/types'; +import { withErrorBoundary } from '@grafana/ui'; +const ContactPointsV1 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints.v1')); +const ContactPointsV2 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints.v2')); +import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; +import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { ContactPointsState } from '../../../types'; - -import { useGetContactPointsState } from './api/receiversApi'; -import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; -import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; -import { NoAlertManagerWarning } from './components/NoAlertManagerWarning'; -import { DuplicateTemplateView } from './components/receivers/DuplicateTemplateView'; -import { EditReceiverView } from './components/receivers/EditReceiverView'; -import { EditTemplateView } from './components/receivers/EditTemplateView'; -import { GlobalConfigForm } from './components/receivers/GlobalConfigForm'; -import { NewReceiverView } from './components/receivers/NewReceiverView'; -import { NewTemplateView } from './components/receivers/NewTemplateView'; -import { ReceiversAndTemplatesView } from './components/receivers/ReceiversAndTemplatesView'; -import { isDuplicating } from './components/receivers/TemplateForm'; -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'; - -export interface NotificationErrorProps { - errorCount: number; -} - -function NotificationError({ errorCount }: NotificationErrorProps) { - const styles = useStyles2(getStyles); - - return ( -
- - - -
{`${errorCount} ${pluralize('error', errorCount)} with contact points`}
-
-
{'Some alert notifications might not be delivered'}
-
-
- ); -} - -type PageType = 'receivers' | 'templates' | 'global-config'; - -const Receivers = () => { - const alertManagers = useAlertManagersByPermission('notification'); - const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); - const dispatch = useDispatch(); - const styles = useStyles2(getStyles); - - const { id, type } = useParams<{ id?: string; type?: PageType }>(); - const location = useLocation(); - const isRoot = location.pathname.endsWith('/alerting/notifications'); - const isduplicatingTemplate = isDuplicating(location); - 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; - const shouldRenderNotificationStatus = isRoot; - - useEffect(() => { - if (alertManagerSourceName && shouldLoadConfig) { - dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); - } - }, [alertManagerSourceName, dispatch, shouldLoadConfig]); - - useEffect(() => { - if ( - alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && - !(receiverTypes.result || receiverTypes.loading || receiverTypes.error) - ) { - dispatch(fetchGrafanaNotifiersAction()); - } - }, [alertManagerSourceName, dispatch, receiverTypes]); - - const contactPointsState: ContactPointsState = useGetContactPointsState(alertManagerSourceName ?? ''); - const integrationsErrorCount = contactPointsState?.errorCount ?? 0; - - const disableAmSelect = !isRoot; - let pageNav = getPageNavigationModel(type, id ? decodeURIComponent(id) : undefined, isduplicatingTemplate); - - if (!alertManagerSourceName) { - return isRoot ? ( - - - - ) : ( - - ); - } - - return ( - -
- - {shouldRenderNotificationStatus && integrationsErrorCount > 0 && ( - - )} -
- {error && !loading && ( - - {error.message || 'Unknown error.'} - - )} - - {loading && !config && } - {config && !error && ( - - - - - - - - - {({ match }: RouteChildrenProps<{ name: string }>) => - match?.params.name && ( - - ) - } - - - {({ match }: RouteChildrenProps<{ name: string }>) => - match?.params.name && ( - - ) - } - - - - - - {({ match }: RouteChildrenProps<{ name: string }>) => - match?.params.name && ( - - ) - } - - - - - - )} -
- ); -}; - -function getPageNavigationModel(type: PageType | undefined, id: string | undefined, isDuplicatingTemplates: boolean) { - let pageNav: NavModelItem | undefined; - if (isDuplicatingTemplates) { - return { - text: `New template`, - subTitle: `Create a new template for your notifications`, - }; - } - if (type === 'receivers' || type === 'templates') { - const objectText = type === 'receivers' ? 'contact point' : 'notification template'; - if (id) { - pageNav = { - text: id, - subTitle: `Edit the settings for a specific ${objectText}`, - }; - } else { - pageNav = { - text: `New ${objectText}`, - subTitle: `Create a new ${objectText} for your notifications`, - }; - } - } else if (type === 'global-config') { - pageNav = { - text: 'Global config', - subTitle: 'Manage your global configuration', - }; - } - return pageNav; -} - -export default withErrorBoundary(Receivers, { style: 'page' }); - -const getStyles = (theme: GrafanaTheme2) => ({ - error: css` - color: ${theme.colors.error.text}; - `, - headingContainer: css` - display: flex; - justify-content: space-between; - `, -}); +import { AlertingFeature } from './features'; +// TODO add pagenav back in – what are we missing if we don't specify it? +const ContactPoints = (props: GrafanaRouteComponentProps): JSX.Element => ( + + + + + + + + +); + +export default withErrorBoundary(ContactPoints, { style: 'page' }); diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx similarity index 96% rename from public/app/features/alerting/unified/Receivers.test.tsx rename to public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx index b311da63e17..be44912898a 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx @@ -20,30 +20,31 @@ import { AccessControlAction, ContactPointsState } from 'app/types'; import 'whatwg-fetch'; -import Receivers from './Receivers'; -import { fetchAlertManagerConfig, fetchStatus, testReceivers, updateAlertManagerConfig } from './api/alertmanager'; -import { AlertmanagersChoiceResponse } from './api/alertmanagerApi'; -import { discoverAlertmanagerFeatures } from './api/buildInfo'; -import { fetchNotifiers } from './api/grafana'; -import * as receiversApi from './api/receiversApi'; -import * as grafanaApp from './components/receivers/grafanaAppReceivers/grafanaApp'; +import { fetchAlertManagerConfig, fetchStatus, testReceivers, updateAlertManagerConfig } from '../../api/alertmanager'; +import { AlertmanagersChoiceResponse } from '../../api/alertmanagerApi'; +import { discoverAlertmanagerFeatures } from '../../api/buildInfo'; +import { fetchNotifiers } from '../../api/grafana'; +import * as receiversApi from '../../api/receiversApi'; +import * as grafanaApp from '../../components/receivers/grafanaAppReceivers/grafanaApp'; import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus, someGrafanaAlertManagerConfig, -} from './mocks'; -import { mockAlertmanagerChoiceResponse } from './mocks/alertmanagerApi'; -import { grafanaNotifiersMock } from './mocks/grafana-notifiers'; -import { getAllDataSources } from './utils/config'; -import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; -import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; -jest.mock('./api/alertmanager'); -jest.mock('./api/grafana'); -jest.mock('./utils/config'); +} from '../../mocks'; +import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi'; +import { grafanaNotifiersMock } from '../../mocks/grafana-notifiers'; +import { getAllDataSources } from '../../utils/config'; +import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../../utils/constants'; +import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; + +import Receivers from './ContactPoints.v1'; +jest.mock('../../api/alertmanager'); +jest.mock('../../api/grafana'); +jest.mock('../../utils/config'); jest.mock('app/core/services/context_srv'); -jest.mock('./api/buildInfo'); +jest.mock('../../api/buildInfo'); const mocks = { getAllDataSources: jest.mocked(getAllDataSources), diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx new file mode 100644 index 00000000000..fb003777276 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx @@ -0,0 +1,181 @@ +import { css } from '@emotion/css'; +import pluralize from 'pluralize'; +import React, { useEffect } from 'react'; +import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Stack } from '@grafana/experimental'; +import { Alert, Icon, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; +import { ContactPointsState, useDispatch } from 'app/types'; + +import { useGetContactPointsState } from '../../api/receiversApi'; +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'; +import { AlertManagerPicker } from '../AlertManagerPicker'; +import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning'; +import { NoAlertManagerWarning } from '../NoAlertManagerWarning'; +import { DuplicateTemplateView } from '../receivers/DuplicateTemplateView'; +import { EditReceiverView } from '../receivers/EditReceiverView'; +import { EditTemplateView } from '../receivers/EditTemplateView'; +import { GlobalConfigForm } from '../receivers/GlobalConfigForm'; +import { NewReceiverView } from '../receivers/NewReceiverView'; +import { NewTemplateView } from '../receivers/NewTemplateView'; +import { ReceiversAndTemplatesView } from '../receivers/ReceiversAndTemplatesView'; + +export interface NotificationErrorProps { + errorCount: number; +} + +function NotificationError({ errorCount }: NotificationErrorProps) { + const styles = useStyles2(getStyles); + + return ( +
+ + + +
{`${errorCount} ${pluralize('error', errorCount)} with contact points`}
+
+
{'Some alert notifications might not be delivered'}
+
+
+ ); +} + +const Receivers = () => { + const alertManagers = useAlertManagersByPermission('notification'); + const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); + const dispatch = useDispatch(); + const styles = useStyles2(getStyles); + + 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; + const shouldRenderNotificationStatus = isRoot; + + useEffect(() => { + if (alertManagerSourceName && shouldLoadConfig) { + dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); + } + }, [alertManagerSourceName, dispatch, shouldLoadConfig]); + + useEffect(() => { + if ( + alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && + !(receiverTypes.result || receiverTypes.loading || receiverTypes.error) + ) { + dispatch(fetchGrafanaNotifiersAction()); + } + }, [alertManagerSourceName, dispatch, receiverTypes]); + + const contactPointsState: ContactPointsState = useGetContactPointsState(alertManagerSourceName ?? ''); + const integrationsErrorCount = contactPointsState?.errorCount ?? 0; + + const disableAmSelect = !isRoot; + + if (!alertManagerSourceName) { + return isRoot ? ( + + ) : ( + + ); + } + + return ( + <> +
+ + {shouldRenderNotificationStatus && integrationsErrorCount > 0 && ( + + )} +
+ {error && !loading && ( + + {error.message || 'Unknown error.'} + + )} + + {loading && !config && } + {config && !error && ( + + + + + + + + + {({ match }: RouteChildrenProps<{ name: string }>) => + match?.params.name && ( + + ) + } + + + {({ match }: RouteChildrenProps<{ name: string }>) => + match?.params.name && ( + + ) + } + + + + + + {({ match }: RouteChildrenProps<{ name: string }>) => + match?.params.name && ( + + ) + } + + + + + + )} + + ); +}; + +export default Receivers; + +const getStyles = (theme: GrafanaTheme2) => ({ + error: css` + color: ${theme.colors.error.text}; + `, + headingContainer: css` + display: flex; + justify-content: space-between; + `, +}); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx new file mode 100644 index 00000000000..016efeb9330 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +const ContactPoints = () => <>Hello, contact points v2!; + +export default ContactPoints; diff --git a/public/app/features/alerting/unified/features.ts b/public/app/features/alerting/unified/features.ts index 5fb013934da..faa7b8d4d3d 100644 --- a/public/app/features/alerting/unified/features.ts +++ b/public/app/features/alerting/unified/features.ts @@ -4,6 +4,7 @@ import { config } from '@grafana/runtime'; export enum AlertingFeature { NotificationPoliciesV2MatchingInstances = 'notification-policies.v2.matching-instances', + ContactPointsV2 = 'contact-points.v2', } const FEATURES: FeatureDescription[] = [ @@ -11,5 +12,9 @@ const FEATURES: FeatureDescription[] = [ name: AlertingFeature.NotificationPoliciesV2MatchingInstances, defaultValue: config.featureToggles.alertingNotificationsPoliciesMatchingInstances, }, + { + name: AlertingFeature.ContactPointsV2, + defaultValue: false, + }, ]; export default FEATURES;