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

pull/107769/head
Gilles De Mey 2 weeks ago committed by GitHub
parent 5d2bbfd3ee
commit 95d4909475
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 21
      packages/grafana-alerting/src/grafana/api/v0alpha1/types.ts
  2. 4
      packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.tsx
  3. 25
      packages/grafana-alerting/src/grafana/contactPoints/hooks/useContactPoints.tsx
  4. 62
      packages/grafana-alerting/src/grafana/contactPoints/hooks/v0alpha1/useContactPoints.tsx
  5. 10
      packages/grafana-alerting/src/unstable.ts
  6. 8
      packages/grafana-alerting/tests/provider.tsx
  7. 4
      public/app/core/reducers/root.ts
  8. 7
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx
  9. 4
      public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx
  10. 4
      public/app/store/configureStore.ts

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

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

@ -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());
};
}, []);
}

@ -1,7 +1,7 @@
import { ReducersMapObject } from '@reduxjs/toolkit';
import { AnyAction, combineReducers } from 'redux';
import { alertingAPIv0alpha1 } from '@grafana/alerting/unstable';
import { alertingAPI as alertingPackageAPI } from '@grafana/alerting/unstable';
import sharedReducers from 'app/core/reducers';
import ldapReducers from 'app/features/admin/state/reducers';
import alertingReducers from 'app/features/alerting/state/reducers';
@ -61,7 +61,7 @@ const rootReducers = {
...authConfigReducers,
plugins: pluginsReducer,
[alertingApi.reducerPath]: alertingApi.reducer,
[alertingAPIv0alpha1.reducerPath]: alertingAPIv0alpha1.reducer,
[alertingPackageAPI.reducerPath]: alertingPackageAPI.reducer,
[publicDashboardApi.reducerPath]: publicDashboardApi.reducer,
[browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer,
[cloudMigrationAPI.reducerPath]: cloudMigrationAPI.reducer,

@ -3,10 +3,7 @@ import { isEmpty } from 'lodash';
import { useEffect } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import {
ContactPointSelector as GrafanaManagedContactPointSelector,
alertingAPIv0alpha1,
} from '@grafana/alerting/unstable';
import { ContactPointSelector as GrafanaManagedContactPointSelector, alertingAPI } from '@grafana/alerting/unstable';
import { Trans, t } from '@grafana/i18n';
import { Field, FieldValidationMessage, Stack, TextLink } from '@grafana/ui';
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
@ -24,7 +21,7 @@ export function ContactPointSelector({ alertManager }: ContactPointSelectorProps
// check if the contact point still exists, we'll use listReceiver to check if the contact point exists because getReceiver doesn't work with
// contact point titles but with UUIDs (which is not what we store on the alert rule definition)
const { currentData, status } = alertingAPIv0alpha1.endpoints.listReceiver.useQuery({
const { currentData, status } = alertingAPI.endpoints.listReceiver.useQuery({
fieldSelector: `spec.title=${contactPointInForm}`,
});

@ -1,7 +1,7 @@
import { ComponentProps } from 'react';
import Skeleton from 'react-loading-skeleton';
import { alertingAPIv0alpha1 } from '@grafana/alerting/unstable';
import { alertingAPI } from '@grafana/alerting/unstable';
import { TextLink } from '@grafana/ui';
import { makeEditContactPointLink } from '../../utils/misc';
@ -12,7 +12,7 @@ interface ContactPointLinkProps extends Omit<ComponentProps<typeof TextLink>, 'h
export const ContactPointLink = ({ name, ...props }: ContactPointLinkProps) => {
// find receiver by name – since this is what we store in the alert rule definition
const { currentData, isLoading, isSuccess } = alertingAPIv0alpha1.endpoints.listReceiver.useQuery({
const { currentData, isLoading, isSuccess } = alertingAPI.endpoints.listReceiver.useQuery({
fieldSelector: `spec.title=${name}`,
});

@ -2,7 +2,7 @@ import { configureStore as reduxConfigureStore, createListenerMiddleware } from
import { setupListeners } from '@reduxjs/toolkit/query';
import { Middleware } from 'redux';
import { alertingAPIv0alpha1 } from '@grafana/alerting/unstable';
import { alertingAPI as alertingPackageAPI } from '@grafana/alerting/unstable';
import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi';
import { cloudMigrationAPI } from 'app/features/migrate-to-cloud/api';
@ -43,7 +43,7 @@ export function configureStore(initialState?: Partial<StoreState>) {
getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }).concat(
listenerMiddleware.middleware,
alertingApi.middleware,
alertingAPIv0alpha1.middleware,
alertingPackageAPI.middleware,
publicDashboardApi.middleware,
browseDashboardsAPI.middleware,
cloudMigrationAPI.middleware,

Loading…
Cancel
Save