diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 29dc61c4ff1..7d0a50363e5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/eslint-suppressions.json b/eslint-suppressions.json index a614933c49b..e9f141df030 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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 diff --git a/package.json b/package.json index b97991fb13e..df96a8b37a2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/README.md b/packages/README.md index 525d77097fb..b42f7082c5c 100644 --- a/packages/README.md +++ b/packages/README.md @@ -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- (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 diff --git a/packages/grafana-api-clients/CHANGELOG.md b/packages/grafana-api-clients/CHANGELOG.md new file mode 100644 index 00000000000..10f3c74e20d --- /dev/null +++ b/packages/grafana-api-clients/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## Unreleased + +- Initial scaffold diff --git a/packages/grafana-api-clients/LICENSE_APACHE2 b/packages/grafana-api-clients/LICENSE_APACHE2 new file mode 100644 index 00000000000..373dde574a0 --- /dev/null +++ b/packages/grafana-api-clients/LICENSE_APACHE2 @@ -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. diff --git a/packages/grafana-api-clients/README.md b/packages/grafana-api-clients/README.md new file mode 100644 index 00000000000..dfc2680926c --- /dev/null +++ b/packages/grafana-api-clients/README.md @@ -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. diff --git a/packages/grafana-api-clients/package.json b/packages/grafana-api-clients/package.json new file mode 100644 index 00000000000..3d645bfb2b3 --- /dev/null +++ b/packages/grafana-api-clients/package.json @@ -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" + } +} diff --git a/packages/grafana-api-clients/project.json b/packages/grafana-api-clients/project.json new file mode 100644 index 00000000000..7e026de5f8a --- /dev/null +++ b/packages/grafana-api-clients/project.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "tags": ["scope:package", "type:runtime"], + "targets": { + "build": {} + } +} diff --git a/packages/grafana-api-clients/rollup.config.ts b/packages/grafana-api-clients/rollup.config.ts new file mode 100644 index 00000000000..b36d59278fc --- /dev/null +++ b/packages/grafana-api-clients/rollup.config.ts @@ -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, +]; diff --git a/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/baseAPI.ts new file mode 100644 index 00000000000..de77960c787 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts similarity index 100% rename from public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts diff --git a/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/index.ts new file mode 100644 index 00000000000..d80fd6d553a --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/index.ts @@ -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({}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/baseAPI.ts new file mode 100644 index 00000000000..04c2b5ddb05 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/public/app/api/clients/correlations/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts similarity index 100% rename from public/app/api/clients/correlations/v0alpha1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts diff --git a/public/app/api/clients/correlations/v0alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/index.ts similarity index 76% rename from public/app/api/clients/correlations/v0alpha1/index.ts rename to packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/index.ts index 02dd06a3ee2..d7922f7f8b0 100644 --- a/public/app/api/clients/correlations/v0alpha1/index.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/index.ts @@ -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; diff --git a/public/app/api/createBaseQuery.ts b/packages/grafana-api-clients/src/clients/rtkq/createBaseQuery.ts similarity index 81% rename from public/app/api/createBaseQuery.ts rename to packages/grafana-api-clients/src/clients/rtkq/createBaseQuery.ts index 51037493e88..477e139faac 100644 --- a/public/app/api/createBaseQuery.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/createBaseQuery.ts @@ -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') }; - } -} diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/baseAPI.ts new file mode 100644 index 00000000000..e6673980326 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/public/app/api/clients/dashboard/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts similarity index 100% rename from public/app/api/clients/dashboard/v0alpha1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/index.ts new file mode 100644 index 00000000000..c0209dacc18 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/index.ts @@ -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'], +}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/baseAPI.ts new file mode 100644 index 00000000000..8e4b08040ef --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/public/app/api/clients/folder/v1beta1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/endpoints.gen.ts similarity index 100% rename from public/app/api/clients/folder/v1beta1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/endpoints.gen.ts diff --git a/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/index.ts new file mode 100644 index 00000000000..d80fd6d553a --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/index.ts @@ -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({}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/baseAPI.ts new file mode 100644 index 00000000000..5806ea071e6 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/public/app/api/clients/iam/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts similarity index 100% rename from public/app/api/clients/iam/v0alpha1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts diff --git a/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/index.ts new file mode 100644 index 00000000000..d80fd6d553a --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/index.ts @@ -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({}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/index.ts b/packages/grafana-api-clients/src/clients/rtkq/index.ts new file mode 100644 index 00000000000..77c02e78170 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/index.ts @@ -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'; diff --git a/public/app/features/migrate-to-cloud/api/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/baseAPI.ts similarity index 66% rename from public/app/features/migrate-to-cloud/api/baseAPI.ts rename to packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/baseAPI.ts index 60c18dfd3e3..08cabc4ad93 100644 --- a/public/app/features/migrate-to-cloud/api/baseAPI.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/baseAPI.ts @@ -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: () => ({}), diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/endpoints.gen.ts similarity index 99% rename from public/app/features/migrate-to-cloud/api/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/endpoints.gen.ts index 339184f1635..87bcb349ecd 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/endpoints.gen.ts @@ -1,4 +1,4 @@ -import { baseAPI as api } from './baseAPI'; +import { api } from './baseAPI'; const injectedRtkApi = api.injectEndpoints({ endpoints: (build) => ({ getSessionList: build.query({ diff --git a/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/index.ts b/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/index.ts new file mode 100644 index 00000000000..d11162b3c54 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/index.ts @@ -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) { + if (!endpoint.query) { + return; + } + + // internal type from rtk-query that isn't exported + type InfiniteQueryCombinedArg = { + queryArg: QueryArg; + pageParam: PageParam; + }; + + const originalQuery = endpoint.query; + endpoint.query = (arg: QueryArg & InfiniteQueryCombinedArg) => { + const baseQuery = originalQuery(arg); + baseQuery.showErrorAlert = false; + return baseQuery; + }; +} + +export * from './endpoints.gen'; diff --git a/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/baseAPI.ts new file mode 100644 index 00000000000..f013d4b7492 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/endpoints.gen.ts similarity index 100% rename from public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/endpoints.gen.ts diff --git a/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/index.ts new file mode 100644 index 00000000000..d80fd6d553a --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/index.ts @@ -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({}); diff --git a/public/app/features/preferences/api/user/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/preferences/user/baseAPI.ts similarity index 65% rename from public/app/features/preferences/api/user/baseAPI.ts rename to packages/grafana-api-clients/src/clients/rtkq/preferences/user/baseAPI.ts index 33316419f6e..397ef5c8ae3 100644 --- a/public/app/features/preferences/api/user/baseAPI.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/preferences/user/baseAPI.ts @@ -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: () => ({}), diff --git a/public/app/features/preferences/api/user/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/preferences/user/endpoints.gen.ts similarity index 98% rename from public/app/features/preferences/api/user/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/preferences/user/endpoints.gen.ts index c2a90d33821..60162b33858 100644 --- a/public/app/features/preferences/api/user/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/preferences/user/endpoints.gen.ts @@ -1,4 +1,4 @@ -import { baseAPI as api } from './baseAPI'; +import { api } from './baseAPI'; const injectedRtkApi = api.injectEndpoints({ endpoints: (build) => ({ getUserPreferences: build.query({ diff --git a/public/app/features/preferences/api/index.ts b/packages/grafana-api-clients/src/clients/rtkq/preferences/user/index.ts similarity index 54% rename from public/app/features/preferences/api/index.ts rename to packages/grafana-api-clients/src/clients/rtkq/preferences/user/index.ts index 1da53081140..d6e1d0fb2ef 100644 --- a/public/app/features/preferences/api/index.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/preferences/user/index.ts @@ -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'; diff --git a/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/baseAPI.ts new file mode 100644 index 00000000000..6ec37437782 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts similarity index 100% rename from public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts diff --git a/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/index.ts new file mode 100644 index 00000000000..d80fd6d553a --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/index.ts @@ -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({}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/baseAPI.ts new file mode 100644 index 00000000000..58ed0204997 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts similarity index 100% rename from public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts diff --git a/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/index.ts new file mode 100644 index 00000000000..d80fd6d553a --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/index.ts @@ -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({}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/baseAPI.ts new file mode 100644 index 00000000000..4d1f28bd5d8 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/endpoints.gen.ts similarity index 100% rename from public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/endpoints.gen.ts diff --git a/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/index.ts new file mode 100644 index 00000000000..d80fd6d553a --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/index.ts @@ -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({}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/user/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/user/baseAPI.ts new file mode 100644 index 00000000000..99baa6a5498 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/user/baseAPI.ts @@ -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: () => ({}), +}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/user/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/user/endpoints.gen.ts new file mode 100644 index 00000000000..c3a4e1e03bf --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/user/endpoints.gen.ts @@ -0,0 +1,37 @@ +import { api } from './baseAPI'; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + unstarDashboardByUid: build.mutation({ + query: (queryArg) => ({ url: `/user/stars/dashboard/uid/${queryArg.dashboardUid}`, method: 'DELETE' }), + }), + starDashboardByUid: build.mutation({ + 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; diff --git a/packages/grafana-api-clients/src/clients/rtkq/user/index.ts b/packages/grafana-api-clients/src/clients/rtkq/user/index.ts new file mode 100644 index 00000000000..b977c002dde --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/user/index.ts @@ -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({ + query: () => ({ url: '/user/stars' }), + providesTags: ['dashboardStars'], + }), + }), + }); + +export const { useGetStarsQuery } = generatedAPI; +export * from './endpoints.gen'; diff --git a/scripts/rtk-client-generator/README.md b/packages/grafana-api-clients/src/generator/README.md similarity index 93% rename from scripts/rtk-client-generator/README.md rename to packages/grafana-api-clients/src/generator/README.md index 8e41f0def0f..b0598b9d005 100644 --- a/scripts/rtk-client-generator/README.md +++ b/packages/grafana-api-clients/src/generator/README.md @@ -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 diff --git a/scripts/rtk-client-generator/helpers.ts b/packages/grafana-api-clients/src/generator/helpers.ts similarity index 56% rename from scripts/rtk-client-generator/helpers.ts rename to packages/grafana-api-clients/src/generator/helpers.ts index 549b121873c..2ac2ce01389 100644 --- a/scripts/rtk-client-generator/helpers.ts +++ b/packages/grafana-api-clients/src/generator/helpers.ts @@ -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); + }) || [] + ); +}; diff --git a/scripts/rtk-client-generator/plopfile.ts b/packages/grafana-api-clients/src/generator/plopfile.ts similarity index 66% rename from scripts/rtk-client-generator/plopfile.ts rename to packages/grafana-api-clients/src/generator/plopfile.ts index 44688c46fd5..ed3953bfdc7 100644 --- a/scripts/rtk-client-generator/plopfile.ts +++ b/packages/grafana-api-clients/src/generator/plopfile.ts @@ -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) { diff --git a/packages/grafana-api-clients/src/generator/templates/baseAPI.ts.hbs b/packages/grafana-api-clients/src/generator/templates/baseAPI.ts.hbs new file mode 100644 index 00000000000..5e84f4ad9b8 --- /dev/null +++ b/packages/grafana-api-clients/src/generator/templates/baseAPI.ts.hbs @@ -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: () => ({}), +}); diff --git a/packages/grafana-api-clients/src/generator/templates/config-entry.hbs b/packages/grafana-api-clients/src/generator/templates/config-entry.hbs new file mode 100644 index 00000000000..bdf1f6f7ded --- /dev/null +++ b/packages/grafana-api-clients/src/generator/templates/config-entry.hbs @@ -0,0 +1,2 @@ +...createAPIConfig('{{groupName}}', '{{version}}'{{#if endpoints}}, [{{{formatEndpoints endpoints}}}]{{/if}}), +// PLOP_INJECT_API_CLIENT - Used by the API client generator diff --git a/packages/grafana-api-clients/src/generator/templates/index.ts.hbs b/packages/grafana-api-clients/src/generator/templates/index.ts.hbs new file mode 100644 index 00000000000..d80fd6d553a --- /dev/null +++ b/packages/grafana-api-clients/src/generator/templates/index.ts.hbs @@ -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({}); diff --git a/scripts/rtk-client-generator/types.ts b/packages/grafana-api-clients/src/generator/types.ts similarity index 68% rename from scripts/rtk-client-generator/types.ts rename to packages/grafana-api-clients/src/generator/types.ts index 0eb73b6ab90..860332f8c76 100644 --- a/scripts/rtk-client-generator/types.ts +++ b/packages/grafana-api-clients/src/generator/types.ts @@ -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; diff --git a/packages/grafana-api-clients/src/index.ts b/packages/grafana-api-clients/src/index.ts new file mode 100644 index 00000000000..96e34be4f87 --- /dev/null +++ b/packages/grafana-api-clients/src/index.ts @@ -0,0 +1 @@ +export { getAPINamespace, getAPIBaseURL, normalizeError, handleRequestError } from './utils/utils'; diff --git a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts new file mode 100644 index 00000000000..82e1339f9bd --- /dev/null +++ b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts @@ -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; diff --git a/scripts/process-specs.ts b/packages/grafana-api-clients/src/scripts/process-specs.ts similarity index 94% rename from scripts/process-specs.ts rename to packages/grafana-api-clients/src/scripts/process-specs.ts index 8ebd24705de..f3de40bf6ed 100644 --- a/scripts/process-specs.ts +++ b/packages/grafana-api-clients/src/scripts/process-specs.ts @@ -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); diff --git a/packages/grafana-api-clients/src/utils/utils.ts b/packages/grafana-api-clients/src/utils/utils.ts new file mode 100644 index 00000000000..48086cc3333 --- /dev/null +++ b/packages/grafana-api-clients/src/utils/utils.ts @@ -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') }; + } +} diff --git a/packages/grafana-api-clients/tsconfig.build.json b/packages/grafana-api-clients/tsconfig.build.json new file mode 100644 index 00000000000..82391831633 --- /dev/null +++ b/packages/grafana-api-clients/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["dist", "node_modules", "**/*.test.ts*"] +} diff --git a/packages/grafana-api-clients/tsconfig.json b/packages/grafana-api-clients/tsconfig.json new file mode 100644 index 00000000000..ed1c294703e --- /dev/null +++ b/packages/grafana-api-clients/tsconfig.json @@ -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*" + ] +} diff --git a/public/app/api/README.md b/public/app/api/README.md deleted file mode 100644 index 67bf87ae19a..00000000000 --- a/public/app/api/README.md +++ /dev/null @@ -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. -
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](./generator/README.md) for details. diff --git a/public/app/api/clients/advisor/v0alpha1/baseAPI.ts b/public/app/api/clients/advisor/v0alpha1/baseAPI.ts deleted file mode 100644 index 2844f269cf0..00000000000 --- a/public/app/api/clients/advisor/v0alpha1/baseAPI.ts +++ /dev/null @@ -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: () => ({}), -}); diff --git a/public/app/api/clients/advisor/v0alpha1/index.ts b/public/app/api/clients/advisor/v0alpha1/index.ts deleted file mode 100644 index b659d3adb4c..00000000000 --- a/public/app/api/clients/advisor/v0alpha1/index.ts +++ /dev/null @@ -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 diff --git a/public/app/api/clients/correlations/v0alpha1/baseAPI.ts b/public/app/api/clients/correlations/v0alpha1/baseAPI.ts deleted file mode 100644 index 8ded2f34c02..00000000000 --- a/public/app/api/clients/correlations/v0alpha1/baseAPI.ts +++ /dev/null @@ -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: () => ({}), -}); diff --git a/public/app/api/clients/dashboard/v0alpha1/baseAPI.ts b/public/app/api/clients/dashboard/v0alpha1/baseAPI.ts deleted file mode 100644 index dca53563fa0..00000000000 --- a/public/app/api/clients/dashboard/v0alpha1/baseAPI.ts +++ /dev/null @@ -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: () => ({}), -}); diff --git a/public/app/api/clients/dashboard/v0alpha1/index.ts b/public/app/api/clients/dashboard/v0alpha1/index.ts index 5cf86831590..02cbd74b8ed 100644 --- a/public/app/api/clients/dashboard/v0alpha1/index.ts +++ b/public/app/api/clients/dashboard/v0alpha1/index.ts @@ -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; diff --git a/public/app/api/clients/folder/v1beta1/baseAPI.ts b/public/app/api/clients/folder/v1beta1/baseAPI.ts deleted file mode 100644 index 3caca81a027..00000000000 --- a/public/app/api/clients/folder/v1beta1/baseAPI.ts +++ /dev/null @@ -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: () => ({}), -}); diff --git a/public/app/api/clients/folder/v1beta1/index.ts b/public/app/api/clients/folder/v1beta1/index.ts index bb0e11f218f..d701554cce7 100644 --- a/public/app/api/clients/folder/v1beta1/index.ts +++ b/public/app/api/clients/folder/v1beta1/index.ts @@ -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'; diff --git a/public/app/api/clients/folder/v1beta1/utils.ts b/public/app/api/clients/folder/v1beta1/utils.ts index 27655554361..0944bd12e7e 100644 --- a/public/app/api/clients/folder/v1beta1/utils.ts +++ b/public/app/api/clients/folder/v1beta1/utils.ts @@ -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( diff --git a/public/app/api/clients/iam/v0alpha1/baseAPI.ts b/public/app/api/clients/iam/v0alpha1/baseAPI.ts deleted file mode 100644 index ae67ceffb7d..00000000000 --- a/public/app/api/clients/iam/v0alpha1/baseAPI.ts +++ /dev/null @@ -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: () => ({}), -}); diff --git a/public/app/api/clients/iam/v0alpha1/index.ts b/public/app/api/clients/iam/v0alpha1/index.ts index 1dd8829e828..3f9e7b1fb6d 100644 --- a/public/app/api/clients/iam/v0alpha1/index.ts +++ b/public/app/api/clients/iam/v0alpha1/index.ts @@ -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; diff --git a/public/app/api/clients/playlist/v0alpha1/baseAPI.ts b/public/app/api/clients/playlist/v0alpha1/baseAPI.ts deleted file mode 100644 index 27cd1f3a325..00000000000 --- a/public/app/api/clients/playlist/v0alpha1/baseAPI.ts +++ /dev/null @@ -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: () => ({}), -}); diff --git a/public/app/api/clients/playlist/v0alpha1/index.ts b/public/app/api/clients/playlist/v0alpha1/index.ts index 3beec79634b..8f264203f8d 100644 --- a/public/app/api/clients/playlist/v0alpha1/index.ts +++ b/public/app/api/clients/playlist/v0alpha1/index.ts @@ -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'; diff --git a/public/app/api/clients/preferences/v1alpha1/baseAPI.ts b/public/app/api/clients/preferences/v1alpha1/baseAPI.ts deleted file mode 100644 index 326e3e772dd..00000000000 --- a/public/app/api/clients/preferences/v1alpha1/baseAPI.ts +++ /dev/null @@ -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: () => ({}), -}); diff --git a/public/app/api/clients/preferences/v1alpha1/index.ts b/public/app/api/clients/preferences/v1alpha1/index.ts index da06eabd3fb..12d87999e8e 100644 --- a/public/app/api/clients/preferences/v1alpha1/index.ts +++ b/public/app/api/clients/preferences/v1alpha1/index.ts @@ -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'; diff --git a/public/app/api/clients/provisioning/utils/getListParams.ts b/public/app/api/clients/provisioning/utils/getListParams.ts index e4005ca866f..ee9a78ec8ae 100644 --- a/public/app/api/clients/provisioning/utils/getListParams.ts +++ b/public/app/api/clients/provisioning/utils/getListParams.ts @@ -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 & Pick; diff --git a/public/app/api/clients/provisioning/v0alpha1/baseAPI.ts b/public/app/api/clients/provisioning/v0alpha1/baseAPI.ts deleted file mode 100644 index b7709ed3b0e..00000000000 --- a/public/app/api/clients/provisioning/v0alpha1/baseAPI.ts +++ /dev/null @@ -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: () => ({}), -}); diff --git a/public/app/api/clients/provisioning/v0alpha1/index.ts b/public/app/api/clients/provisioning/v0alpha1/index.ts index 9cfa13f2033..7f41904cf94 100644 --- a/public/app/api/clients/provisioning/v0alpha1/index.ts +++ b/public/app/api/clients/provisioning/v0alpha1/index.ts @@ -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'; diff --git a/public/app/api/clients/shorturl/v1alpha1/baseAPI.ts b/public/app/api/clients/shorturl/v1alpha1/baseAPI.ts deleted file mode 100644 index 8ded2f34c02..00000000000 --- a/public/app/api/clients/shorturl/v1alpha1/baseAPI.ts +++ /dev/null @@ -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: () => ({}), -}); diff --git a/public/app/api/clients/shorturl/v1alpha1/index.ts b/public/app/api/clients/shorturl/v1alpha1/index.ts index aa7aedd0adf..276fd01f3a8 100644 --- a/public/app/api/clients/shorturl/v1alpha1/index.ts +++ b/public/app/api/clients/shorturl/v1alpha1/index.ts @@ -1,4 +1,4 @@ -import { generatedAPI } from './endpoints.gen'; +import { generatedAPI } from '@grafana/api-clients/rtkq/shorturl/v1alpha1'; export const shortURLAPIv1alpha1 = generatedAPI.enhanceEndpoints({ endpoints: { diff --git a/public/app/api/legacy/user/api.ts b/public/app/api/legacy/user/api.ts deleted file mode 100644 index c7f110db504..00000000000 --- a/public/app/api/legacy/user/api.ts +++ /dev/null @@ -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({ - query: () => ({ url: '/user/stars' }), - providesTags: ['dashboardStars'], - }), - starDashboard: build.mutation({ - query: ({ id }) => ({ url: `/user/stars/dashboard/uid/${id}`, method: 'POST' }), - invalidatesTags: ['dashboardStars'], - }), - unstarDashboard: build.mutation({ - query: ({ id }) => ({ url: `/user/stars/dashboard/uid/${id}`, method: 'DELETE' }), - invalidatesTags: ['dashboardStars'], - }), - }), -}); - -export const { useGetStarsQuery, useStarDashboardMutation, useUnstarDashboardMutation } = legacyUserAPI; diff --git a/public/app/api/utils.ts b/public/app/api/utils.ts index 5242b18fcfb..3866bbe977d 100644 --- a/public/app/api/utils.ts +++ b/public/app/api/utils.ts @@ -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'; diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index 36955e8d405..c2be1a66141 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -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'; diff --git a/public/app/core/components/AppChrome/MegaMenu/hooks.ts b/public/app/core/components/AppChrome/MegaMenu/hooks.ts index db8bb2807f9..62a33dcbd7f 100644 --- a/public/app/core/components/AppChrome/MegaMenu/hooks.ts +++ b/public/app/core/components/AppChrome/MegaMenu/hooks.ts @@ -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'; diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 41e11265c09..890879ca7a2 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -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 = {}; diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index 9872d7f7237..c01988d2262 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -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'; diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index d884e47fe36..313c74d6c33 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -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'; diff --git a/public/app/features/dashboard/api/publicDashboardApi.ts b/public/app/features/dashboard/api/publicDashboardApi.ts index 507a8ee1d9d..59e9f81fef0 100644 --- a/public/app/features/dashboard/api/publicDashboardApi.ts +++ b/public/app/features/dashboard/api/publicDashboardApi.ts @@ -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 { diff --git a/public/app/features/migrate-to-cloud/api/README.md b/public/app/features/migrate-to-cloud/api/README.md deleted file mode 100644 index b26fb9c21b6..00000000000 --- a/public/app/features/migrate-to-cloud/api/README.md +++ /dev/null @@ -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. diff --git a/public/app/features/migrate-to-cloud/api/index.ts b/public/app/features/migrate-to-cloud/api/index.ts index b7087cde2b5..6a89e11b039 100644 --- a/public/app/features/migrate-to-cloud/api/index.ts +++ b/public/app/features/migrate-to-cloud/api/index.ts @@ -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({ - 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({ + 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) { - if (!endpoint.query) { - return; - } - - // internal type from rtk-query that isn't exported - type InfiniteQueryCombinedArg = { - queryArg: QueryArg; - pageParam: PageParam; - }; - - const originalQuery = endpoint.query; - endpoint.query = (arg: QueryArg & InfiniteQueryCombinedArg) => { - 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'; diff --git a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx index a0f31445a2e..09d6b44c58b 100644 --- a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx +++ b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx @@ -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'; diff --git a/public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx b/public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx index cdf80faec33..ba4ebf87d9d 100644 --- a/public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx +++ b/public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx @@ -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'; diff --git a/public/app/features/provisioning/Shared/RepositoryTypeCards.tsx b/public/app/features/provisioning/Shared/RepositoryTypeCards.tsx index 7dc529c070c..9fc36d249c7 100644 --- a/public/app/features/provisioning/Shared/RepositoryTypeCards.tsx +++ b/public/app/features/provisioning/Shared/RepositoryTypeCards.tsx @@ -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'; diff --git a/public/app/features/scopes/ScopesApiClient.ts b/public/app/features/scopes/ScopesApiClient.ts index 5c1a43fd8ec..1b2c9f9d1a9 100644 --- a/public/app/features/scopes/ScopesApiClient.ts +++ b/public/app/features/scopes/ScopesApiClient.ts @@ -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 { diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts index d363c7f2068..7888730d36b 100644 --- a/public/app/features/search/service/unified.ts +++ b/public/app/features/search/service/unified.ts @@ -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 diff --git a/public/app/features/stars/hooks.ts b/public/app/features/stars/hooks.ts index 8b10fa989df..6c0aea29425 100644 --- a/public/app/features/stars/hooks.ts +++ b/public/app/features/stars/hooks.ts @@ -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); diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index ebe79de7cd6..5b2771c879a 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -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) { // 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', diff --git a/scripts/generate-rtk-apis.ts b/scripts/generate-rtk-apis.ts deleted file mode 100644 index 61d73868411..00000000000 --- a/scripts/generate-rtk-apis.ts +++ /dev/null @@ -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; diff --git a/scripts/rtk-client-generator/templates/baseAPI.ts.hbs b/scripts/rtk-client-generator/templates/baseAPI.ts.hbs deleted file mode 100644 index a5fd29cf488..00000000000 --- a/scripts/rtk-client-generator/templates/baseAPI.ts.hbs +++ /dev/null @@ -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: () => ({}), -}); diff --git a/scripts/rtk-client-generator/templates/config-entry.hbs b/scripts/rtk-client-generator/templates/config-entry.hbs deleted file mode 100644 index 2ccb0a54e6e..00000000000 --- a/scripts/rtk-client-generator/templates/config-entry.hbs +++ /dev/null @@ -1,9 +0,0 @@ -'{{apiPathPrefix}}/{{groupName}}/{{version}}/endpoints.gen.ts': { - apiFile: '{{apiPathPrefix}}/{{groupName}}/{{version}}/baseAPI.ts', - schemaFile: '../data/openapi/{{group}}-{{version}}.json', - {{#if endpoints}} - filterEndpoints: [{{{formatEndpoints endpoints}}}], - {{/if}} - tag: true, -}, -// PLOP_INJECT_API_CLIENT - Used by the API client generator diff --git a/scripts/rtk-client-generator/templates/index.ts.hbs b/scripts/rtk-client-generator/templates/index.ts.hbs deleted file mode 100644 index de9c3c66cf8..00000000000 --- a/scripts/rtk-client-generator/templates/index.ts.hbs +++ /dev/null @@ -1,3 +0,0 @@ -import { generatedAPI } from './endpoints.gen'; - -export const {{reducerPath}} = generatedAPI.enhanceEndpoints({}); diff --git a/yarn.lock b/yarn.lock index 25678c12d2d..cfcfdb59e48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2324,15 +2324,15 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.3.6": - version: 2.3.6 - resolution: "@formatjs/ecma402-abstract@npm:2.3.6" +"@formatjs/ecma402-abstract@npm:2.3.4": + version: 2.3.4 + resolution: "@formatjs/ecma402-abstract@npm:2.3.4" dependencies: "@formatjs/fast-memoize": "npm:2.2.7" - "@formatjs/intl-localematcher": "npm:0.6.2" + "@formatjs/intl-localematcher": "npm:0.6.1" decimal.js: "npm:^10.4.3" tslib: "npm:^2.8.0" - checksum: 10/30b1b5cd6b62ba46245f934429936592df5500bc1b089dc92dd49c826757b873dd92c305dcfe370701e4df6b057bf007782113abb9b65db550d73be4961718bc + checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2 languageName: node linkType: hard @@ -2376,13 +2376,13 @@ __metadata: linkType: hard "@formatjs/intl-durationformat@npm:^0.7.0": - version: 0.7.6 - resolution: "@formatjs/intl-durationformat@npm:0.7.6" + version: 0.7.4 + resolution: "@formatjs/intl-durationformat@npm:0.7.4" dependencies: - "@formatjs/ecma402-abstract": "npm:2.3.6" - "@formatjs/intl-localematcher": "npm:0.6.2" + "@formatjs/ecma402-abstract": "npm:2.3.4" + "@formatjs/intl-localematcher": "npm:0.6.1" tslib: "npm:^2.8.0" - checksum: 10/442236ba85bcd9cb7296c43a708271fa09f110b1ca9d5899066d00812fc2965eaeaec6b5240be421b80daba62860352131088449ba0fcd2061f671cec6240f0b + checksum: 10/d62273ecd635475ca91e9b501301f3f396403fa91b584c550734b19b2d194ba1316b27303fed985c1d42ae933d54eb220da6540edfdf376b0d9371ecfd0d4e15 languageName: node linkType: hard @@ -2395,12 +2395,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.6.2": - version: 0.6.2 - resolution: "@formatjs/intl-localematcher@npm:0.6.2" +"@formatjs/intl-localematcher@npm:0.6.1": + version: 0.6.1 + resolution: "@formatjs/intl-localematcher@npm:0.6.1" dependencies: tslib: "npm:^2.8.0" - checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064 + checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998 languageName: node linkType: hard @@ -3000,6 +3000,22 @@ __metadata: languageName: unknown linkType: soft +"@grafana/api-clients@workspace:*, @grafana/api-clients@workspace:packages/grafana-api-clients": + version: 0.0.0-use.local + resolution: "@grafana/api-clients@workspace:packages/grafana-api-clients" + dependencies: + "@rtk-query/codegen-openapi": "npm:^2.0.0" + openapi-types: "npm:^12.1.3" + plop: "npm:^4.0.1" + rollup: "npm:^4.22.4" + typescript: "npm:5.9.2" + peerDependencies: + "@grafana/runtime": ">=11.6 <= 12.x" + "@reduxjs/toolkit": ^2.8.0 + rxjs: 7.8.2 + languageName: unknown + linkType: soft + "@grafana/assistant@npm:0.1.0": version: 0.1.0 resolution: "@grafana/assistant@npm:0.1.0" @@ -5804,9 +5820,9 @@ __metadata: linkType: hard "@openfeature/core@npm:^1.9.0": - version: 1.9.1 - resolution: "@openfeature/core@npm:1.9.1" - checksum: 10/6099e16b1b4cd6e3c45c05ab4acd44c9cb4ab501b676ab6f3e77f0be1b56abc7506c5629187381333a02975d315d831e0905582435b356d9676255e72a465899 + version: 1.9.0 + resolution: "@openfeature/core@npm:1.9.0" + checksum: 10/c6d20edc09053afd99752fe46d8328158680950bca4b86679f67f79249d7226eea127b31fffdc38e26ecb729f2bab5a4a5a7c1db708ae76b7fbbac68cd56f094 languageName: node linkType: hard @@ -5831,11 +5847,11 @@ __metadata: linkType: hard "@openfeature/web-sdk@npm:^1.6.1": - version: 1.6.2 - resolution: "@openfeature/web-sdk@npm:1.6.2" + version: 1.6.1 + resolution: "@openfeature/web-sdk@npm:1.6.1" peerDependencies: "@openfeature/core": ^1.9.0 - checksum: 10/0fcc0ef76ff51d4725a00ff07755b21941b647f775d1ff04fc3d973143e78c909ccba485f582fcbc7f6201f42c795c85bcb103c43d64924fedf13ea517f625ac + checksum: 10/8bd7d1ea386e21cdd7492cab2fd1d2b138b4e6a376a4c0a40244633e5955f6452039bc2633fc5230bd7b494506a4137ba7210d40850634f9618f77a0ee435f9d languageName: node linkType: hard @@ -18387,6 +18403,7 @@ __metadata: "@formatjs/intl-durationformat": "npm:^0.7.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/alerting": "workspace:*" + "@grafana/api-clients": "workspace:*" "@grafana/assistant": "npm:0.1.0" "@grafana/aws-sdk": "npm:0.7.1" "@grafana/azure-sdk": "npm:0.0.7" @@ -18632,11 +18649,9 @@ __metadata: ol: "npm:10.6.1" ol-ext: "npm:4.0.35" open: "npm:^10.2.0" - openapi-types: "npm:^12.1.3" ora: "npm:^9.0.0" pa11y-ci: "npm:^4.0.0" pdf-parse: "npm:^1.1.1" - plop: "npm:^4.0.1" pluralize: "npm:^8.0.0" postcss: "npm:8.5.6" postcss-loader: "npm:8.1.1" @@ -23722,11 +23737,11 @@ __metadata: linkType: hard "nanoid@npm:^5.0.9": - version: 5.1.6 - resolution: "nanoid@npm:5.1.6" + version: 5.1.5 + resolution: "nanoid@npm:5.1.5" bin: nanoid: bin/nanoid.js - checksum: 10/4109dbcf596d7f297a9b42f459b8f01694a03ebbdd2f41408d963ad54e5ec7234cbe7b4acad137751f31add11bb4fb3415a3e688082516745812811f05570014 + checksum: 10/6de2d006b51c983be385ef7ee285f7f2a57bd96f8c0ca881c4111461644bd81fafc2544f8e07cb834ca0f3e0f3f676c1fe78052183f008b0809efe6e273119f5 languageName: node linkType: hard