API clients: Extract into a package (#111810)

Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Tom Ratcliffe
2025-10-23 13:57:51 +00:00
committed by GitHub
co-authored by Alex Khomenko
parent 91b7ff2ece
commit 811ee99dac
103 changed files with 1335 additions and 727 deletions
+3 -4
View File
@@ -741,6 +741,9 @@
# @grafana/test-utils
/packages/grafana-test-utils @grafana/grafana-frontend-platform
# @grafana/api-clients
/packages/grafana-api-clients/ @grafana/grafana-frontend-platform @grafana/grafana-search-navigate-organise
# root files, mostly frontend
/.browserslistrc @grafana/frontend-ops
/package.json @grafana/frontend-ops
@@ -959,7 +962,6 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform
/public/app/features/transformers/timeSeriesTable/ @grafana/dataviz-squad @grafana/app-o11y-visualizations
/public/app/features/users/ @grafana/access-squad
/public/app/features/variables/ @grafana/dashboards-squad
/public/app/features/preferences/ @grafana/grafana-frontend-platform
/public/app/features/bookmarks/ @grafana/grafana-search-navigate-organise
/public/app/plugins/panel/* @grafana/dataviz-squad
/public/app/plugins/panel/alertlist/ @grafana/alerting-frontend
@@ -1055,13 +1057,10 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform
/scripts/trigger_windows_build.sh @grafana/grafana-developer-enablement-squad
/scripts/cleanup-husky.sh @grafana/frontend-ops
/scripts/verify-repo-update/ @grafana/grafana-developer-enablement-squad
/scripts/generate-rtk-apis.ts @grafana/grafana-frontend-platform
/scripts/process-specs.ts @grafana/grafana-frontend-platform
/scripts/generate-alerting-rtk-apis.ts @grafana/alerting-frontend
/scripts/levitate-parse-json-report.js @grafana/plugins-platform-frontend
/scripts/levitate-show-affected-plugins.js @grafana/plugins-platform-frontend
/scripts/codemods/explicit-barrel-imports.cjs @grafana/frontend-ops
/scripts/rtk-client-generator/ @grafana/grafana-search-navigate-organise
/scripts/codeowners-manifest/ @grafana/dataviz-squad
/scripts/test-coverage-by-codeowner.js @grafana/dataviz-squad
-5
View File
@@ -3248,11 +3248,6 @@
"count": 4
}
},
"public/app/features/migrate-to-cloud/api/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 1
}
},
"public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/CreateTokenModal.tsx": {
"no-restricted-syntax": {
"count": 1
+3 -5
View File
@@ -75,9 +75,8 @@
"plugin:build:dev": "nx run-many -t dev --projects='tag:scope:plugin' --maxParallel=100",
"plugin:test:ci": "nx run-many -t test:ci --projects='tag:scope:plugin' --maxParallel=2",
"plugin:i18n-extract": "nx run-many -t i18n-extract --projects='tag:scope:plugin'",
"process-specs": "node --experimental-strip-types scripts/process-specs.ts",
"generate-apis": "yarn process-specs && rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts",
"generate:api-client": "NODE_OPTIONS='--experimental-strip-types --disable-warning=ExperimentalWarning' plop --plopfile scripts/rtk-client-generator/plopfile.ts"
"generate-apis": "yarn workspace @grafana/api-clients generate-apis",
"generate:api-client": "yarn workspace @grafana/api-clients generate:api-client"
},
"grafana": {
"whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v%[1]s-%[2]s/",
@@ -231,11 +230,9 @@
"node-notifier": "10.0.1",
"nx": "21.3.11",
"open": "^10.2.0",
"openapi-types": "^12.1.3",
"ora": "^9.0.0",
"pa11y-ci": "^4.0.0",
"pdf-parse": "^1.1.1",
"plop": "^4.0.1",
"postcss": "8.5.6",
"postcss-loader": "8.1.1",
"postcss-reporter": "7.1.0",
@@ -280,6 +277,7 @@
"@formatjs/intl-durationformat": "^0.7.0",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/alerting": "workspace:*",
"@grafana/api-clients": "workspace:*",
"@grafana/assistant": "0.1.0",
"@grafana/aws-sdk": "0.7.1",
"@grafana/azure-sdk": "0.0.7",
+1 -1
View File
@@ -20,7 +20,7 @@ All packages are versioned according to the current Grafana version:
- Grafana v6.2.5 -> @grafana/\* packages @ 6.2.5
- Grafana - main branch version (based on package.json, i.e. 6.4.0-pre) -> @grafana/\* packages @ 6.4.0-pre-<COMMIT-SHA> (see details below about packages publishing channels)
> Please note that @grafana/ui, @grafana/data, and @grafana/runtime packages are considered ALPHA even though they are not released as alpha versions.
> Please note that the @grafana/api-clients package is considered ALPHA even though it is not released as an alpha version.
### Stable releases
@@ -0,0 +1,7 @@
# Changelog
All notable changes to this project will be documented in this file.
## Unreleased
- Initial scaffold
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Grafana Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+32
View File
@@ -0,0 +1,32 @@
# Grafana API Clients
> **@grafana/api-clients is currently in BETA**.
This package hosts reusable clients for Grafana APIs. The implementation is currently in BETA and not yet published.
## Generating RTK Query API Clients
This guide explains how to generate API clients for Grafana APIs using this package.
### 1. Generate an OpenAPI snapshot
First, check if the `group` and the `version` are already present in [openapi_test.go](../../pkg/tests/apis/openapi_test.go). If so, move on to the next step. If you need to add a new block, you can check for the right `group` and `version` in the backend API call that you want to replicate in the frontend.
```go
{
Group: "dashboard.grafana.app",
Version: "v0alpha1",
}
```
Afterwards, you need to run the `TestIntegrationOpenAPIs` test. Note that it will fail the first time you run it. On the second run, it will generate the corresponding OpenAPI spec, which you can find in [openapi_snapshots](../../pkg/tests/apis/openapi_snapshots).
> Note: You don't need to follow these two steps if the `group` you're working with is already in the `openapi_test.go` file.
### 2. Run the API generator script
Run `yarn generate:api-client` and follow the prompts. See [API Client Generator](./src/generator/README.md) for details.
## Updating generated clients
To update the existing clients, for example, when the OpenAPI spec has changed, run `yarn generate-apis`. This will regenerate all the clients based on the current OpenAPI snapshots.
+111
View File
@@ -0,0 +1,111 @@
{
"author": "Grafana Labs",
"license": "Apache-2.0",
"private": true,
"name": "@grafana/api-clients",
"version": "12.3.0-pre",
"description": "Grafana API client utilities",
"keywords": [
"grafana",
"typescript"
],
"sideEffects": false,
"repository": {
"type": "git",
"url": "https://github.com/grafana/grafana.git",
"directory": "packages/grafana-api-clients"
},
"main": "src/index.ts",
"module": "src/index.ts",
"types": "src/index.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./src/index.ts",
"require": "./src/index.ts"
},
"./rtkq": {
"import": "./src/clients/rtkq/index.ts",
"require": "./src/clients/rtkq/index.ts"
},
"./rtkq/advisor/v0alpha1": {
"import": "./src/clients/rtkq/advisor/v0alpha1/index.ts",
"require": "./src/clients/rtkq/advisor/v0alpha1/index.ts"
},
"./rtkq/dashboard/v0alpha1": {
"import": "./src/clients/rtkq/dashboard/v0alpha1/index.ts",
"require": "./src/clients/rtkq/dashboard/v0alpha1/index.ts"
},
"./rtkq/folder/v1beta1": {
"import": "./src/clients/rtkq/folder/v1beta1/index.ts",
"require": "./src/clients/rtkq/folder/v1beta1/index.ts"
},
"./rtkq/iam/v0alpha1": {
"import": "./src/clients/rtkq/iam/v0alpha1/index.ts",
"require": "./src/clients/rtkq/iam/v0alpha1/index.ts"
},
"./rtkq/legacy/migrate-to-cloud": {
"import": "./src/clients/rtkq/migrate-to-cloud/index.ts",
"require": "./src/clients/rtkq/migrate-to-cloud/index.ts"
},
"./rtkq/legacy/preferences": {
"import": "./src/clients/rtkq/preferences/user/index.ts",
"require": "./src/clients/rtkq/preferences/user/index.ts"
},
"./rtkq/legacy/user": {
"import": "./src/clients/rtkq/user/index.ts",
"require": "./src/clients/rtkq/user/index.ts"
},
"./rtkq/playlist/v0alpha1": {
"import": "./src/clients/rtkq/playlist/v0alpha1/index.ts",
"require": "./src/clients/rtkq/playlist/v0alpha1/index.ts"
},
"./rtkq/preferences/v1alpha1": {
"import": "./src/clients/rtkq/preferences/v1alpha1/index.ts",
"require": "./src/clients/rtkq/preferences/v1alpha1/index.ts"
},
"./rtkq/provisioning/v0alpha1": {
"import": "./src/clients/rtkq/provisioning/v0alpha1/index.ts",
"require": "./src/clients/rtkq/provisioning/v0alpha1/index.ts"
},
"./rtkq/shorturl/v1alpha1": {
"import": "./src/clients/rtkq/shorturl/v1alpha1/index.ts",
"require": "./src/clients/rtkq/shorturl/v1alpha1/index.ts"
}
},
"publishConfig": {
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"access": "public"
},
"files": [
"./dist",
"./README.md",
"./CHANGELOG.md",
"./LICENSE_APACHE2"
],
"scripts": {
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
"bundle": "rollup -c rollup.config.ts --configPlugin esbuild",
"clean": "rimraf ./dist ./compiled ./package.tgz",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json",
"process-specs": "NODE_OPTIONS='--disable-warning=ExperimentalWarning' node --experimental-strip-types src/scripts/process-specs.ts",
"generate-apis": "yarn process-specs && NODE_OPTIONS='--disable-warning=ExperimentalWarning' rtk-query-codegen-openapi ./src/scripts/generate-rtk-apis.ts",
"generate:api-client": "NODE_OPTIONS='--experimental-strip-types --disable-warning=ExperimentalWarning' plop --plopfile src/generator/plopfile.ts"
},
"devDependencies": {
"@rtk-query/codegen-openapi": "^2.0.0",
"openapi-types": "^12.1.3",
"plop": "^4.0.1",
"rollup": "^4.22.4",
"typescript": "5.9.2"
},
"peerDependencies": {
"@grafana/runtime": ">=11.6 <= 12.x",
"@reduxjs/toolkit": "^2.8.0",
"rxjs": "7.8.2"
}
}
@@ -0,0 +1,8 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"tags": ["scope:package", "type:runtime"],
"targets": {
"build": {}
}
}
@@ -0,0 +1,39 @@
import { createRequire } from 'node:module';
import { cjsOutput, entryPoint, esmOutput, plugins } from '../rollup.config.parts';
const rq = createRequire(import.meta.url);
const pkg = rq('./package.json');
const apiClients = Object.entries<{ import: string; require: string }>(pkg.exports).filter(([key]) =>
key.startsWith('./rtkq/')
);
const apiClientConfigs = apiClients.map(([name, { import: importPath }]) => {
const baseCjsOutput = cjsOutput(pkg);
const entryFileNames = name.replace('./', '') + '.cjs';
const cjsOutputConfig = { ...baseCjsOutput, entryFileNames };
return {
input: importPath.replace('./', ''),
plugins,
output: [cjsOutputConfig, esmOutput(pkg, 'grafana-api-clients')],
treeshake: false,
};
});
export default [
{
input: entryPoint,
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')],
treeshake: false,
},
{
input: 'src/clients/rtkq/index.ts',
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')],
treeshake: false,
},
...apiClientConfigs,
];
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'advisor.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'advisorAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'correlations.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'correlationsAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,6 +1,8 @@
import { generatedAPI } from './endpoints.gen';
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export const correlationAPIv0alpha1 = generatedAPI.enhanceEndpoints({
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({
endpoints: {
createCorrelation: (endpointDefinition) => {
const originalQuery = endpointDefinition.query;
@@ -1,9 +1,11 @@
import { BaseQueryFn } from '@reduxjs/toolkit/query';
import { lastValueFrom } from 'rxjs';
import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime';
import { type BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
interface RequestOptions extends BackendSrvRequest {
import { handleRequestError } from '../../utils/utils';
export interface RequestOptions extends BackendSrvRequest {
manageError?: (err: unknown) => { error: unknown };
body?: BackendSrvRequest['data'];
}
@@ -49,11 +51,3 @@ export function createBaseQuery({ baseURL }: CreateBaseQueryOptions): BaseQueryF
return backendSrvBaseQuery;
}
export function handleRequestError(error: unknown) {
if (isFetchError(error) || error instanceof Error) {
return { error };
} else {
return { error: new Error('Unknown error') };
}
}
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'dashboard.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'dashboardAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,7 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { addTagTypes, generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({
addTagTypes: [...addTagTypes, 'Folder', 'Dashboard'],
});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'folder.grafana.app' as const;
export const API_VERSION = 'v1beta1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'folderAPIv1beta1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'iam.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'iamAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,52 @@
/**
* Don't manually add to this file! Use the `generate:api-client` command to add new API clients.
*/
import { generatedAPI as advisorAPIv0alpha1 } from './advisor/v0alpha1';
import { generatedAPI as correlationsAPIv0alpha1 } from './correlations/v0alpha1';
import { generatedAPI as dashboardAPIv0alpha1 } from './dashboard/v0alpha1';
import { generatedAPI as folderAPIv1beta1 } from './folder/v1beta1';
import { generatedAPI as iamAPIv0alpha1 } from './iam/v0alpha1';
import { generatedAPI as migrateToCloudAPI } from './migrate-to-cloud';
import { generatedAPI as playlistAPIv0alpha1 } from './playlist/v0alpha1';
import { generatedAPI as preferencesUserAPI } from './preferences/user';
import { generatedAPI as preferencesAPIv1alpha1 } from './preferences/v1alpha1';
import { generatedAPI as provisioningAPIv0alpha1 } from './provisioning/v0alpha1';
import { generatedAPI as shortURLAPIv1alpha1 } from './shorturl/v1alpha1';
import { generatedAPI as legacyUserAPI } from './user';
// PLOP_INJECT_IMPORT
/** RTK Query middleware for all API clients */
export const allMiddleware = [
advisorAPIv0alpha1.middleware,
dashboardAPIv0alpha1.middleware,
folderAPIv1beta1.middleware,
iamAPIv0alpha1.middleware,
migrateToCloudAPI.middleware,
playlistAPIv0alpha1.middleware,
preferencesAPIv1alpha1.middleware,
preferencesUserAPI.middleware,
provisioningAPIv0alpha1.middleware,
shortURLAPIv1alpha1.middleware,
correlationsAPIv0alpha1.middleware,
legacyUserAPI.middleware,
// PLOP_INJECT_MIDDLEWARE
] as const;
/** RTK Query reducers for all API clients */
export const allReducers = {
[advisorAPIv0alpha1.reducerPath]: advisorAPIv0alpha1.reducer,
[dashboardAPIv0alpha1.reducerPath]: dashboardAPIv0alpha1.reducer,
[folderAPIv1beta1.reducerPath]: folderAPIv1beta1.reducer,
[iamAPIv0alpha1.reducerPath]: iamAPIv0alpha1.reducer,
[migrateToCloudAPI.reducerPath]: migrateToCloudAPI.reducer,
[playlistAPIv0alpha1.reducerPath]: playlistAPIv0alpha1.reducer,
[preferencesAPIv1alpha1.reducerPath]: preferencesAPIv1alpha1.reducer,
[preferencesUserAPI.reducerPath]: preferencesUserAPI.reducer,
[provisioningAPIv0alpha1.reducerPath]: provisioningAPIv0alpha1.reducer,
[shortURLAPIv1alpha1.reducerPath]: shortURLAPIv1alpha1.reducer,
[correlationsAPIv0alpha1.reducerPath]: correlationsAPIv0alpha1.reducer,
[legacyUserAPI.reducerPath]: legacyUserAPI.reducer,
// PLOP_INJECT_REDUCER
};
export * from './createBaseQuery';
@@ -1,8 +1,8 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { createBaseQuery } from '../createBaseQuery';
export const baseAPI = createApi({
export const api = createApi({
reducerPath: 'migrateToCloudGeneratedAPI',
baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
@@ -1,4 +1,4 @@
import { baseAPI as api } from './baseAPI';
import { api } from './baseAPI';
const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({
getSessionList: build.query<GetSessionListApiResponse, GetSessionListApiArg>({
@@ -0,0 +1,89 @@
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query';
import { generatedAPI as rawAPI } from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({
addTagTypes: [
'cloud-migration-token',
'cloud-migration-session',
'cloud-migration-snapshot',
'cloud-migration-resource-dependencies',
],
endpoints: {
// Cloud-side - create token
getCloudMigrationToken: {
providesTags: ['cloud-migration-token'],
},
createCloudMigrationToken: {
invalidatesTags: ['cloud-migration-token'],
},
deleteCloudMigrationToken: {
invalidatesTags: ['cloud-migration-token'],
},
// On-prem session management (entering token)
getSessionList: {
providesTags: ['cloud-migration-session'] /* should this be a -list? */,
},
getSession: {
providesTags: ['cloud-migration-session'],
},
createSession: {
invalidatesTags: ['cloud-migration-session'],
},
deleteSession: {
invalidatesTags: ['cloud-migration-session', 'cloud-migration-snapshot'],
},
// Snapshot management
getShapshotList: {
providesTags: ['cloud-migration-snapshot'],
},
getSnapshot: {
providesTags: ['cloud-migration-snapshot'],
},
createSnapshot: {
invalidatesTags: ['cloud-migration-snapshot'],
},
uploadSnapshot: {
invalidatesTags: ['cloud-migration-snapshot'],
},
// Resource dependencies
getResourceDependencies: {
providesTags: ['cloud-migration-resource-dependencies'],
},
getDashboardByUid: suppressErrorsOnQuery,
getLibraryElementByUid: suppressErrorsOnQuery,
},
});
function suppressErrorsOnQuery<
QueryArg,
BaseQuery extends BaseQueryFn,
TagTypes extends string,
ResultType,
ReducerPath extends string,
PageParam,
>(endpoint: EndpointDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath, PageParam>) {
if (!endpoint.query) {
return;
}
// internal type from rtk-query that isn't exported
type InfiniteQueryCombinedArg<QueryArg, PageParam> = {
queryArg: QueryArg;
pageParam: PageParam;
};
const originalQuery = endpoint.query;
endpoint.query = (arg: QueryArg & InfiniteQueryCombinedArg<QueryArg, PageParam>) => {
const baseQuery = originalQuery(arg);
baseQuery.showErrorAlert = false;
return baseQuery;
};
}
export * from './endpoints.gen';
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'playlist.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'playlistAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -1,8 +1,8 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { createBaseQuery } from '../../createBaseQuery';
export const baseAPI = createApi({
export const api = createApi({
reducerPath: 'userPreferencesAPI',
baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
@@ -1,4 +1,4 @@
import { baseAPI as api } from './baseAPI';
import { api } from './baseAPI';
const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({
getUserPreferences: build.query<GetUserPreferencesApiResponse, GetUserPreferencesApiArg>({
@@ -1,9 +1,6 @@
import { generatedAPI } from './user/endpoints.gen';
import { generatedAPI as rawAPI } from './endpoints.gen';
export const { useGetUserPreferencesQuery, usePatchUserPreferencesMutation, useUpdateUserPreferencesMutation } =
generatedAPI;
export const userPreferencesAPI = generatedAPI.enhanceEndpoints({
export const generatedAPI = rawAPI.enhanceEndpoints({
addTagTypes: ['UserPreferences'],
endpoints: {
getUserPreferences: {
@@ -17,3 +14,5 @@ export const userPreferencesAPI = generatedAPI.enhanceEndpoints({
},
},
});
export * from './endpoints.gen';
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'preferences.grafana.app' as const;
export const API_VERSION = 'v1alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'preferencesAPIv1alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'provisioning.grafana.app' as const;
export const API_VERSION = 'v0alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'provisioningAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
export const API_GROUP = 'shorturl.grafana.app' as const;
export const API_VERSION = 'v1alpha1' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: 'shortURLAPIv1alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -0,0 +1,9 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from '../createBaseQuery';
export const api = createApi({
reducerPath: 'legacyUserAPI',
baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
});
@@ -0,0 +1,37 @@
import { api } from './baseAPI';
const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({
unstarDashboardByUid: build.mutation<UnstarDashboardByUidApiResponse, UnstarDashboardByUidApiArg>({
query: (queryArg) => ({ url: `/user/stars/dashboard/uid/${queryArg.dashboardUid}`, method: 'DELETE' }),
}),
starDashboardByUid: build.mutation<StarDashboardByUidApiResponse, StarDashboardByUidApiArg>({
query: (queryArg) => ({ url: `/user/stars/dashboard/uid/${queryArg.dashboardUid}`, method: 'POST' }),
}),
}),
overrideExisting: false,
});
export { injectedRtkApi as generatedAPI };
export type UnstarDashboardByUidApiResponse =
/** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
export type UnstarDashboardByUidApiArg = {
dashboardUid: string;
};
export type StarDashboardByUidApiResponse =
/** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
export type StarDashboardByUidApiArg = {
dashboardUid: string;
};
export type SuccessResponseBody = {
message?: string;
};
export type ErrorResponseBody = {
/** Error An optional detailed description of the actual error. Only included if running in developer mode. */
error?: string;
/** a human readable version of the error */
message: string;
/** Status An optional status to denote the cause of the error.
For example, a 412 Precondition Failed error may include additional information of why that error happened. */
status?: string;
};
export const { useUnstarDashboardByUidMutation, useStarDashboardByUidMutation } = injectedRtkApi;
@@ -0,0 +1,25 @@
import { generatedAPI as rawAPI } from './endpoints.gen';
export const generatedAPI = rawAPI
.enhanceEndpoints({
addTagTypes: ['dashboardStars'],
endpoints: {
starDashboardByUid: {
invalidatesTags: ['dashboardStars'],
},
unstarDashboardByUid: {
invalidatesTags: ['dashboardStars'],
},
},
})
.injectEndpoints({
endpoints: (build) => ({
getStars: build.query<string[], void>({
query: () => ({ url: '/user/stars' }),
providesTags: ['dashboardStars'],
}),
}),
});
export const { useGetStarsQuery } = generatedAPI;
export * from './endpoints.gen';
@@ -26,7 +26,9 @@ The generator automates the following:
- `scripts/generate-rtk-apis.ts` for OSS APIs
- `local/generate-enterprise-apis.ts` for Enterprise APIs
3. Creates the `index.ts` file with proper exports
4. For OSS APIs only: Registers Redux reducers and middleware in the store. For Enterprise this needs to be done manually
4. For OSS APIs only:
- Exports Redux reducers and middleware in `rtkq.ts`
- Updates `package.json` exports to include the new API client
5. Formats all generated files using Prettier and ESLint
6. Automatically runs the appropriate command to generate endpoints from the OpenAPI schema
@@ -1,4 +1,6 @@
import { execSync } from 'child_process';
import fs from 'fs';
import { OpenAPIV3 } from 'openapi-types';
import path from 'path';
type PlopActionFunction = (
@@ -34,14 +36,24 @@ export const formatEndpoints = () => (endpointsInput: string | string[]) => {
// List of created or modified files
export const getFilesToFormat = (groupName: string, version: string, isEnterprise = false) => {
const apiClientBasePath = isEnterprise ? 'public/app/extensions/api/clients' : 'public/app/api/clients';
const generateScriptPath = isEnterprise ? 'local/generate-enterprise-apis.ts' : 'scripts/generate-rtk-apis.ts';
const apiClientBasePath = isEnterprise
? 'public/app/extensions/api/clients'
: 'packages/grafana-api-clients/src/clients/rtkq';
const generateScriptPath = isEnterprise
? 'local/generate-enterprise-apis.ts'
: 'packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts';
return [
`${apiClientBasePath}/${groupName}/${version}/baseAPI.ts`,
`${apiClientBasePath}/${groupName}/${version}/index.ts`,
generateScriptPath,
...(isEnterprise ? [] : [`public/app/core/reducers/root.ts`, `public/app/store/configureStore.ts`]),
...(isEnterprise
? []
: [
`packages/grafana-api-clients/src/index.ts`,
`packages/grafana-api-clients/src/clients/rtkq/index.ts`,
`packages/grafana-api-clients/package.json`,
]),
];
};
@@ -53,9 +65,10 @@ export const runGenerateApis =
let command;
if (isEnterprise) {
command = 'yarn process-specs && npx rtk-query-codegen-openapi ./local/generate-enterprise-apis.ts';
command =
'yarn workspace @grafana/api-clients process-specs && npx rtk-query-codegen-openapi ./local/generate-enterprise-apis.ts';
} else {
command = 'yarn generate-apis';
command = 'yarn workspace @grafana/api-clients generate-apis';
}
console.log(`⏳ Running ${command} to generate endpoints...`);
@@ -113,3 +126,62 @@ export const validateGroup = (group: string) => {
export const validateVersion = (version: string) => {
return version && /^v\d+[a-z]*\d+$/.test(version) ? true : 'Version should be in format: v0alpha1, v1beta2, etc.';
};
export const updatePackageJsonExports =
(basePath: string): PlopActionFunction =>
(answers) => {
try {
const { groupName, version } = answers;
if (!groupName || !version) {
return '❌ Missing groupName or version for package.json update';
}
const packageJsonPath = path.join(basePath, 'packages/grafana-api-clients/package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Create the new export entry
const newExportKey = `./rtkq/${groupName}/${version}`;
const newExportValue = {
import: `./src/clients/rtkq/${groupName}/${version}/index.ts`,
require: `./src/clients/rtkq/${groupName}/${version}/index.ts`,
};
// Check if export already exists
if (packageJson.exports[newExportKey]) {
return `✅ Export for ${newExportKey} already exists in package.json`;
}
// Add the new export entry
packageJson.exports[newExportKey] = newExportValue;
// Write the updated package.json back to file
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
return `✅ Added export for ${newExportKey} to package.json`;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('❌ Failed to update package.json exports:', errorMessage);
return '❌ Failed to update package.json exports. See error above.';
}
};
/**
* TODO: Make this work more generically with endpoints that don't have operationIds defined -
* then we can allow selection of endpoints in the generator
*/
export const getOperationIds = (apiSpec: OpenAPIV3.Document) => {
return (
Object.values(apiSpec.paths).flatMap((path) => {
return Object.entries(path || {})
.filter(([name]) => name !== 'parameters')
.flatMap(([_, pathItem]) => {
if (typeof pathItem === 'object' && 'operationId' in pathItem && pathItem.operationId) {
return pathItem.operationId;
}
return null;
})
.filter((id) => id !== null);
}) || []
);
};
@@ -1,27 +1,26 @@
import fs from 'fs';
import path from 'path';
import type { NodePlopAPI, PlopGeneratorConfig } from 'plop';
import {
formatEndpoints,
validateGroup,
validateVersion,
formatFiles,
getFilesToFormat,
runGenerateApis,
formatFiles,
// The file extension is necessary to make the imports
// work with the '--experimental-strip-types' flag
// @ts-ignore
updatePackageJsonExports,
validateGroup,
validateVersion,
} from './helpers.ts';
// @ts-ignore
import { type ActionConfig, type PlopData, isPlopData } from './types.ts';
export default function plopGenerator(plop: NodePlopAPI) {
// Grafana root path
const basePath = path.resolve(import.meta.dirname, '../..');
const basePath = path.resolve(import.meta.dirname, '../../../..');
// Register custom action types
plop.setActionType('runGenerateApis', runGenerateApis(basePath));
plop.setActionType('formatFiles', formatFiles(basePath));
plop.setActionType('updatePackageJsonExports', updatePackageJsonExports(basePath));
// Used in templates to format endpoints
plop.setHelper('formatEndpoints', formatEndpoints());
@@ -29,13 +28,16 @@ export default function plopGenerator(plop: NodePlopAPI) {
const generateRtkApiActions = (data: PlopData) => {
const { reducerPath, groupName, version, isEnterprise } = data;
const apiClientBasePath = isEnterprise ? 'public/app/extensions/api/clients' : 'public/app/api/clients';
const generateScriptPath = isEnterprise ? 'local/generate-enterprise-apis.ts' : 'scripts/generate-rtk-apis.ts';
const apiClientBasePath = isEnterprise
? 'public/app/extensions/api/clients'
: 'packages/grafana-api-clients/src/clients/rtkq';
const generateScriptPath = isEnterprise
? 'local/generate-enterprise-apis.ts'
: 'packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts';
// Using app path, so the imports work on any file level
const clientImportPath = isEnterprise ? '../extensions/api/clients' : 'app/api/clients';
const clientImportPath = isEnterprise ? '../extensions/api/clients' : '.';
const apiPathPrefix = isEnterprise ? '../public/app/extensions/api/clients' : '../public/app/api/clients';
const apiPathPrefix = isEnterprise ? '../public/app/extensions/api/clients' : '../clients';
const templateData = {
...data,
@@ -68,27 +70,24 @@ export default function plopGenerator(plop: NodePlopAPI) {
actions.push(
{
type: 'modify',
path: path.join(basePath, 'public/app/core/reducers/root.ts'),
path: '../clients/rtkq/index.ts',
pattern: '// PLOP_INJECT_IMPORT',
template: `import { ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
template: `import { generatedAPI as ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
},
{
type: 'modify',
path: path.join(basePath, 'public/app/core/reducers/root.ts'),
path: '../clients/rtkq/index.ts',
pattern: '// PLOP_INJECT_REDUCER',
template: `[${reducerPath}.reducerPath]: ${reducerPath}.reducer,\n // PLOP_INJECT_REDUCER`,
},
{
type: 'modify',
path: path.join(basePath, 'public/app/store/configureStore.ts'),
pattern: '// PLOP_INJECT_IMPORT',
template: `import { ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
},
{
type: 'modify',
path: path.join(basePath, 'public/app/store/configureStore.ts'),
path: '../clients/rtkq/index.ts',
pattern: '// PLOP_INJECT_MIDDLEWARE',
template: `${reducerPath}.middleware,\n // PLOP_INJECT_MIDDLEWARE`,
},
{
type: 'updatePackageJsonExports',
}
);
}
@@ -108,6 +107,16 @@ export default function plopGenerator(plop: NodePlopAPI) {
return actions;
};
// Read files from data/openapi directory and use as an array of API clients that the user can select
const openapiDir = path.join(basePath, 'data/openapi');
let possibleOpenAPISpecs: string[] = [];
try {
possibleOpenAPISpecs = fs.readdirSync(openapiDir).filter((file: string) => file.endsWith('.json'));
} catch (e) {
possibleOpenAPISpecs = [];
}
const generator: PlopGeneratorConfig = {
description: 'Generate RTK Query API client for a Grafana API group',
prompts: [
@@ -118,30 +127,43 @@ export default function plopGenerator(plop: NodePlopAPI) {
default: false,
},
{
type: 'input',
name: 'groupName',
message: 'API group name (e.g. dashboard):',
type: 'list',
loop: false,
choices: possibleOpenAPISpecs,
pageSize: 50,
name: 'apiInfo',
message: 'OpenAPI spec:',
validate: (input: string) => (input?.trim() ? true : 'Group name is required'),
},
{
type: 'input',
name: 'groupName',
message: 'API group name:',
validate: (input: string) => (input?.trim() ? true : 'Group name is required'),
default: (answers: { apiInfo?: string }) => answers.apiInfo?.split('.grafana.app-')[0],
},
{
type: 'input',
name: 'group',
message: 'API group (e.g. dashboard.grafana.app):',
message: 'API group:',
default: (answers: { groupName?: string }) => `${answers.groupName}.grafana.app`,
validate: validateGroup,
},
{
type: 'input',
name: 'version',
message: 'API version (e.g. v0alpha1):',
default: 'v0alpha1',
message: 'API version:',
default: (answers: { apiInfo?: string }) => answers.apiInfo?.split('.grafana.app-')[1].replace(/\.json$/, ''),
validate: validateVersion,
},
{
type: 'input',
name: 'reducerPath',
message: 'Reducer path (e.g. dashboardAPIv0alpha1):',
default: (answers: { groupName?: string; version?: string }) => `${answers.groupName}API${answers.version}`,
message: 'Reducer path:',
default: (answers: { groupName?: string; version?: string }) => {
const groupNameParsed = answers.groupName?.replace(/\.([a-z])/g, (_, letter) => letter.toUpperCase());
return `${groupNameParsed}API${answers.version}`;
},
validate: (input: string) =>
input?.endsWith('API') || input?.match(/API[a-z]\d+[a-z]*\d*$/)
? true
@@ -150,8 +172,8 @@ export default function plopGenerator(plop: NodePlopAPI) {
{
type: 'input',
name: 'endpoints',
default: undefined,
message: 'Endpoints to include (comma-separated, optional):',
validate: () => true,
},
],
actions: function (data) {
@@ -0,0 +1,21 @@
import { createApi } from '@reduxjs/toolkit/query/react';
{{#if isEnterprise}}
import { getAPIBaseURL } from '@grafana/api-clients';
import { createBaseQuery } from '@grafana/api-clients/rtkq';
{{else}}
import { getAPIBaseURL } from '../../../../utils/utils';
import { createBaseQuery } from '../../createBaseQuery';
{{/if}}
export const API_GROUP = '{{group}}' as const;
export const API_VERSION = '{{version}}' as const;
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
export const api = createApi({
reducerPath: '{{reducerPath}}',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -0,0 +1,2 @@
...createAPIConfig('{{groupName}}', '{{version}}'{{#if endpoints}}, [{{{formatEndpoints endpoints}}}]{{/if}}),
// PLOP_INJECT_API_CLIENT - Used by the API client generator
@@ -0,0 +1,5 @@
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -10,8 +10,17 @@ export interface RunGenerateApisActionConfig {
isEnterprise: boolean;
}
export interface UpdatePackageJsonExportsActionConfig {
type: 'updatePackageJsonExports';
}
// Union type of all possible action configs
export type ActionConfig = AddActionConfig | ModifyActionConfig | FormatFilesActionConfig | RunGenerateApisActionConfig;
export type ActionConfig =
| AddActionConfig
| ModifyActionConfig
| FormatFilesActionConfig
| RunGenerateApisActionConfig
| UpdatePackageJsonExportsActionConfig;
export interface PlopData {
groupName: string;
@@ -0,0 +1 @@
export { getAPINamespace, getAPIBaseURL, normalizeError, handleRequestError } from './utils/utils';
@@ -0,0 +1,111 @@
// Generates Redux Toolkit API slices for certain APIs from the OpenAPI spec
import type { ConfigFile } from '@rtk-query/codegen-openapi';
import { OpenAPIV3 } from 'openapi-types';
import path from 'path';
// Grafana root path - navigate up from this script's directory
const basePath = path.resolve(__dirname, '../../../..');
// Include some types that are used inside the @rtk-query/codegen-openapi package
// but not exported
declare const operationKeys: readonly ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
type OperationDefinition = {
path: string;
verb: (typeof operationKeys)[number];
pathItem: OpenAPIV3.PathItemObject;
operation: OpenAPIV3.OperationObject;
};
type EndpointMatcher = string[] | ((operationName: string, operationDefinition: OperationDefinition) => boolean);
/**
* Helper to return consistent base API generation config
*/
const createAPIConfig = (app: string, version: string, filterEndpoints?: EndpointMatcher, additional = {}) => {
const filePath = `../clients/rtkq/${app}/${version}/endpoints.gen.ts`;
return {
[filePath]: {
schemaFile: path.join(basePath, `data/openapi/${app}.grafana.app-${version}.json`),
apiFile: `../clients/rtkq/${app}/${version}/baseAPI.ts`,
filterEndpoints,
tag: true,
...additional,
},
};
};
const config: ConfigFile = {
schemaFile: '', // leave this empty, and instead populate the outputFiles object below
apiFile: '', // leave this empty, and instead populate the outputFiles object below
exportName: 'generatedAPI',
outputFiles: {
'../clients/rtkq/migrate-to-cloud/endpoints.gen.ts': {
schemaFile: path.join(basePath, 'public/openapi3.json'),
apiFile: '../clients/rtkq/migrate-to-cloud/baseAPI.ts',
hooks: true,
filterEndpoints: [
'getSessionList',
'getSession',
'deleteSession',
'createSession',
'getShapshotList',
'getSnapshot',
'uploadSnapshot',
'createSnapshot',
'cancelSnapshot',
'createCloudMigrationToken',
'deleteCloudMigrationToken',
'getCloudMigrationToken',
'getDashboardByUid',
'getLibraryElementByUid',
'getResourceDependencies',
],
},
'../clients/rtkq/preferences/user/endpoints.gen.ts': {
schemaFile: path.join(basePath, 'public/openapi3.json'),
hooks: true,
apiFile: '../clients/rtkq/preferences/user/baseAPI.ts',
filterEndpoints: ['getUserPreferences', 'updateUserPreferences', 'patchUserPreferences'],
},
'../clients/rtkq/user/endpoints.gen.ts': {
schemaFile: path.join(basePath, 'public/openapi3.json'),
hooks: true,
apiFile: '../clients/rtkq/user/baseAPI.ts',
filterEndpoints: ['starDashboardByUid', 'unstarDashboardByUid'],
},
...createAPIConfig('iam', 'v0alpha1', ['getDisplayMapping']),
...createAPIConfig('provisioning', 'v0alpha1', filterEndpoints, { hooks: true }),
...createAPIConfig('folder', 'v1beta1', undefined),
...createAPIConfig('advisor', 'v0alpha1', [
'createCheck',
'getCheck',
'listCheck',
'deleteCheck',
'updateCheck',
'listCheckType',
'updateCheckType',
]),
...createAPIConfig('playlist', 'v0alpha1', [
'listPlaylist',
'getPlaylist',
'createPlaylist',
'deletePlaylist',
'replacePlaylist',
]),
...createAPIConfig('shorturl', 'v1alpha1'),
...createAPIConfig('preferences', 'v1alpha1', undefined, { hooks: true }),
...createAPIConfig('dashboard', 'v0alpha1', ['getSearch']),
...createAPIConfig('correlations', 'v0alpha1'),
// PLOP_INJECT_API_CLIENT - Used by the API client generator
},
};
function filterEndpoints(name: string) {
return !name.toLowerCase().includes('getapiresources') && !name.toLowerCase().includes('update');
}
export default config;
@@ -172,11 +172,14 @@ function processDirectory(sourceDir: string, outputDir: string) {
}
}
// Grafana root path - navigate up from this script's directory
const basePath = path.resolve(import.meta.dirname, '../../../..');
const sourceDirs = [
path.resolve(import.meta.dirname, '../pkg/tests/apis/openapi_snapshots'),
path.resolve(import.meta.dirname, '../pkg/extensions/apiserver/tests/openapi_snapshots'),
path.join(basePath, 'pkg/tests/apis/openapi_snapshots'),
path.join(basePath, 'pkg/extensions/apiserver/tests/openapi_snapshots'),
];
const outputDir = path.resolve(import.meta.dirname, '../data/openapi');
const outputDir = path.join(basePath, 'data/openapi');
for (const sourceDir of sourceDirs) {
processDirectory(sourceDir, outputDir);
@@ -0,0 +1,62 @@
import { config, isFetchError } from '@grafana/runtime';
export const getAPINamespace = () => config.namespace;
/**
* Get a base URL for a k8s API endpoint with parameterised namespace given it's group and version
* @param group the k8s group, e.g. dashboard.grafana.app
* @param version e.g. v0alpha1
* @returns
*/
export const getAPIBaseURL = (group: string, version: string) => {
return `/apis/${group}/${version}/namespaces/${getAPINamespace()}` as const;
};
/**
* Normalize error from various error formats into a string message
* @param e the raw error
* @returns string error message
*/
export const normalizeError = (e: unknown): string => {
if (!e) {
return 'Unknown error';
}
if (typeof e === 'object') {
if ('data' in e && e.data && typeof e.data === 'object' && 'message' in e.data) {
return String(e.data.message);
}
// Direct error with message property
if ('message' in e) {
return String(e.message);
}
// Nested error wrapper (RTK Query format)
if ('error' in e) {
if (e.error instanceof Error) {
return e.error.message;
} else if (isFetchError(e.error)) {
if (e.error.data && typeof e.error.data === 'object' && 'message' in e.error.data) {
return String(e.error.data.message);
}
if (Array.isArray(e.error.data.errors) && e.error.data.errors.length) {
return e.error.data.errors.join('\n');
}
}
}
}
if (e instanceof Error) {
return e.message;
}
return String(e);
};
export function handleRequestError(error: unknown) {
if (isFetchError(error) || error instanceof Error) {
return { error };
} else {
return { error: new Error('Unknown error') };
}
}
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["dist", "node_modules", "**/*.test.ts*"]
}
@@ -0,0 +1,21 @@
{
"extends": "../../scripts/tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"declaration": true,
"jsx": "react-jsx",
"declarationDir": "./dist/types",
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."],
"allowImportingTsExtensions": true
},
"exclude": ["dist/**/*"],
"include": [
"../../public/app/types/window.d.ts",
"../../public/app/types/images.d.ts",
"../grafana-ui/src/types/*.d.ts",
"../grafana-i18n/src/types/*.d.ts",
"src/**/*.ts*"
]
}
-25
View File
@@ -1,25 +0,0 @@
## Generating RTK Query API Clients
To show the steps to follow, we are going to work on adding an API client to create a new dashboard. Just adapt the following guide to your use case.
### 1. Generate an OpenAPI snapshot
First, check if the `group` and the `version` are already present in [openapi_test.go](/pkg/tests/apis/openapi_test.go). If so, move on to the next step.
<br/> If you need to add a new block, you can check for the right `group` and `version` in the backend API call that you want to replicate in the frontend.
```go
{
Group: "dashboard.grafana.app",
Version: "v0alpha1",
}
```
Afterwards, you need to run the `TestIntegrationOpenAPIs` test. Note that it will fail the first time you run it. On the second run, it will generate the corresponding OpenAPI spec, which you can find in [openapi_snapshots](/pkg/tests/apis/openapi_snapshots).
<br/>
<br/>
> Note: You don’t need to follow these two steps if the `group` you’re working with is already in the `openapi_test.go` file.
### 2. Run the API generator script
Run `yarn generate:api-client` and follow the prompts. See [API Client Generator](./generator/README.md) for details.
@@ -1,14 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('advisor.grafana.app', 'v0alpha1');
export const api = createApi({
reducerPath: 'advisorAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,41 +0,0 @@
import { generatedAPI } from './endpoints.gen';
export const advisorAPIv0alpha1 = generatedAPI.enhanceEndpoints({
endpoints: {
// Need to mutate the generated query to set the Content-Type header correctly
updateCheck: (endpointDefinition) => {
const originalQuery = endpointDefinition.query;
if (originalQuery) {
endpointDefinition.query = (requestOptions) => ({
...originalQuery(requestOptions),
headers: {
'Content-Type': 'application/json-patch+json',
},
body: JSON.stringify(requestOptions.patch),
});
}
},
updateCheckType: (endpointDefinition) => {
const originalQuery = endpointDefinition.query;
if (originalQuery) {
endpointDefinition.query = (requestOptions) => ({
...originalQuery(requestOptions),
headers: {
'Content-Type': 'application/json-patch+json',
},
body: JSON.stringify(requestOptions.patch),
});
}
},
},
});
export const {
useGetCheckQuery,
useListCheckQuery,
useCreateCheckMutation,
useDeleteCheckMutation,
useUpdateCheckMutation,
useListCheckTypeQuery,
useUpdateCheckTypeMutation,
} = advisorAPIv0alpha1;
export { type Check, type CheckType } from './endpoints.gen'; // eslint-disable-line
@@ -1,14 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('shorturl.grafana.app', 'v1alpha1');
export const api = createApi({
reducerPath: 'shortURLAPIv1alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,14 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('dashboard.grafana.app', 'v0alpha1');
export const api = createApi({
reducerPath: 'dashboardAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,12 +1,5 @@
import { generatedAPI } from './endpoints.gen';
import { generatedAPI } from '@grafana/api-clients/rtkq/dashboard/v0alpha1';
export const dashboardAPIv0alpha1 = generatedAPI.enhanceEndpoints({
addTagTypes: ['Folder', 'Dashboard'],
endpoints: {
getSearch: (endpointDefinition) => {
endpointDefinition.providesTags = ['Search', 'Folder', 'Dashboard'];
},
},
});
export const dashboardAPIv0alpha1 = generatedAPI.enhanceEndpoints({});
export const { useGetSearchQuery } = dashboardAPIv0alpha1;
@@ -1,14 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('folder.grafana.app', 'v1beta1');
export const api = createApi({
reducerPath: 'folderAPIv1beta1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,6 +1,6 @@
import { generatedAPI } from '@grafana/api-clients/rtkq/folder/v1beta1';
import { DescendantCount } from 'app/types/folders';
import { generatedAPI } from './endpoints.gen';
import { getParsedCounts } from './utils';
export const folderAPIv1beta1 = generatedAPI
@@ -80,4 +80,9 @@ export const {
} = folderAPIv1beta1;
// eslint-disable-next-line no-barrel-files/no-barrel-files
export { type CreateFolderApiArg, type Folder, type FolderList, type ReplaceFolderApiArg } from './endpoints.gen';
export {
type Folder,
type FolderList,
type CreateFolderApiArg,
type ReplaceFolderApiArg,
} from '@grafana/api-clients/rtkq/folder/v1beta1';
@@ -1,3 +1,4 @@
import { ResourceStats } from '@grafana/api-clients/rtkq/folder/v1beta1';
import { AppEvents } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
@@ -6,8 +7,6 @@ import appEvents from '../../../../core/app_events';
import { isProvisionedFolder } from '../../../../features/browse-dashboards/api/isProvisioned';
import { useDispatch } from '../../../../types/store';
import { ResourceStats } from './endpoints.gen';
import { folderAPIv1beta1 as folderAPI } from './index';
export async function isProvisionedFolderCheck(
@@ -1,12 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('iam.grafana.app', 'v0alpha1');
export const api = createApi({
baseQuery: createBaseQuery({ baseURL: BASE_URL }),
reducerPath: 'iamAPIv0alpha1',
endpoints: () => ({}),
});
+2 -2
View File
@@ -1,5 +1,5 @@
import { generatedAPI } from './endpoints.gen';
import { generatedAPI } from '@grafana/api-clients/rtkq/iam/v0alpha1';
export const iamAPIv0alpha1 = generatedAPI.enhanceEndpoints({});
export const { useGetDisplayMappingQuery, useLazyGetDisplayMappingQuery } = generatedAPI;
export const { useGetDisplayMappingQuery, useLazyGetDisplayMappingQuery } = iamAPIv0alpha1;
@@ -1,14 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('playlist.grafana.app', 'v0alpha1');
export const api = createApi({
reducerPath: 'playlistAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,3 +1,4 @@
import { generatedAPI, type Playlist, type PlaylistSpec } from '@grafana/api-clients/rtkq/playlist/v0alpha1';
import { getBackendSrv } from '@grafana/runtime';
import { notifyApp } from '../../../../core/actions';
@@ -5,8 +6,6 @@ import { createSuccessNotification } from '../../../../core/copy/appNotification
import { contextSrv } from '../../../../core/services/context_srv';
import { handleError } from '../../../utils';
import { generatedAPI, Playlist, PlaylistSpec } from './endpoints.gen';
export const playlistAPIv0alpha1 = generatedAPI.enhanceEndpoints({
endpoints: {
getPlaylist: {
@@ -87,4 +86,4 @@ export const {
} = playlistAPIv0alpha1;
// eslint-disable-next-line no-barrel-files/no-barrel-files
export type { Playlist, PlaylistSpec } from './endpoints.gen';
export type { Playlist, PlaylistSpec } from '@grafana/api-clients/rtkq/playlist/v0alpha1';
@@ -1,14 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('preferences.grafana.app', 'v1alpha1');
export const api = createApi({
reducerPath: 'preferencesAPIv1alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,9 +1,8 @@
import { generatedAPI } from '@grafana/api-clients/rtkq/preferences/v1alpha1';
import { t } from '@grafana/i18n';
import { notifyApp } from 'app/core/actions';
import { createSuccessNotification, createErrorNotification } from 'app/core/copy/appNotification';
import { generatedAPI } from './endpoints.gen';
export const preferencesAPIv1alpha1 = generatedAPI.enhanceEndpoints({
endpoints: {
addStar: {
@@ -40,4 +39,4 @@ export const preferencesAPIv1alpha1 = generatedAPI.enhanceEndpoints({
});
// eslint-disable-next-line no-barrel-files/no-barrel-files
export * from './endpoints.gen';
export * from '@grafana/api-clients/rtkq/preferences/v1alpha1';
@@ -1,6 +1,6 @@
import { parseListOptionsSelector } from '../../../../features/apiserver/client';
import { ListOptions } from '../../../../features/apiserver/types';
import { ListRepositoryApiArg } from '../v0alpha1/endpoints.gen';
import { ListRepositoryApiArg } from 'app/api/clients/provisioning/v0alpha1';
import { parseListOptionsSelector } from 'app/features/apiserver/client';
import { ListOptions } from 'app/features/apiserver/types';
type ListParams = Omit<ListRepositoryApiArg, 'fieldSelector' | 'labelSelector'> &
Pick<ListOptions, 'labelSelector' | 'fieldSelector'>;
@@ -1,14 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { config } from '@grafana/runtime';
import { createBaseQuery } from 'app/api/createBaseQuery';
export const BASE_URL = `apis/provisioning.grafana.app/v0alpha1/namespaces/${config.namespace}`;
export const api = createApi({
reducerPath: 'provisioningAPIv0alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,3 +1,12 @@
import {
generatedAPI,
type JobSpec,
type JobStatus,
type RepositorySpec,
type RepositoryStatus,
type ErrorDetails,
type Status,
} from '@grafana/api-clients/rtkq/provisioning/v0alpha1';
import { t } from '@grafana/i18n';
import { isFetchError } from '@grafana/runtime';
import { clearFolders } from 'app/features/browse-dashboards/state/slice';
@@ -7,18 +16,9 @@ import { notifyApp } from '../../../../core/actions';
import { createSuccessNotification, createErrorNotification } from '../../../../core/copy/appNotification';
import { PAGE_SIZE } from '../../../../features/browse-dashboards/api/services';
import { refetchChildren } from '../../../../features/browse-dashboards/state/actions';
import { handleError } from '../../../utils';
import { createOnCacheEntryAdded } from '../utils/createOnCacheEntryAdded';
import {
generatedAPI,
JobSpec,
JobStatus,
RepositorySpec,
RepositoryStatus,
ErrorDetails,
Status,
} from './endpoints.gen';
export const provisioningAPIv0alpha1 = generatedAPI.enhanceEndpoints({
endpoints: {
listJob: {
@@ -104,11 +104,8 @@ export const provisioningAPIv0alpha1 = generatedAPI.enhanceEndpoints({
try {
await queryFulfilled;
} catch (e) {
if (!e) {
dispatch(notifyApp(createErrorNotification('Error validating repository', new Error('Unknown error'))));
} else if (e instanceof Error) {
dispatch(notifyApp(createErrorNotification('Error validating repository', e)));
} else if (typeof e === 'object' && 'error' in e && isFetchError(e.error)) {
// Handle special cases first
if (typeof e === 'object' && e && 'error' in e && isFetchError(e.error)) {
// Handle Status error responses (Kubernetes style)
if (e.error.data.kind === 'Status' && e.error.data.status === 'Failure') {
const statusError: Status = e.error.data;
@@ -120,16 +117,21 @@ export const provisioningAPIv0alpha1 = generatedAPI.enhanceEndpoints({
)
)
);
return;
}
// Handle TestResults error responses with field errors
else if (Array.isArray(e.error.data.errors) && e.error.data.errors.length) {
if (Array.isArray(e.error.data.errors) && e.error.data.errors.length) {
const nonFieldErrors = e.error.data.errors.filter((err: ErrorDetails) => !err.field);
// Only show notification if there are errors that don't have a field, field errors are handled by the form
if (nonFieldErrors.length > 0) {
dispatch(notifyApp(createErrorNotification('Error validating repository')));
}
return;
}
}
// For all other cases, use handleError
handleError(e, dispatch, 'Error validating repository');
}
},
},
@@ -242,4 +244,4 @@ export const provisioningAPIv0alpha1 = generatedAPI.enhanceEndpoints({
});
// eslint-disable-next-line no-barrel-files/no-barrel-files
export * from './endpoints.gen';
export * from '@grafana/api-clients/rtkq/provisioning/v0alpha1';
@@ -1,14 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('shorturl.grafana.app', 'v1alpha1');
export const api = createApi({
reducerPath: 'shortURLAPIv1alpha1',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});
@@ -1,4 +1,4 @@
import { generatedAPI } from './endpoints.gen';
import { generatedAPI } from '@grafana/api-clients/rtkq/shorturl/v1alpha1';
export const shortURLAPIv1alpha1 = generatedAPI.enhanceEndpoints({
endpoints: {
-25
View File
@@ -1,25 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
export const legacyUserAPI = createApi({
reducerPath: 'legacyUserAPI',
baseQuery: createBaseQuery({ baseURL: '/api' }),
tagTypes: ['dashboardStars'],
endpoints: (build) => ({
getStars: build.query<string[], void>({
query: () => ({ url: '/user/stars' }),
providesTags: ['dashboardStars'],
}),
starDashboard: build.mutation<void, { id: string }>({
query: ({ id }) => ({ url: `/user/stars/dashboard/uid/${id}`, method: 'POST' }),
invalidatesTags: ['dashboardStars'],
}),
unstarDashboard: build.mutation<void, { id: string }>({
query: ({ id }) => ({ url: `/user/stars/dashboard/uid/${id}`, method: 'DELETE' }),
invalidatesTags: ['dashboardStars'],
}),
}),
});
export const { useGetStarsQuery, useStarDashboardMutation, useUnstarDashboardMutation } = legacyUserAPI;
+8 -26
View File
@@ -1,21 +1,9 @@
import { config, isFetchError } from '@grafana/runtime';
import { normalizeError } from '@grafana/api-clients';
import { ThunkDispatch } from 'app/types/store';
import { notifyApp } from '../core/actions';
import { createErrorNotification } from '../core/copy/appNotification';
export const getAPINamespace = () => config.namespace;
/**
* Get a base URL for a k8s API endpoint with parameterised namespace given it's group and version
* @param group the k8s group, e.g. dashboard.grafana.app
* @param version e.g. v0alpha1
* @returns
*/
export const getAPIBaseURL = (group: string, version: string) => {
return `/apis/${group}/${version}/namespaces/${getAPINamespace()}`;
};
/**
* Handle an error from a k8s API call
* @param e the raw error
@@ -23,19 +11,8 @@ export const getAPIBaseURL = (group: string, version: string) => {
* @param message error alert title. error details will also be surfaced
*/
export const handleError = (e: unknown, dispatch: ThunkDispatch, message: string) => {
if (!e) {
dispatch(notifyApp(createErrorNotification(message, new Error('Unknown error'))));
} else if (e instanceof Error) {
dispatch(notifyApp(createErrorNotification(message, e)));
} else if (typeof e === 'object' && 'error' in e) {
if (e.error instanceof Error) {
dispatch(notifyApp(createErrorNotification(message, e.error)));
} else if (isFetchError(e.error)) {
if (Array.isArray(e.error.data.errors) && e.error.data.errors.length) {
dispatch(notifyApp(createErrorNotification(message, e.error.data.errors.join('\n'))));
}
}
}
const errorMessage = normalizeError(e);
dispatch(notifyApp(createErrorNotification(message, errorMessage)));
};
export function extractErrorMessage(error: unknown): string {
@@ -49,3 +26,8 @@ export function extractErrorMessage(error: unknown): string {
}
return String(error);
}
// TODO: Change imports to be directly from api-clients package
// Best done after hackathon
// eslint-disable-next-line no-barrel-files/no-barrel-files
export { getAPIBaseURL, getAPINamespace } from '@grafana/api-clients';
@@ -3,6 +3,7 @@ import { DOMAttributes } from '@react-types/shared';
import { memo, forwardRef, useCallback } from 'react';
import { useLocation } from 'react-router-dom-v5-compat';
import { usePatchUserPreferencesMutation } from '@grafana/api-clients/rtkq/legacy/preferences';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
@@ -10,7 +11,6 @@ import { config, reportInteraction } from '@grafana/runtime';
import { ScrollContainer, useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { setBookmark } from 'app/core/reducers/navBarTree';
import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index';
import { useDispatch, useSelector } from 'app/types/store';
import { MegaMenuHeader } from './MegaMenuHeader';
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useGetUserPreferencesQuery } from '@grafana/api-clients/rtkq/legacy/preferences';
import { config } from '@grafana/runtime';
import { useGetUserPreferencesQuery } from 'app/features/preferences/api';
import { contextSrv } from '../../../services/context_srv';
+2 -26
View File
@@ -2,10 +2,7 @@ import { ReducersMapObject } from '@reduxjs/toolkit';
import { AnyAction, combineReducers } from 'redux';
import { notificationsAPIv0alpha1, rulesAPIv0alpha1 } from '@grafana/alerting/unstable';
import { dashboardAPIv0alpha1 } from 'app/api/clients/dashboard/v0alpha1';
import { preferencesAPIv1alpha1 } from 'app/api/clients/preferences/v1alpha1';
import { shortURLAPIv1alpha1 } from 'app/api/clients/shorturl/v1alpha1';
import { legacyUserAPI } from 'app/api/legacy/user/api';
import { allReducers as allApiClientReducers } from '@grafana/api-clients/rtkq';
import sharedReducers from 'app/core/reducers';
import ldapReducers from 'app/features/admin/state/reducers';
import alertingReducers from 'app/features/alerting/state/reducers';
@@ -20,7 +17,6 @@ import exploreReducers from 'app/features/explore/state/main';
import foldersReducers from 'app/features/folders/state/reducers';
import invitesReducers from 'app/features/invites/state/reducers';
import importDashboardReducers from 'app/features/manage-dashboards/state/reducers';
import { cloudMigrationAPI } from 'app/features/migrate-to-cloud/api';
import organizationReducers from 'app/features/org/state/reducers';
import panelsReducers from 'app/features/panel/state/reducers';
import { reducer as pluginsReducer } from 'app/features/plugins/admin/state/reducer';
@@ -31,16 +27,8 @@ import teamsReducers from 'app/features/teams/state/reducers';
import usersReducers from 'app/features/users/state/reducers';
import templatingReducers from 'app/features/variables/state/keyedVariablesReducer';
import { advisorAPIv0alpha1 } from '../../api/clients/advisor/v0alpha1';
import { folderAPIv1beta1 } from '../../api/clients/folder/v1beta1';
import { iamAPIv0alpha1 } from '../../api/clients/iam/v0alpha1';
import { playlistAPIv0alpha1 } from '../../api/clients/playlist/v0alpha1';
import { provisioningAPIv0alpha1 } from '../../api/clients/provisioning/v0alpha1';
import { alertingApi } from '../../features/alerting/unified/api/alertingApi';
import { userPreferencesAPI } from '../../features/preferences/api';
import { cleanUpAction } from '../actions/cleanUp';
// Used by the API client generator
// PLOP_INJECT_IMPORT
const rootReducers = {
...sharedReducers,
@@ -65,23 +53,11 @@ const rootReducers = {
...authConfigReducers,
plugins: pluginsReducer,
[alertingApi.reducerPath]: alertingApi.reducer,
[legacyUserAPI.reducerPath]: legacyUserAPI.reducer,
[notificationsAPIv0alpha1.reducerPath]: notificationsAPIv0alpha1.reducer,
[rulesAPIv0alpha1.reducerPath]: rulesAPIv0alpha1.reducer,
[publicDashboardApi.reducerPath]: publicDashboardApi.reducer,
[browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer,
[cloudMigrationAPI.reducerPath]: cloudMigrationAPI.reducer,
[iamAPIv0alpha1.reducerPath]: iamAPIv0alpha1.reducer,
[playlistAPIv0alpha1.reducerPath]: playlistAPIv0alpha1.reducer,
[userPreferencesAPI.reducerPath]: userPreferencesAPI.reducer,
[provisioningAPIv0alpha1.reducerPath]: provisioningAPIv0alpha1.reducer,
[folderAPIv1beta1.reducerPath]: folderAPIv1beta1.reducer,
[advisorAPIv0alpha1.reducerPath]: advisorAPIv0alpha1.reducer,
[dashboardAPIv0alpha1.reducerPath]: dashboardAPIv0alpha1.reducer,
[shortURLAPIv1alpha1.reducerPath]: shortURLAPIv1alpha1.reducer,
[preferencesAPIv1alpha1.reducerPath]: preferencesAPIv1alpha1.reducer,
// PLOP_INJECT_REDUCER
// Used by the API client generator
...allApiClientReducers,
};
const addedReducers = {};
+1 -1
View File
@@ -1,5 +1,6 @@
import memoizeOne from 'memoize-one';
import { generatedAPI } from '@grafana/api-clients/rtkq/shorturl/v1alpha1';
import { AbsoluteTimeRange, LogRowModel, UrlQueryMap } from '@grafana/data';
import { t } from '@grafana/i18n';
import { getBackendSrv, config, locationService } from '@grafana/runtime';
@@ -11,7 +12,6 @@ import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboard
import { dispatch } from 'app/store/store';
import { ShortURL } from '../../../../apps/shorturl/plugin/src/generated/shorturl/v1alpha1/shorturl_object_gen';
import { generatedAPI } from '../../api/clients/shorturl/v1alpha1/endpoints.gen';
import { extractErrorMessage } from '../../api/utils';
import { ShareLinkConfiguration } from '../../features/dashboard-scene/sharing/ShareButton/utils';
@@ -1,13 +1,14 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { handleRequestError } from '@grafana/api-clients';
import { createBaseQuery } from '@grafana/api-clients/rtkq';
import { generatedAPI as legacyUserAPI } from '@grafana/api-clients/rtkq/legacy/user';
import { AppEvents, locationUtil } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { isProvisionedFolderCheck } from 'app/api/clients/folder/v1beta1/utils';
import { createBaseQuery, handleRequestError } from 'app/api/createBaseQuery';
import { legacyUserAPI } from 'app/api/legacy/user/api';
import appEvents from 'app/core/app_events';
import { contextSrv } from 'app/core/core';
import { setStarred } from 'app/core/reducers/navBarTree';
@@ -1,8 +1,8 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from '@grafana/api-clients/rtkq';
import { t } from '@grafana/i18n';
import { config, FetchError, isFetchError } from '@grafana/runtime';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import {
@@ -1,8 +0,0 @@
## Migrate to Cloud API
The [`endpoints.gen.ts`](./endpoints.gen.ts) file is machine generated. In order to update it, follow these steps:
- Run: `make swagger-clean && make openapi3-gen`
- Run: `yarn generate-apis`
If you run into issues, try updating your Node.js version.
+19 -107
View File
@@ -1,113 +1,25 @@
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query';
import { handleRequestError } from '@grafana/api-clients';
import { generatedAPI } from '@grafana/api-clients/rtkq/legacy/migrate-to-cloud';
import { getLocalPlugins } from 'app/features/plugins/admin/api';
import { LocalPlugin } from 'app/features/plugins/admin/types';
import { handleRequestError } from '../../../api/createBaseQuery';
import { generatedAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const cloudMigrationAPI = generatedAPI
.injectEndpoints({
endpoints: (build) => ({
// Manually written because the Swagger specifications for the plugins endpoint do not exist
getLocalPluginList: build.query<LocalPlugin[], void>({
queryFn: async () => {
try {
const list = await getLocalPlugins();
return { data: list };
} catch (error) {
return handleRequestError(error);
}
},
}),
export const cloudMigrationAPI = generatedAPI.injectEndpoints({
endpoints: (build) => ({
// Manually written because the Swagger specifications for the plugins endpoint do not exist
getLocalPluginList: build.query<LocalPlugin[], void>({
queryFn: async () => {
try {
const list = await getLocalPlugins();
return { data: list };
} catch (error) {
return handleRequestError(error);
}
},
}),
})
.enhanceEndpoints({
addTagTypes: [
'cloud-migration-token',
'cloud-migration-session',
'cloud-migration-snapshot',
'cloud-migration-resource-dependencies',
],
endpoints: {
// Cloud-side - create token
getCloudMigrationToken: {
providesTags: ['cloud-migration-token'],
},
createCloudMigrationToken: {
invalidatesTags: ['cloud-migration-token'],
},
deleteCloudMigrationToken: {
invalidatesTags: ['cloud-migration-token'],
},
// On-prem session management (entering token)
getSessionList: {
providesTags: ['cloud-migration-session'] /* should this be a -list? */,
},
getSession: {
providesTags: ['cloud-migration-session'],
},
createSession: {
invalidatesTags: ['cloud-migration-session'],
},
deleteSession: {
invalidatesTags: ['cloud-migration-session', 'cloud-migration-snapshot'],
},
// Snapshot management
getShapshotList: {
providesTags: ['cloud-migration-snapshot'],
},
getSnapshot: {
providesTags: ['cloud-migration-snapshot'],
},
createSnapshot: {
invalidatesTags: ['cloud-migration-snapshot'],
},
uploadSnapshot: {
invalidatesTags: ['cloud-migration-snapshot'],
},
// Resource dependencies
getResourceDependencies: {
providesTags: ['cloud-migration-resource-dependencies'],
},
getDashboardByUid: suppressErrorsOnQuery,
getLibraryElementByUid: suppressErrorsOnQuery,
getLocalPluginList: suppressErrorsOnQuery,
},
});
function suppressErrorsOnQuery<
QueryArg,
BaseQuery extends BaseQueryFn,
TagTypes extends string,
ResultType,
ReducerPath extends string,
PageParam,
>(endpoint: EndpointDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath, PageParam>) {
if (!endpoint.query) {
return;
}
// internal type from rtk-query that isn't exported
type InfiniteQueryCombinedArg<QueryArg, PageParam> = {
queryArg: QueryArg;
pageParam: PageParam;
};
const originalQuery = endpoint.query;
endpoint.query = (arg: QueryArg & InfiniteQueryCombinedArg<QueryArg, PageParam>) => {
const baseQuery = originalQuery(arg);
baseQuery.showErrorAlert = false;
return baseQuery;
};
}
}),
});
export const { useGetLocalPluginListQuery } = cloudMigrationAPI;
// eslint-disable-next-line no-barrel-files/no-barrel-files
export * from '@grafana/api-clients/rtkq/legacy/migrate-to-cloud';
@@ -1,14 +1,14 @@
import { useCallback, useState } from 'react';
import { Trans, t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { Box, Button, Text } from '@grafana/ui';
import {
useCreateCloudMigrationTokenMutation,
useDeleteCloudMigrationTokenMutation,
useGetCloudMigrationTokenQuery,
} from '../../api';
} from '@grafana/api-clients/rtkq/legacy/migrate-to-cloud';
import { Trans, t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { Box, Button, Text } from '@grafana/ui';
import { maybeAPIError } from '../../api/errors';
import { TokenErrorAlert } from '../TokenErrorAlert';
@@ -2,8 +2,7 @@ import { useNavigate } from 'react-router-dom-v5-compat';
import { t, Trans } from '@grafana/i18n';
import { Button, Dropdown, Icon, Menu, Stack } from '@grafana/ui';
import { Repository } from 'app/api/clients/provisioning/v0alpha1';
import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1/endpoints.gen';
import { useGetFrontendSettingsQuery, Repository } from 'app/api/clients/provisioning/v0alpha1';
import { CONNECT_URL, DEFAULT_REPOSITORY_TYPES } from '../constants';
import { checkSyncSettings } from '../utils/checkSyncSettings';
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { Trans } from '@grafana/i18n';
import { Card, Stack, Text, useStyles2 } from '@grafana/ui';
import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1/endpoints.gen';
import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1';
import { CONNECT_URL } from '../constants';
import { getOrderedRepositoryConfigs } from '../utils/repositoryTypes';
@@ -1,14 +1,10 @@
import { getAPIBaseURL } from '@grafana/api-clients';
import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data';
import { getBackendSrv, config } from '@grafana/runtime';
import { getAPINamespace } from '../../api/utils';
import { ScopeNavigation } from './dashboards/types';
const apiGroup = 'scope.grafana.app';
const apiVersion = 'v0alpha1';
const apiNamespace = getAPINamespace();
const apiUrl = `/apis/${apiGroup}/${apiVersion}/namespaces/${apiNamespace}`;
const apiUrl = getAPIBaseURL('scope.grafana.app', 'v0alpha1');
export class ScopesApiClient {
async fetchScope(name: string): Promise<Scope | undefined> {
@@ -1,10 +1,11 @@
import { isEmpty } from 'lodash';
import { BASE_URL as v0alphaBaseURL } from '@grafana/api-clients/rtkq/dashboard/v0alpha1';
import { generatedAPI as legacyUserAPI } from '@grafana/api-clients/rtkq/legacy/user';
import { DataFrame, DataFrameView, getDisplayProcessor, SelectableValue, toDataFrame } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config, getBackendSrv } from '@grafana/runtime';
import { generatedAPI, ListStarsApiResponse } from 'app/api/clients/preferences/v1alpha1';
import { legacyUserAPI } from 'app/api/legacy/user/api';
import { getAPIBaseURL } from 'app/api/utils';
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
import { contextSrv } from 'app/core/core';
@@ -26,8 +27,7 @@ import { filterSearchResults, replaceCurrentFolderQuery } from './utils';
// and that it can not serve any search requests. We are temporarily using the old SQL Search API as a fallback when that happens.
const loadingFrameName = 'Loading';
const baseURL = getAPIBaseURL('dashboard.grafana.app', 'v0alpha1');
const searchURI = `${baseURL}/search`;
const searchURI = `${v0alphaBaseURL}/search`;
export type SearchHit = {
resource: string; // dashboards | folders
+7 -7
View File
@@ -1,14 +1,14 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useMemo } from 'react';
import {
useGetStarsQuery as useLegacyGetStarsQuery,
useStarDashboardByUidMutation as useLegacyStarDashboardMutation,
useUnstarDashboardByUidMutation as useLegacyUnstarDashboardMutation,
} from '@grafana/api-clients/rtkq/legacy/user';
import { locationUtil } from '@grafana/data';
import { config } from '@grafana/runtime';
import { useAddStarMutation, useRemoveStarMutation, useListStarsQuery } from 'app/api/clients/preferences/v1alpha1';
import {
useGetStarsQuery as useLegacyGetStarsQuery,
useStarDashboardMutation as useLegacyStarDashboardMutation,
useUnstarDashboardMutation as useLegacyUnstarDashboardMutation,
} from 'app/api/legacy/user/api';
import { contextSrv } from 'app/core/core';
import { setStarred } from 'app/core/reducers/navBarTree';
import { dispatch } from 'app/store/store';
@@ -45,9 +45,9 @@ export const useStarItem = (group: string, kind: string) => {
return async ({ id, title }: StarItemArgs, newStarredState: boolean) => {
if (newStarredState) {
await addStarLegacy({ id });
await addStarLegacy({ dashboardUid: id });
} else {
await removeStarLegacy({ id });
await removeStarLegacy({ dashboardUid: id });
}
updateStarred({ id, title }, newStarredState);
+2 -26
View File
@@ -3,23 +3,11 @@ import { setupListeners } from '@reduxjs/toolkit/query';
import { Middleware } from 'redux';
import { notificationsAPIv0alpha1, rulesAPIv0alpha1 } from '@grafana/alerting/unstable';
import { dashboardAPIv0alpha1 } from 'app/api/clients/dashboard/v0alpha1';
import { preferencesAPIv1alpha1 } from 'app/api/clients/preferences/v1alpha1';
import { shortURLAPIv1alpha1 } from 'app/api/clients/shorturl/v1alpha1';
import { legacyUserAPI } from 'app/api/legacy/user/api';
import { allMiddleware as allApiClientMiddleware } from '@grafana/api-clients/rtkq';
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';
import { userPreferencesAPI } from 'app/features/preferences/api';
import { StoreState } from 'app/types/store';
import { advisorAPIv0alpha1 } from '../api/clients/advisor/v0alpha1';
import { folderAPIv1beta1 } from '../api/clients/folder/v1beta1';
import { iamAPIv0alpha1 } from '../api/clients/iam/v0alpha1';
import { playlistAPIv0alpha1 } from '../api/clients/playlist/v0alpha1';
import { provisioningAPIv0alpha1 } from '../api/clients/provisioning/v0alpha1';
// Used by the API client generator
// PLOP_INJECT_IMPORT
import { buildInitialState } from '../core/reducers/navModel';
import { addReducer, createRootReducer } from '../core/reducers/root';
import { alertingApi } from '../features/alerting/unified/api/alertingApi';
@@ -54,19 +42,7 @@ export function configureStore(initialState?: Partial<StoreState>) {
// other Grafana core APIs
publicDashboardApi.middleware,
browseDashboardsAPI.middleware,
legacyUserAPI.middleware,
cloudMigrationAPI.middleware,
userPreferencesAPI.middleware,
iamAPIv0alpha1.middleware,
playlistAPIv0alpha1.middleware,
provisioningAPIv0alpha1.middleware,
folderAPIv1beta1.middleware,
advisorAPIv0alpha1.middleware,
dashboardAPIv0alpha1.middleware,
shortURLAPIv1alpha1.middleware,
preferencesAPIv1alpha1.middleware,
// PLOP_INJECT_MIDDLEWARE
// Used by the API client generator
...allApiClientMiddleware,
...extraMiddleware
),
devTools: process.env.NODE_ENV !== 'production',
-116
View File
@@ -1,116 +0,0 @@
// Generates Redux Toolkit API slices for certain APIs from the OpenAPI spec
import type { ConfigFile } from '@rtk-query/codegen-openapi';
const config: ConfigFile = {
schemaFile: '', // leave this empty, and instead populate the outputFiles object below
apiFile: '', // leave this empty, and instead populate the outputFiles object below
exportName: 'generatedAPI',
outputFiles: {
'../public/app/features/migrate-to-cloud/api/endpoints.gen.ts': {
schemaFile: '../public/openapi3.json',
apiFile: '../public/app/features/migrate-to-cloud/api/baseAPI.ts',
apiImport: 'baseAPI',
hooks: true,
filterEndpoints: [
'getSessionList',
'getSession',
'deleteSession',
'createSession',
'getShapshotList',
'getSnapshot',
'uploadSnapshot',
'createSnapshot',
'cancelSnapshot',
'createCloudMigrationToken',
'deleteCloudMigrationToken',
'getCloudMigrationToken',
'getDashboardByUid',
'getLibraryElementByUid',
'getResourceDependencies',
],
},
'../public/app/features/preferences/api/user/endpoints.gen.ts': {
schemaFile: '../public/openapi3.json',
hooks: true,
apiFile: '../public/app/features/preferences/api/user/baseAPI.ts',
apiImport: 'baseAPI',
filterEndpoints: ['getUserPreferences', 'updateUserPreferences', 'patchUserPreferences'],
},
'../public/app/api/clients/iam/v0alpha1/endpoints.gen.ts': {
schemaFile: '../data/openapi/iam.grafana.app-v0alpha1.json',
apiFile: '../public/app/api/clients/iam/v0alpha1/baseAPI.ts',
filterEndpoints: ['getDisplayMapping'],
tag: true,
},
'../public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts': {
apiFile: '../public/app/api/clients/provisioning/v0alpha1/baseAPI.ts',
schemaFile: '../data/openapi/provisioning.grafana.app-v0alpha1.json',
filterEndpoints,
tag: true,
hooks: true,
},
'../public/app/api/clients/folder/v1beta1/endpoints.gen.ts': {
apiFile: '../public/app/api/clients/folder/v1beta1/baseAPI.ts',
schemaFile: '../data/openapi/folder.grafana.app-v1beta1.json',
tag: true,
},
'../public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts': {
apiFile: '../public/app/api/clients/advisor/v0alpha1/baseAPI.ts',
schemaFile: '../data/openapi/advisor.grafana.app-v0alpha1.json',
filterEndpoints: [
'createCheck',
'getCheck',
'listCheck',
'deleteCheck',
'updateCheck',
'listCheckType',
'updateCheckType',
],
tag: true,
},
'../public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts': {
apiFile: '../public/app/api/clients/playlist/v0alpha1/baseAPI.ts',
schemaFile: '../data/openapi/playlist.grafana.app-v0alpha1.json',
filterEndpoints: ['listPlaylist', 'getPlaylist', 'createPlaylist', 'deletePlaylist', 'replacePlaylist'],
tag: true,
},
'../public/app/api/clients/dashboard/v0alpha1/endpoints.gen.ts': {
apiFile: '../public/app/api/clients/dashboard/v0alpha1/baseAPI.ts',
schemaFile: '../data/openapi/dashboard.grafana.app-v0alpha1.json',
filterEndpoints: [
// Do not use any other endpoints from this version
// If other endpoints are required, they must be used from a newer version of the dashboard API
'getSearch',
],
tag: true,
},
'../public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts': {
apiFile: '../public/app/api/clients/shorturl/v1alpha1/baseAPI.ts',
schemaFile: '../data/openapi/shorturl.grafana.app-v1alpha1.json',
tag: true,
},
'../public/app/api/clients/correlations/v0alpha1/endpoints.gen.ts': {
apiFile: '../public/app/api/clients/correlations/v0alpha1/baseAPI.ts',
schemaFile: '../data/openapi/correlations.grafana.app-v0alpha1.json',
tag: true,
},
'../public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts': {
apiFile: '../public/app/api/clients/preferences/v1alpha1/baseAPI.ts',
schemaFile: '../data/openapi/preferences.grafana.app-v1alpha1.json',
tag: true,
hooks: true,
},
// PLOP_INJECT_API_CLIENT - Used by the API client generator
},
};
function filterEndpoints(name: string) {
return !name.toLowerCase().includes('getapiresources') && !name.toLowerCase().includes('update');
}
export default config;
@@ -1,14 +0,0 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('{{group}}', '{{version}}');
export const api = createApi({
reducerPath: '{{reducerPath}}',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
}),
endpoints: () => ({}),
});

Some files were not shown because too many files have changed in this diff Show More