Alerting: Contact Points v2 feature flag (#70165)

pull/70023/head
Gilles De Mey 2 years ago committed by GitHub
parent 35d455e931
commit 69b1153205
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 240
      public/app/features/alerting/unified/Receivers.tsx
  2. 35
      public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx
  3. 181
      public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx
  4. 5
      public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx
  5. 5
      public/app/features/alerting/unified/features.ts

@ -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 (
<div className={styles.error} data-testid="receivers-notification-error">
<Stack alignItems="flex-end" direction="column" gap={0}>
<Stack alignItems="center" gap={1}>
<Icon name="exclamation-circle" />
<div>{`${errorCount} ${pluralize('error', errorCount)} with contact points`}</div>
</Stack>
<div>{'Some alert notifications might not be delivered'}</div>
</Stack>
</div>
);
}
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 ? (
<AlertingPageWrapper pageId="receivers" pageNav={pageNav}>
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
</AlertingPageWrapper>
) : (
<Redirect to="/alerting/notifications" />
);
}
return (
<AlertingPageWrapper pageId="receivers" pageNav={pageNav}>
<div className={styles.headingContainer}>
<AlertManagerPicker
current={alertManagerSourceName}
disabled={disableAmSelect}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
{shouldRenderNotificationStatus && integrationsErrorCount > 0 && (
<NotificationError errorCount={integrationsErrorCount} />
)}
</div>
{error && !loading && (
<Alert severity="error" title="Error loading Alertmanager config">
{error.message || 'Unknown error.'}
</Alert>
)}
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
{loading && !config && <LoadingPlaceholder text="loading configuration..." />}
{config && !error && (
<Switch>
<Route exact={true} path="/alerting/notifications">
<ReceiversAndTemplatesView config={config} alertManagerName={alertManagerSourceName} />
</Route>
<Route exact={true} path="/alerting/notifications/templates/new">
<NewTemplateView config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
<Route exact={true} path="/alerting/notifications/templates/:name/duplicate">
{({ match }: RouteChildrenProps<{ name: string }>) =>
match?.params.name && (
<DuplicateTemplateView
alertManagerSourceName={alertManagerSourceName}
config={config}
templateName={decodeURIComponent(match?.params.name)}
/>
)
}
</Route>
<Route exact={true} path="/alerting/notifications/templates/:name/edit">
{({ match }: RouteChildrenProps<{ name: string }>) =>
match?.params.name && (
<EditTemplateView
alertManagerSourceName={alertManagerSourceName}
config={config}
templateName={decodeURIComponent(match?.params.name)}
/>
)
}
</Route>
<Route exact={true} path="/alerting/notifications/receivers/new">
<NewReceiverView config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
<Route exact={true} path="/alerting/notifications/receivers/:name/edit">
{({ match }: RouteChildrenProps<{ name: string }>) =>
match?.params.name && (
<EditReceiverView
alertManagerSourceName={alertManagerSourceName}
config={config}
receiverName={decodeURIComponent(match?.params.name)}
/>
)
}
</Route>
<Route exact={true} path="/alerting/notifications/global-config">
<GlobalConfigForm config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
</Switch>
)}
</AlertingPageWrapper>
);
};
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 => (
<AlertingPageWrapper pageId="receivers">
<Enable feature={AlertingFeature.ContactPointsV2}>
<ContactPointsV2 {...props} />
</Enable>
<Disable feature={AlertingFeature.ContactPointsV2}>
<ContactPointsV1 {...props} />
</Disable>
</AlertingPageWrapper>
);
export default withErrorBoundary(ContactPoints, { style: 'page' });

@ -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),

@ -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 (
<div className={styles.error} data-testid="receivers-notification-error">
<Stack alignItems="flex-end" direction="column" gap={0}>
<Stack alignItems="center" gap={1}>
<Icon name="exclamation-circle" />
<div>{`${errorCount} ${pluralize('error', errorCount)} with contact points`}</div>
</Stack>
<div>{'Some alert notifications might not be delivered'}</div>
</Stack>
</div>
);
}
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 ? (
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
) : (
<Redirect to="/alerting/notifications" />
);
}
return (
<>
<div className={styles.headingContainer}>
<AlertManagerPicker
current={alertManagerSourceName}
disabled={disableAmSelect}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
{shouldRenderNotificationStatus && integrationsErrorCount > 0 && (
<NotificationError errorCount={integrationsErrorCount} />
)}
</div>
{error && !loading && (
<Alert severity="error" title="Error loading Alertmanager config">
{error.message || 'Unknown error.'}
</Alert>
)}
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
{loading && !config && <LoadingPlaceholder text="loading configuration..." />}
{config && !error && (
<Switch>
<Route exact={true} path="/alerting/notifications">
<ReceiversAndTemplatesView config={config} alertManagerName={alertManagerSourceName} />
</Route>
<Route exact={true} path="/alerting/notifications/templates/new">
<NewTemplateView config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
<Route exact={true} path="/alerting/notifications/templates/:name/duplicate">
{({ match }: RouteChildrenProps<{ name: string }>) =>
match?.params.name && (
<DuplicateTemplateView
alertManagerSourceName={alertManagerSourceName}
config={config}
templateName={decodeURIComponent(match?.params.name)}
/>
)
}
</Route>
<Route exact={true} path="/alerting/notifications/templates/:name/edit">
{({ match }: RouteChildrenProps<{ name: string }>) =>
match?.params.name && (
<EditTemplateView
alertManagerSourceName={alertManagerSourceName}
config={config}
templateName={decodeURIComponent(match?.params.name)}
/>
)
}
</Route>
<Route exact={true} path="/alerting/notifications/receivers/new">
<NewReceiverView config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
<Route exact={true} path="/alerting/notifications/receivers/:name/edit">
{({ match }: RouteChildrenProps<{ name: string }>) =>
match?.params.name && (
<EditReceiverView
alertManagerSourceName={alertManagerSourceName}
config={config}
receiverName={decodeURIComponent(match?.params.name)}
/>
)
}
</Route>
<Route exact={true} path="/alerting/notifications/global-config">
<GlobalConfigForm config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
</Switch>
)}
</>
);
};
export default Receivers;
const getStyles = (theme: GrafanaTheme2) => ({
error: css`
color: ${theme.colors.error.text};
`,
headingContainer: css`
display: flex;
justify-content: space-between;
`,
});

@ -0,0 +1,5 @@
import React from 'react';
const ContactPoints = () => <>Hello, contact points v2!</>;
export default ContactPoints;

@ -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;

Loading…
Cancel
Save