Alerting: Add versioning to the API client (#104944)

This commit is contained in:
Gilles De Mey
2025-05-08 16:17:28 +02:00
committed by GitHub
parent 682943ed1a
commit 53b4112abe
15 changed files with 284 additions and 408 deletions

View File

@ -37,7 +37,7 @@
}, },
"scripts": { "scripts": {
"typecheck": "tsc --emitDeclarationOnly false --noEmit", "typecheck": "tsc --emitDeclarationOnly false --noEmit",
"codegen": "yarn run rtk-query-codegen-openapi ./scripts/codegen.ts" "codegen": "rtk-query-codegen-openapi ./scripts/codegen.ts"
}, },
"devDependencies": { "devDependencies": {
"@grafana/tsconfig": "^2.0.0", "@grafana/tsconfig": "^2.0.0",
@ -57,7 +57,7 @@
"react-dom": "^18.0.0" "react-dom": "^18.0.0"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.7.0", "@reduxjs/toolkit": "^2.8.0",
"lodash": "^4.17.21" "lodash": "^4.17.21"
} }
} }

View File

@ -0,0 +1,4 @@
These files are built using the `yarn run codegen` command.
API clients will be written to `src/grafana/api/<version>/api.gen.ts`.
Make sure to create a versioned API client for each API version see `src/grafana/api/v0alpha1/api.ts` as an example.

View File

@ -1,21 +1,47 @@
/** /**
* This script will generate TypeScript type definitions and a RTKQ client for the alerting k8s APIs. * This script will generate TypeScript type definitions and a RTKQ clients for the alerting k8s APIs.
* It downloads the OpenAPI schema from a running Grafana instance and generates the types.
* *
* Run `yarn run codegen` from the "grafana-alerting" package to invoke this script. * Run `yarn run codegen` from the "grafana-alerting" package to invoke this script.
*
* API clients will be placed in "src/grafana/api/<version>/api.gen.ts"
*/ */
import { type ConfigFile } from '@rtk-query/codegen-openapi'; import type { ConfigFile } from '@rtk-query/codegen-openapi';
import { resolve } from 'node:path';
// these snapshots are generated by running "go test pkg/tests/apis/openapi_test.go", see the README in the "openapi_snapshots" directory // append versions here to generate additional API clients
const OPENAPI_SCHEMA_LOCATION = resolve( const VERSIONS = ['v0alpha1'] as const;
'../../../pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json'
); type OutputFile = Omit<ConfigFile, 'outputFile'>;
type OutputFiles = Record<string, OutputFile>;
const outputFiles = VERSIONS.reduce<OutputFiles>((acc, version) => {
// we append the version here so we export versioned API clients from this package without having to re-export with an alias
const exportName = 'alertingAPI';
// these snapshots are generated by running "go test pkg/tests/apis/openapi_test.go" and "scripts/process-specs.ts",
// see the README in the "openapi_snapshots" directory
const schemaFile = `../../../data/openapi/notifications.alerting.grafana.app-${version}.json`;
// make sure there is a API file in each versioned directory
const apiFile = `../src/grafana/api/${version}/api.ts`;
// output each api client into a versioned directory
const outputPath = `../src/grafana/api/${version}/api.gen.ts`;
acc[outputPath] = {
exportName,
schemaFile,
apiFile,
tag: true, // generate tags for cache invalidation
} satisfies OutputFile;
return acc;
}, {});
export default { export default {
exportName: 'alertingAPI', // these are intentionally empty but will be set for each versioned config file
schemaFile: OPENAPI_SCHEMA_LOCATION, exportName: '',
apiFile: '../src/grafana/api.ts', schemaFile: '',
outputFile: resolve('../src/grafana/api.gen.ts'), apiFile: '',
tag: true,
outputFiles,
} satisfies ConfigFile; } satisfies ConfigFile;

View File

@ -1,11 +0,0 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const BASE_URL = '/';
export const api = createApi({
reducerPath: 'grafanaAlertingAPI',
baseQuery: fetchBaseQuery({
baseUrl: BASE_URL,
}),
endpoints: () => ({}),
});

View File

@ -0,0 +1,6 @@
# Versioned Alerting API clients
1. create a new folder for your new API version
2. create a `api.ts` file in the new folder, see existing ones
3. run `yarn codegen` to generate the `api.get.ts` file, which is your new RTKQ client
4. (optional) create a `types.ts` file in the new folder, see existing ones to enhance the types that are auto-generated.

View File

@ -0,0 +1,13 @@
/**
* @TODO move this to some shared package, currently copied from Grafana core (app/api/utils)
*/
import { config } from '@grafana/runtime';
export const getAPINamespace = () => config.namespace;
export const getAPIBaseURL = (group: string, version: string) =>
`/apis/${group}/${version}/namespaces/${getAPINamespace()}` as const;
// By including the version in the reducer path we can prevent cache bugs when different versions of the API are used for the same entities
export const getAPIReducerPath = (group: string, version: string) => `${group}/${version}` as const;

