diff --git a/packages/grafana-alerting/src/grafana/api/v0alpha1/types.ts b/packages/grafana-alerting/src/grafana/api/v0alpha1/types.ts index cc4effa3d3e..acd8045d63e 100644 --- a/packages/grafana-alerting/src/grafana/api/v0alpha1/types.ts +++ b/packages/grafana-alerting/src/grafana/api/v0alpha1/types.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 diff --git a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.tsx b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.tsx index 3107319a9ea..56dacd8dca5 100644 --- a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.tsx +++ b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.tsx @@ -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; * @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) diff --git a/packages/grafana-alerting/src/grafana/contactPoints/hooks/useContactPoints.tsx b/packages/grafana-alerting/src/grafana/contactPoints/hooks/useContactPoints.tsx deleted file mode 100644 index ab0169f2891..00000000000 --- a/packages/grafana-alerting/src/grafana/contactPoints/hooks/useContactPoints.tsx +++ /dev/null @@ -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 ->; - -/** - * 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({}); -} - -export { useListContactPointsv0alpha1 }; diff --git a/packages/grafana-alerting/src/grafana/contactPoints/hooks/v0alpha1/useContactPoints.tsx b/packages/grafana-alerting/src/grafana/contactPoints/hooks/v0alpha1/useContactPoints.tsx new file mode 100644 index 00000000000..25d1ab9a6bf --- /dev/null +++ b/packages/grafana-alerting/src/grafana/contactPoints/hooks/v0alpha1/useContactPoints.tsx @@ -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 +>; + +/** + * 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({}); +} + +// type narrowing mutations requires us to define a few helper types +type CreateContactPointArgs = OverrideProperties< + CreateReceiverApiArg, + { receiver: Omit } +>; + +type CreateContactPointMutation = TypedUseMutationResult< + ContactPoint, + CreateContactPointArgs, + ReturnType +>; + +type UseCreateContactPointOptions = Parameters< + typeof alertingAPI.endpoints.createReceiver.useMutation +>[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(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; +} diff --git a/packages/grafana-alerting/src/unstable.ts b/packages/grafana-alerting/src/unstable.ts index f1e7ce62356..5c10dcbf3eb 100644 --- a/packages/grafana-alerting/src/unstable.ts +++ b/packages/grafana-alerting/src/unstable.ts @@ -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'; diff --git a/packages/grafana-alerting/tests/provider.tsx b/packages/grafana-alerting/tests/provider.tsx index 039015eb8bf..4331f7e6b8a 100644 --- a/packages/grafana-alerting/tests/provider.tsx +++ b/packages/grafana-alerting/tests/provider.tsx @@ -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()); }; }, []); } diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index d28d2f9caba..67078fdbd27 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -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, diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx index a2e88321e4f..c512615ce78 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx @@ -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}`, }); diff --git a/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx b/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx index 14b0a6b87cb..b3c9196e664 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx @@ -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, '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}`, }); diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index c00680f5aae..db0c15c8f7f 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -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) { getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }).concat( listenerMiddleware.middleware, alertingApi.middleware, - alertingAPIv0alpha1.middleware, + alertingPackageAPI.middleware, publicDashboardApi.middleware, browseDashboardsAPI.middleware, cloudMigrationAPI.middleware,