Alerting: Add oncall contact point type and narrow create hook function (#107711)

This commit is contained in:
Gilles De Mey
2025-07-08 13:35:09 +02:00
committed by GitHub
parent 5d2bbfd3ee
commit 95d4909475
10 changed files with 98 additions and 51 deletions
@@ -51,7 +51,26 @@ type SlackIntegration = OverrideProperties<
}
>;
export type Integration = EmailIntegration | SlackIntegration | GenericIntegration;
// Based on https://github.com/grafana/alerting/blob/main/receivers/oncall/config.go#L14-L27
type OnCallIntegration = OverrideProperties<
GenericIntegration,
{
type: 'OnCall';
settings: {
url: string;
httpMethod?: 'POST' | 'PUT';
maxAlerts?: number;
authorization_scheme?: string;
authorization_credentials?: string;
username?: string;
password?: string;
title?: string;
message?: string;
};
}
>;
export type Integration = EmailIntegration | SlackIntegration | OnCallIntegration | GenericIntegration;
// Enhanced version of ContactPoint with typed integrations
// ⚠️ MergeDeep does not check if the property you are overriding exists in the base type and there is no "DeepOverrideProperties" helper
@@ -3,7 +3,7 @@ import { chain } from 'lodash';
import { Combobox, ComboboxOption } from '@grafana/ui';
import type { ContactPoint } from '../../../api/v0alpha1/types';
import { useListContactPointsv0alpha1 } from '../../hooks/useContactPoints';
import { useListContactPoints } from '../../hooks/v0alpha1/useContactPoints';
import { getContactPointDescription } from '../../utils';
import { CustomComboBoxProps } from './ComboBox.types';
@@ -17,7 +17,7 @@ export type ContactPointSelectorProps = CustomComboBoxProps<ContactPoint>;
* @TODO make ComboBox accept a ReactNode so we can use icons and such
*/
function ContactPointSelector(props: ContactPointSelectorProps) {
const { currentData: contactPoints, isLoading } = useListContactPointsv0alpha1();
const { currentData: contactPoints, isLoading } = useListContactPoints();
// Create a mapping of options with their corresponding contact points
const contactPointOptions = chain(contactPoints?.items)
@@ -1,25 +0,0 @@
import { type TypedUseQueryHookResult, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { type ListReceiverApiArg, alertingAPI } from '../../api/v0alpha1/api.gen';
import type { EnhancedListReceiverApiResponse } from '../../api/v0alpha1/types';
// this is a workaround for the fact that the generated types are not narrow enough
type EnhancedHookResult = TypedUseQueryHookResult<
EnhancedListReceiverApiResponse,
ListReceiverApiArg,
ReturnType<typeof fetchBaseQuery>
>;
/**
* useListContactPoints is a hook that fetches a list of contact points
*
* This function wraps the alertingAPI.useListReceiverQuery with proper typing
* to ensure that the returned ContactPoints are correctly typed in the data.items array.
*
* It automatically uses the configured namespace for the query.
*/
function useListContactPointsv0alpha1() {
return alertingAPI.useListReceiverQuery<EnhancedHookResult>({});
}
export { useListContactPointsv0alpha1 };
@@ -0,0 +1,62 @@
import {
type TypedUseMutationResult,
type TypedUseQueryHookResult,
fetchBaseQuery,
} from '@reduxjs/toolkit/query/react';
import { OverrideProperties } from 'type-fest';
import { CreateReceiverApiArg, type ListReceiverApiArg, alertingAPI } from '../../../api/v0alpha1/api.gen';
import type { ContactPoint, EnhancedListReceiverApiResponse } from '../../../api/v0alpha1/types';
// this is a workaround for the fact that the generated types are not narrow enough
type ListContactPointsHookResult = TypedUseQueryHookResult<
EnhancedListReceiverApiResponse,
ListReceiverApiArg,
ReturnType<typeof fetchBaseQuery>
>;
/**
* useListContactPoints is a hook that fetches a list of contact points
*
* This function wraps the alertingAPI.useListReceiverQuery with proper typing
* to ensure that the returned ContactPoints are correctly typed in the data.items array.
*
* It automatically uses the configured namespace for the query.
*/
export function useListContactPoints() {
return alertingAPI.useListReceiverQuery<ListContactPointsHookResult>({});
}
// type narrowing mutations requires us to define a few helper types
type CreateContactPointArgs = OverrideProperties<
CreateReceiverApiArg,
{ receiver: Omit<ContactPoint, 'status' | 'metadata'> }
>;
type CreateContactPointMutation = TypedUseMutationResult<
ContactPoint,
CreateContactPointArgs,
ReturnType<typeof fetchBaseQuery>
>;
type UseCreateContactPointOptions = Parameters<
typeof alertingAPI.endpoints.createReceiver.useMutation<CreateContactPointMutation>
>[0];
/**
* useCreateContactPoint is a hook that creates a new contact point with one or more integrations
*
* This function wraps the alertingAPI.useCreateReceiverMutation with proper typing
* to ensure that the payload supports type narrowing.
*/
export function useCreateContactPoint(options?: UseCreateContactPointOptions) {
const [updateFn, result] = alertingAPI.endpoints.createReceiver.useMutation<CreateContactPointMutation>(options);
const typedUpdateFn = (args: CreateContactPointArgs) => {
// @ts-expect-error this one is just impossible for me to figure out
const response = updateFn(args);
return response;
};
return [typedUpdateFn, result] as const;
}
+2 -8
View File
@@ -4,14 +4,8 @@
// Contact Points
export * from './grafana/api/v0alpha1/types';
export { useListContactPointsv0alpha1 } from './grafana/contactPoints/hooks/useContactPoints';
export { useListContactPoints } from './grafana/contactPoints/hooks/v0alpha1/useContactPoints';
export { ContactPointSelector } from './grafana/contactPoints/components/ContactPointSelector/ContactPointSelector';
// Low-level API hooks
export { alertingAPI as alertingAPIv0alpha1 } from './grafana/api/v0alpha1/api.gen';
// model factories / mocks
export * as mocksV0alpha1 from './grafana/api/v0alpha1/mocks/fakes/Receivers';
// MSW handlers
export * as handlersV0alpha1 from './grafana/api/v0alpha1/mocks/handlers';
export { alertingAPI } from './grafana/api/v0alpha1/api.gen';
+4 -4
View File
@@ -2,13 +2,13 @@ import { configureStore } from '@reduxjs/toolkit';
import { useEffect } from 'react';
import { Provider } from 'react-redux';
import { alertingAPIv0alpha1 } from '../src/unstable';
import { alertingAPI } from '../src/unstable';
// create an empty store
export const store = configureStore({
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(alertingAPIv0alpha1.middleware),
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(alertingAPI.middleware),
reducer: {
[alertingAPIv0alpha1.reducerPath]: alertingAPIv0alpha1.reducer,
[alertingAPI.reducerPath]: alertingAPI.reducer,
},
});
@@ -35,7 +35,7 @@ export const getDefaultWrapper = () => {
function useResetQueryCacheAfterUnmount() {
useEffect(() => {
return () => {
store.dispatch(alertingAPIv0alpha1.util.resetApiState());
store.dispatch(alertingAPI.util.resetApiState());
};
}, []);
}