diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx
index d4a1b73113b..06ff3cd0ae6 100644
--- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx
@@ -42,7 +42,12 @@ const ContactPointsTab = () => {
const { selectedAlertmanager } = useAlertmanager();
const [queryParams] = useURLSearchParams();
- const { isLoading, error, contactPoints } = useContactPointsWithStatus();
+ const { isLoading, error, contactPoints } = useContactPointsWithStatus({
+ alertmanager: selectedAlertmanager!,
+ fetchPolicies: true,
+ fetchStatuses: true,
+ });
+
const { deleteTrigger, updateAlertmanagerState } = useDeleteContactPoint(selectedAlertmanager!);
const [addContactPointSupported, addContactPointAllowed] = useAlertmanagerAbility(
AlertmanagerAction.CreateContactPoint
@@ -160,7 +165,9 @@ const ContactPointsPageContents = () => {
const { selectedAlertmanager } = useAlertmanager();
const [activeTab, setActiveTab] = useTabQueryParam();
- const { contactPoints } = useContactPointsWithStatus();
+ const { contactPoints } = useContactPointsWithStatus({
+ alertmanager: selectedAlertmanager!,
+ });
const showingContactPoints = activeTab === ActiveTab.ContactPoints;
const showNotificationTemplates = activeTab === ActiveTab.NotificationTemplates;
diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx
index 72f53409c25..e56c7fd44fb 100644
--- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx
@@ -2,66 +2,79 @@ import { renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { getWrapper } from 'test/test-utils';
+import { config } from '@grafana/runtime';
import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure';
import { setOnCallIntegrations } from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
+import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { AccessControlAction } from 'app/types';
import { setupMswServer } from '../../mockApi';
import { grantUserPermissions } from '../../mocks';
-import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { useContactPointsWithStatus } from './useContactPoints';
const wrapper = ({ children }: { children: ReactNode }) => {
const ProviderWrapper = getWrapper({ renderWithRouter: true });
- return (
-
-
- {children}
-
-
- );
+ return {children};
};
setupMswServer();
+const getHookResponse = async (featureToggleEnabled: boolean) => {
+ config.featureToggles.alertingApiServer = featureToggleEnabled;
+ const { result } = renderHook(
+ () =>
+ useContactPointsWithStatus({
+ alertmanager: GRAFANA_RULES_SOURCE_NAME,
+ fetchPolicies: true,
+ fetchStatuses: true,
+ }),
+ {
+ wrapper,
+ }
+ );
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ // Only return some properties, as we don't want to compare all
+ // RTK query properties in snapshots/comparison between k8s and non-k8s implementations
+ // (would include properties like requestId, fulfilled, etc.)
+ const { contactPoints, error, isLoading } = result.current;
+
+ return { contactPoints, error, isLoading };
+};
+
describe('useContactPoints', () => {
- beforeAll(() => {
+ beforeEach(() => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
+ setOnCallIntegrations([
+ {
+ display_name: 'grafana-integration',
+ value: 'ABC123',
+ integration_url: 'https://oncall-endpoint.example.com',
+ },
+ ]);
});
it('should return contact points with status', async () => {
disablePlugin(SupportedPlugin.OnCall);
+ const snapshot = await getHookResponse(false);
+ expect(snapshot).toMatchSnapshot();
+ });
- const { result } = renderHook(() => useContactPointsWithStatus(), {
- wrapper,
- });
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
- expect(result.current).toMatchSnapshot();
+ it('returns matching responses with and without alertingApiServer', async () => {
+ const snapshotAmConfig = await getHookResponse(false);
+ const snapshotAlertingApiServer = await getHookResponse(true);
+ expect(snapshotAmConfig).toEqual(snapshotAlertingApiServer);
});
describe('when having oncall plugin installed and no alert manager config data', () => {
it('should return contact points with oncall metadata', async () => {
- setOnCallIntegrations([
- {
- display_name: 'grafana-integration',
- value: 'ABC123',
- integration_url: 'https://oncall-endpoint.example.com',
- },
- ]);
-
- const { result } = renderHook(() => useContactPointsWithStatus(), {
- wrapper,
- });
-
- await waitFor(() => {
- expect(result.current.isLoading).toBe(false);
- });
- expect(result.current).toMatchSnapshot();
+ const snapshot = await getHookResponse(false);
+ expect(snapshot).toMatchSnapshot();
});
});
});
diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx
index c661ead47f0..1f0e3ed94c6 100644
--- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx
@@ -11,6 +11,7 @@ import {
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver,
generatedReceiversApi,
} from 'app/features/alerting/unified/openapi/receiversApi.gen';
+import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { getNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
@@ -18,7 +19,6 @@ import { getNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/uti
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { onCallApi } from '../../api/onCallApi';
import { usePluginBridge } from '../../hooks/usePluginBridge';
-import { useAlertmanager } from '../../state/AlertmanagerContext';
import { SupportedPlugin } from '../../types/pluginBridges';
import { enhanceContactPointsWithMetadata } from './utils';
@@ -44,12 +44,17 @@ const {
const { useGrafanaOnCallIntegrationsQuery } = onCallApi;
const { useListNamespacedReceiverQuery } = generatedReceiversApi;
+const defaultOptions = {
+ refetchOnFocus: true,
+ refetchOnReconnect: true,
+};
+
/**
* Check if OnCall is installed, and fetch the list of integrations if so.
*
* Otherwise, returns no data
*/
-const useOnCallIntegrations = ({ skip }: { skip?: boolean } = {}) => {
+const useOnCallIntegrations = ({ skip }: Skippable = {}) => {
const { installed, loading } = usePluginBridge(SupportedPlugin.OnCall);
const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery(undefined, { skip: skip || !installed });
@@ -84,24 +89,53 @@ const useK8sContactPoints = (...[hookParams, queryOptions]: Parameters {
+/**
+ * Fetch contact points for Grafana Alertmanager, either from the k8s API,
+ * or the `/notifications/receivers` endpoint
+ */
+const useFetchGrafanaContactPoints = ({ skip }: Skippable = {}) => {
const namespace = getNamespace();
const useK8sApi = shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME);
- const grafanaResponse = useGetContactPointsListQuery(undefined, { skip: useK8sApi });
- const k8sResponse = useK8sContactPoints({ namespace }, { skip: !useK8sApi });
+
+ const grafanaResponse = useGetContactPointsListQuery(undefined, { skip: skip || useK8sApi });
+ const k8sResponse = useK8sContactPoints({ namespace }, { skip: skip || !useK8sApi });
return useK8sApi ? k8sResponse : grafanaResponse;
};
+type GrafanaFetchOptions = {
+ /**
+ * Should we fetch and include status information about each contact point?
+ */
+ fetchStatuses?: boolean;
+ /**
+ * Should we fetch and include the number of notification policies that reference each contact point?
+ */
+ fetchPolicies?: boolean;
+};
+
/**
* Fetch contact points from separate endpoint (i.e. not the Alertmanager config) and combine with
* OnCall integrations and any additional metadata from list of notifiers
* (e.g. hydrate with additional names/descriptions)
*/
-export const useGetContactPoints = () => {
- const onCallResponse = useOnCallIntegrations();
- const alertNotifiers = useGrafanaNotifiersQuery();
- const contactPointsListResponse = useGetGrafanaContactPoints();
+export const useGrafanaContactPoints = ({
+ fetchStatuses,
+ fetchPolicies,
+ skip,
+}: GrafanaFetchOptions & Skippable = {}) => {
+ const potentiallySkip = { skip };
+ const onCallResponse = useOnCallIntegrations(potentiallySkip);
+ const alertNotifiers = useGrafanaNotifiersQuery(undefined, potentiallySkip);
+ const contactPointsListResponse = useFetchGrafanaContactPoints(potentiallySkip);
+ const contactPointsStatusResponse = useGetContactPointsStatusQuery(undefined, {
+ ...defaultOptions,
+ pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL,
+ skip: skip || !fetchStatuses,
+ });
+ const alertmanagerConfigResponse = useGetAlertmanagerConfigurationQuery(GRAFANA_RULES_SOURCE_NAME, {
+ skip: skip || !fetchPolicies,
+ });
return useMemo(() => {
const isLoading = onCallResponse.isLoading || alertNotifiers.isLoading || contactPointsListResponse.isLoading;
@@ -118,83 +152,55 @@ export const useGetContactPoints = () => {
};
}
- const enhanced = enhanceContactPointsWithMetadata(
- [],
- alertNotifiers.data,
- onCallResponse?.data,
- contactPointsListResponse.data,
- undefined
- );
+ const enhanced = enhanceContactPointsWithMetadata({
+ status: contactPointsStatusResponse.data,
+ notifiers: alertNotifiers.data,
+ onCallIntegrations: onCallResponse?.data,
+ contactPoints: contactPointsListResponse.data,
+ alertmanagerConfiguration: alertmanagerConfigResponse.data,
+ });
return {
...contactPointsListResponse,
contactPoints: enhanced,
};
}, [
- alertNotifiers.data,
- alertNotifiers.isLoading,
+ alertNotifiers,
+ alertmanagerConfigResponse,
contactPointsListResponse,
- onCallResponse?.data,
- onCallResponse.isLoading,
+ contactPointsStatusResponse,
+ onCallResponse,
]);
};
-export function useContactPointsWithStatus() {
- const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager();
-
- const defaultOptions = {
- refetchOnFocus: true,
- refetchOnReconnect: true,
- };
-
- // fetch receiver status if we're dealing with a Grafana Managed Alertmanager
- const fetchContactPointsStatus = useGetContactPointsStatusQuery(undefined, {
- ...defaultOptions,
- // re-fetch status every so often for up-to-date information
- pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL,
- // skip fetching receiver statuses if not Grafana AM
+export function useContactPointsWithStatus({
+ alertmanager,
+ fetchStatuses,
+ fetchPolicies,
+}: GrafanaFetchOptions & BaseAlertmanagerArgs) {
+ const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
+ const grafanaResponse = useGrafanaContactPoints({
skip: !isGrafanaAlertmanager,
+ fetchStatuses,
+ fetchPolicies,
});
- // fetch notifier metadata from the Grafana API if we're using a Grafana AM – this will be used to add additional
- // metadata and canonical names to the receiver
- const fetchReceiverMetadata = useGrafanaNotifiersQuery(undefined, {
- skip: !isGrafanaAlertmanager,
- });
-
- // if the OnCall plugin is installed, fetch its list of integrations so we can match those to the Grafana Managed contact points
- const { data: onCallMetadata, isLoading: onCallPluginIntegrationsLoading } = useOnCallIntegrations({
- skip: !isGrafanaAlertmanager,
- });
-
- // fetch the latest config from the Alertmanager
- // we use this endpoint only when we need to get the number of policies
- const fetchAlertmanagerConfiguration = useGetAlertmanagerConfigurationQuery(selectedAlertmanager!, {
+ const alertmanagerConfigResponse = useGetAlertmanagerConfigurationQuery(alertmanager, {
...defaultOptions,
selectFromResult: (result) => ({
...result,
contactPoints: result.data
- ? enhanceContactPointsWithMetadata(
- fetchContactPointsStatus.data,
- isGrafanaAlertmanager ? fetchReceiverMetadata.data : cloudNotifierTypes,
- onCallMetadata,
- result.data.alertmanager_config.receivers ?? [],
- result.data
- )
+ ? enhanceContactPointsWithMetadata({
+ notifiers: cloudNotifierTypes,
+ contactPoints: result.data.alertmanager_config.receivers ?? [],
+ alertmanagerConfiguration: result.data,
+ })
: [],
}),
+ skip: isGrafanaAlertmanager,
});
- // we will fail silently for fetching OnCall plugin status and integrations
- const error = fetchAlertmanagerConfiguration.error || fetchContactPointsStatus.error;
- const isLoading =
- fetchAlertmanagerConfiguration.isLoading || fetchContactPointsStatus.isLoading || onCallPluginIntegrationsLoading;
-
- return {
- error,
- isLoading,
- contactPoints: fetchAlertmanagerConfiguration.contactPoints,
- };
+ return isGrafanaAlertmanager ? grafanaResponse : alertmanagerConfigResponse;
}
export function useDeleteContactPoint(selectedAlertmanager: string) {
diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts
index 1fe8c162a20..566e1f34763 100644
--- a/public/app/features/alerting/unified/components/contact-points/utils.ts
+++ b/public/app/features/alerting/unified/components/contact-points/utils.ts
@@ -102,6 +102,14 @@ export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
grafana_managed_receiver_configs: ReceiverConfigWithMetadata[];
}
+type EnhanceContactPointsArgs = {
+ status?: ReceiversStateDTO[];
+ notifiers?: NotifierDTO[];
+ onCallIntegrations?: OnCallIntegrationDTO[] | undefined | null;
+ contactPoints: Receiver[];
+ alertmanagerConfiguration?: AlertManagerCortexConfig;
+};
+
/**
* This function adds the status information for each of the integrations (contact point types) in a contact point
* 1. we iterate over all contact points
@@ -110,13 +118,13 @@ export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
* alertmanagerConfiguration: optional as is passed when we need to get number of policies for each contact point
* and we prefer using the data from the read-only endpoint.
*/
-export function enhanceContactPointsWithMetadata(
- status: ReceiversStateDTO[] = [],
- notifiers: NotifierDTO[] = [],
- onCallIntegrations: OnCallIntegrationDTO[] | undefined | null,
- contactPoints: Receiver[],
- alertmanagerConfiguration?: AlertManagerCortexConfig
-): ContactPointWithMetadata[] {
+export function enhanceContactPointsWithMetadata({
+ status = [],
+ notifiers = [],
+ onCallIntegrations,
+ contactPoints,
+ alertmanagerConfiguration,
+}: EnhanceContactPointsArgs): ContactPointWithMetadata[] {
// compute the entire inherited tree before finding what notification policies are using a particular contact point
const fullyInheritedTree = computeInheritedTree(alertmanagerConfiguration?.alertmanager_config?.route ?? {});
const usedContactPoints = getUsedContactPoints(fullyInheritedTree);
diff --git a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx
index 9b6a67db99e..c5ef82d8ed8 100644
--- a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx
+++ b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx
@@ -13,6 +13,7 @@ import {
ReadNamespacedTimeIntervalApiResponse,
} from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
import { deleteMuteTimingAction, updateAlertManagerConfigAction } from 'app/features/alerting/unified/state/actions';
+import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks';
import { renameMuteTimings } from 'app/features/alerting/unified/utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
@@ -27,15 +28,6 @@ const {
useDeleteNamespacedTimeIntervalMutation,
} = timeIntervalsApi;
-type BaseAlertmanagerArgs = {
- /**
- * Name of alertmanager being used for mute timings management.
- *
- * Hooks will behave differently depending on whether this is `grafana` or an external alertmanager
- */
- alertmanager: string;
-};
-
/**
* Alertmanager mute time interval, with optional additional metadata
* (returned in the case of K8S API implementation)
diff --git a/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx
index 9985715abc3..29d496d1eec 100644
--- a/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx
+++ b/public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx
@@ -1,12 +1,14 @@
import { SelectableValue } from '@grafana/data';
import { Select, SelectCommonProps, Text, Stack } from '@grafana/ui';
+import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY } from '../contact-points/constants';
import { useContactPointsWithStatus } from '../contact-points/useContactPoints';
import { ReceiverConfigWithMetadata } from '../contact-points/utils';
export const ContactPointSelector = (props: SelectCommonProps) => {
- const { contactPoints, isLoading, error } = useContactPointsWithStatus();
+ const { selectedAlertmanager } = useAlertmanager();
+ const { contactPoints, isLoading, error } = useContactPointsWithStatus({ alertmanager: selectedAlertmanager! });
// TODO error handling
if (error) {
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx
index 25921e40e4a..cbfebc00157 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx
@@ -8,7 +8,7 @@ import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource';
import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoint';
-import { useGetContactPoints } from '../../../contact-points/useContactPoints';
+import { useGrafanaContactPoints } from '../../../contact-points/useContactPoints';
import { ContactPointWithMetadata } from '../../../contact-points/utils';
import { ContactPointDetails } from './contactPoint/ContactPointDetails';
@@ -29,7 +29,7 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo
error: errorInContactPointStatus,
contactPoints,
refetch: refetchReceivers,
- } = useGetContactPoints();
+ } = useGrafanaContactPoints();
const [selectedContactPointWithMetadata, setSelectedContactPointWithMetadata] = useState<
ContactPointWithMetadata | undefined
diff --git a/public/app/features/alerting/unified/types/hooks.ts b/public/app/features/alerting/unified/types/hooks.ts
new file mode 100644
index 00000000000..8cd35251313
--- /dev/null
+++ b/public/app/features/alerting/unified/types/hooks.ts
@@ -0,0 +1,16 @@
+export type BaseAlertmanagerArgs = {
+ /**
+ * Name of alertmanager to use for config entity management
+ *
+ * Hooks will behave differently depending on whether this is `grafana` or an external alertmanager
+ */
+ alertmanager: string;
+};
+
+export type Skippable = {
+ /**
+ * Should we skip requests altogether?
+ * Useful for cases where we want to conditionally call hook methods
+ */
+ skip?: boolean;
+};