View File

@ -0,0 +1,17 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL, getAPIReducerPath } from '../util';
const VERSION = 'v0alpha1';
const GROUP = 'notifications.alerting.grafana.app';
const baseUrl = getAPIBaseURL(GROUP, VERSION);
const reducerPath = getAPIReducerPath(GROUP, VERSION);
export const api = createApi({
reducerPath,
baseQuery: fetchBaseQuery({
baseUrl,
}),
endpoints: () => ({}),
});

View File

@ -3,14 +3,10 @@
*/ */
import { MergeDeep, MergeExclusive, OverrideProperties } from 'type-fest'; import { MergeDeep, MergeExclusive, OverrideProperties } from 'type-fest';
import { import type { Receiver, Integration as ReceiverIntegration, ListReceiverApiResponse } from './api.gen';
ComGithubGrafanaGrafanaAppsAlertingNotificationsPkgApisReceiverV0Alpha1Receiver as ContactPointV0Alpha1,
ComGithubGrafanaGrafanaAppsAlertingNotificationsPkgApisReceiverV0Alpha1Integration as IntegrationV0Alpha1,
ListReceiverApiResponse,
} from '../api.gen';
type GenericIntegration = OverrideProperties< type GenericIntegration = OverrideProperties<
IntegrationV0Alpha1, ReceiverIntegration,
{ {
settings: Record<string, unknown>; settings: Record<string, unknown>;
} }
@ -60,7 +56,7 @@ export type Integration = EmailIntegration | SlackIntegration | GenericIntegrati
// Enhanced version of ContactPoint with typed integrations // 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 // ⚠️ MergeDeep does not check if the property you are overriding exists in the base type and there is no "DeepOverrideProperties" helper
export type ContactPoint = MergeDeep< export type ContactPoint = MergeDeep<
ContactPointV0Alpha1, Receiver,
{ {
spec: { spec: {
integrations: Integration[]; integrations: Integration[];
@ -68,7 +64,7 @@ export type ContactPoint = MergeDeep<
} }
>; >;
export type EnhancedListReceiverResponse = OverrideProperties< export type EnhancedListReceiverApiResponse = OverrideProperties<
ListReceiverApiResponse, ListReceiverApiResponse,
{ {
items: ContactPoint[]; items: ContactPoint[];

View File

@ -2,8 +2,8 @@ import { chain } from 'lodash';
import { Combobox, ComboboxOption } from '@grafana/ui'; import { Combobox, ComboboxOption } from '@grafana/ui';
import { useListContactPoints } from '../hooks/useContactPoints'; import { ContactPoint } from '../../api/v0alpha1/types';
import { ContactPoint } from '../types'; import { useListContactPointsv0alpha1 } from '../hooks/useContactPoints';
import { getContactPointDescription } from '../utils'; import { getContactPointDescription } from '../utils';
const collator = new Intl.Collator('en', { sensitivity: 'accent' }); const collator = new Intl.Collator('en', { sensitivity: 'accent' });
@ -17,7 +17,7 @@ type ContactPointSelectorProps = {
* @TODO make ComboBox accept a ReactNode so we can use icons and such * @TODO make ComboBox accept a ReactNode so we can use icons and such
*/ */
function ContactPointSelector({ onChange }: ContactPointSelectorProps) { function ContactPointSelector({ onChange }: ContactPointSelectorProps) {
const { currentData: contactPoints, isLoading } = useListContactPoints(); const { currentData: contactPoints, isLoading } = useListContactPointsv0alpha1();
// Create a mapping of options with their corresponding contact points // Create a mapping of options with their corresponding contact points
const contactPointOptions = chain(contactPoints?.items) const contactPointOptions = chain(contactPoints?.items)
@ -27,7 +27,7 @@ function ContactPointSelector({ onChange }: ContactPointSelectorProps) {
label: contactPoint.spec.title, label: contactPoint.spec.title,
value: contactPoint.metadata.uid ?? contactPoint.spec.title, value: contactPoint.metadata.uid ?? contactPoint.spec.title,
description: getContactPointDescription(contactPoint), description: getContactPointDescription(contactPoint),
}, } satisfies ComboboxOption<string>,
contactPoint, contactPoint,
})) }))
.value() .value()

View File

@ -1,15 +1,11 @@
import { fetchBaseQuery, TypedUseQueryHookResult } from '@reduxjs/toolkit/query/react'; import { fetchBaseQuery, TypedUseQueryHookResult } from '@reduxjs/toolkit/query/react';
import { config } from '@grafana/runtime'; import { alertingAPI, type ListReceiverApiArg } from '../../api/v0alpha1/api.gen';
import type { EnhancedListReceiverApiResponse } from '../../api/v0alpha1/types';
import { alertingAPI, ListReceiverApiArg } from '../../api.gen';
import { EnhancedListReceiverResponse } from '../types';
const { namespace } = config;
// this is a workaround for the fact that the generated types are not narrow enough // this is a workaround for the fact that the generated types are not narrow enough
type EnhancedHookResult = TypedUseQueryHookResult< type EnhancedHookResult = TypedUseQueryHookResult<
EnhancedListReceiverResponse, EnhancedListReceiverApiResponse,
ListReceiverApiArg, ListReceiverApiArg,
ReturnType<typeof fetchBaseQuery> ReturnType<typeof fetchBaseQuery>
>; >;
@ -22,8 +18,8 @@ type EnhancedHookResult = TypedUseQueryHookResult<
* *
* It automatically uses the configured namespace for the query. * It automatically uses the configured namespace for the query.
*/ */
function useListContactPoints() { function useListContactPointsv0alpha1() {
return alertingAPI.useListReceiverQuery<EnhancedHookResult>({ namespace }); return alertingAPI.useListReceiverQuery<EnhancedHookResult>({});
} }
export { useListContactPoints }; export { useListContactPointsv0alpha1 };

View File

@ -1,6 +1,6 @@
import { countBy, isEmpty } from 'lodash'; import { countBy, isEmpty } from 'lodash';
import { ContactPoint } from './types'; import { ContactPoint } from '../api/v0alpha1/types';
/** /**
* Generates a human-readable description of a ContactPoint by summarizing its integrations. * Generates a human-readable description of a ContactPoint by summarizing its integrations.

View File

@ -1,6 +1,4 @@
/** /**
* Export things here that you want to be available under @grafana/alerting/internal * Export things here that you want to be available under @grafana/alerting/internal
*/ */
export { alertingAPI } from './grafana/api.gen';
export default {}; export default {};

View File

@ -3,6 +3,9 @@
*/ */
// Contact Points // Contact Points
export * from './grafana/contactPoints/types'; export * from './grafana/api/v0alpha1/types';
export { useListContactPoints } from './grafana/contactPoints/hooks/useContactPoints'; export { useListContactPointsv0alpha1 } from './grafana/contactPoints/hooks/useContactPoints';
export { ContactPointSelector } from './grafana/contactPoints/components/ContactPointSelector'; export { ContactPointSelector } from './grafana/contactPoints/components/ContactPointSelector';
// Low-level API hooks
export { alertingAPI as alertingAPIv0alpha1 } from './grafana/api/v0alpha1/api.gen';

View File

@ -2915,7 +2915,7 @@ __metadata:
resolution: "@grafana/alerting@workspace:packages/grafana-alerting" resolution: "@grafana/alerting@workspace:packages/grafana-alerting"
dependencies: dependencies:
"@grafana/tsconfig": "npm:^2.0.0" "@grafana/tsconfig": "npm:^2.0.0"
"@reduxjs/toolkit": "npm:^2.7.0" "@reduxjs/toolkit": "npm:^2.8.0"
"@rtk-query/codegen-openapi": "npm:^2.0.0" "@rtk-query/codegen-openapi": "npm:^2.0.0"
"@types/lodash": "npm:^4" "@types/lodash": "npm:^4"
"@types/react": "npm:18.3.18" "@types/react": "npm:18.3.18"
@ -6517,9 +6517,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@reduxjs/toolkit@npm:^2.7.0": "@reduxjs/toolkit@npm:^2.8.0":
version: 2.7.0 version: 2.8.0
resolution: "@reduxjs/toolkit@npm:2.7.0" resolution: "@reduxjs/toolkit@npm:2.8.0"
dependencies: dependencies:
"@standard-schema/spec": "npm:^1.0.0" "@standard-schema/spec": "npm:^1.0.0"
"@standard-schema/utils": "npm:^0.3.0" "@standard-schema/utils": "npm:^0.3.0"
@ -6535,7 +6535,7 @@ __metadata:
optional: true optional: true
react-redux: react-redux:
optional: true optional: true
checksum: 10/cc264efc95f9ebeafa469bf1040d106a33768a802e6f46aa678bf9f26822d049c18b5f10864aa8badb2e62febe58e242860256174528e62b09e8f897d32cd182 checksum: 10/22a97393e6d8688edacea748efeff2e5c8165c61aa05239192cca8856dbbf175c49e8dd9fcf954e0c09014acaefcf56dcd61303b905e4e0eb47e77ad09f230d8
languageName: node languageName: node
linkType: hard linkType: hard