[release-11 5.8] backport build fixes (#109465)
* [release-11.6.5] Update npm build in daggerbuild & revert some script changes (#109352) * update daggerbuild/frontend/npm.go * combine lerna commands * revert scripts changes (cherry picked from commit2d2d6d1ad5) * [release-11.6.5] update nx (#109400) * update nx (cherry picked from commit b065bf1fd9bd355614a2d3acd1eba3864267d042) * set some NX env variables * add make to cypress container * update cypress image * also install gcc * Also add g++ * upgrade lerna too (cherry picked from commit7d067d343a)
This commit is contained in:
+4
-2
@@ -207,13 +207,15 @@
|
|||||||
"jest-matcher-utils": "29.7.0",
|
"jest-matcher-utils": "29.7.0",
|
||||||
"jest-watch-typeahead": "^2.2.2",
|
"jest-watch-typeahead": "^2.2.2",
|
||||||
"jsdom-testing-mocks": "^1.13.1",
|
"jsdom-testing-mocks": "^1.13.1",
|
||||||
"lerna": "8.1.8",
|
"lerna": "8.2.3",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"msw": "2.7.0",
|
"msw": "2.7.0",
|
||||||
"mutationobserver-shim": "0.3.7",
|
"mutationobserver-shim": "0.3.7",
|
||||||
"ngtemplate-loader": "2.1.0",
|
"ngtemplate-loader": "2.1.0",
|
||||||
"node-notifier": "10.0.1",
|
"node-notifier": "10.0.1",
|
||||||
"nx": "19.8.2",
|
"nx": "20.7.1",
|
||||||
|
"openapi-types": "^12.1.3",
|
||||||
|
"pdf-parse": "^1.1.1",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.4.49",
|
||||||
"postcss-loader": "8.1.1",
|
"postcss-loader": "8.1.1",
|
||||||
"postcss-reporter": "7.1.0",
|
"postcss-reporter": "7.1.0",
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func CypressImage(version string) string {
|
func CypressImage(version string) string {
|
||||||
return "cypress/included:13.1.0"
|
return "cypress/included:13.10.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// CypressContainer returns a docker container with everything set up that is needed to build or run e2e tests.
|
// CypressContainer returns a docker container with everything set up that is needed to build or run e2e tests.
|
||||||
func CypressContainer(d *dagger.Client, base string) *dagger.Container {
|
func CypressContainer(d *dagger.Client, base string) *dagger.Container {
|
||||||
container := d.Container().From(base).WithEntrypoint([]string{})
|
container := d.Container().From(base).WithEntrypoint([]string{}).
|
||||||
|
WithExec([]string{"apt-get", "update", "-yq"}).
|
||||||
|
WithExec([]string{"apt-get", "install", "-yq", "make", "gcc", "g++"})
|
||||||
|
|
||||||
return container
|
return container
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ func Builder(d *dagger.Client, platform dagger.Platform, src *dagger.Directory,
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
).
|
).
|
||||||
|
WithEnvVariable("NX_ISOLATE_PLUGINS", "false").
|
||||||
|
WithEnvVariable("NX_DAEMON", "false").
|
||||||
WithWorkdir("/src")
|
WithWorkdir("/src")
|
||||||
|
|
||||||
// TODO: Should figure out exactly what we can include without all the extras so we can take advantage of caching better.
|
// TODO: Should figure out exactly what we can include without all the extras so we can take advantage of caching better.
|
||||||
|
|||||||
@@ -12,22 +12,14 @@ import (
|
|||||||
// NPMPackages versions and packs the npm packages into tarballs into `npm-packages` directory.
|
// NPMPackages versions and packs the npm packages into tarballs into `npm-packages` directory.
|
||||||
// It then returns the npm-packages directory as a dagger.Directory.
|
// It then returns the npm-packages directory as a dagger.Directory.
|
||||||
func NPMPackages(builder *dagger.Container, d *dagger.Client, log *slog.Logger, src *dagger.Directory, ersion string) (*dagger.Directory, error) {
|
func NPMPackages(builder *dagger.Container, d *dagger.Client, log *slog.Logger, src *dagger.Directory, ersion string) (*dagger.Directory, error) {
|
||||||
// Check if the version of Grafana uses lerna or nx to manage package versioning.
|
|
||||||
var (
|
var (
|
||||||
out = fmt.Sprintf("/src/npm-packages/%%s-%v.tgz", "v"+ersion)
|
out = fmt.Sprintf("/src/npm-packages/%%s-%v.tgz", "v"+ersion)
|
||||||
|
|
||||||
lernaBuild = fmt.Sprintf("yarn run packages:build && yarn lerna version %s --exact --no-git-tag-version --no-push --force-publish -y", ersion)
|
|
||||||
lernaPack = fmt.Sprintf("yarn lerna exec --no-private -- yarn pack --out %s", out)
|
|
||||||
|
|
||||||
nxBuild = fmt.Sprintf("yarn run packages:build && yarn nx release version %s --no-git-commit --no-git-tag --no-stage-changes --group grafanaPackages", ersion)
|
|
||||||
nxPack = fmt.Sprintf("yarn nx exec --projects=$(cat nx.json | jq -r '.release.groups.grafanaPackages.projects | join(\",\")') -- yarn pack --out %s", out)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return builder.WithExec([]string{"mkdir", "npm-packages"}).
|
return builder.WithExec([]string{"mkdir", "npm-packages"}).
|
||||||
WithEnvVariable("SHELL", "/bin/bash").
|
WithEnvVariable("SHELL", "/bin/bash").
|
||||||
WithExec([]string{"yarn", "install", "--immutable"}).
|
WithExec([]string{"yarn", "install", "--immutable"}).
|
||||||
WithExec([]string{"/bin/bash", "-c", fmt.Sprintf("if [ -f lerna.json ]; then %s; else %s; fi", lernaBuild, nxBuild)}).
|
WithExec([]string{"/bin/bash", "-c", fmt.Sprintf("yarn run packages:build && yarn lerna version %s --exact --no-git-tag-version --no-push --force-publish -y && yarn lerna exec --no-private -- yarn pack --out %s", ersion, out)}).
|
||||||
WithExec([]string{"/bin/bash", "-c", fmt.Sprintf("if [ -f lerna.json ]; then %s; else %s; fi", lernaPack, nxPack)}).
|
|
||||||
Directory("./npm-packages"), nil
|
Directory("./npm-packages"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import { writeFile } from 'node:fs/promises';
|
|||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
|
||||||
import { createTheme } from '@grafana/data';
|
import { createTheme } from '@grafana/data';
|
||||||
|
import { darkThemeVarsTemplate } from '@grafana/ui/src/themes/_variables.dark.scss.tmpl';
|
||||||
import { darkThemeVarsTemplate } from './themeTemplates/_variables.dark.scss.tmpl';
|
import { lightThemeVarsTemplate } from '@grafana/ui/src/themes/_variables.light.scss.tmpl';
|
||||||
import { lightThemeVarsTemplate } from './themeTemplates/_variables.light.scss.tmpl';
|
import { commonThemeVarsTemplate } from '@grafana/ui/src/themes/_variables.scss.tmpl';
|
||||||
import { commonThemeVarsTemplate } from './themeTemplates/_variables.scss.tmpl';
|
|
||||||
|
|
||||||
const darkThemeVariablesPath = resolve(__dirname, 'public', 'sass', '_variables.dark.generated.scss');
|
const darkThemeVariablesPath = resolve(__dirname, 'public', 'sass', '_variables.dark.generated.scss');
|
||||||
const lightThemeVariablesPath = resolve(__dirname, 'public', 'sass', '_variables.light.generated.scss');
|
const lightThemeVariablesPath = resolve(__dirname, 'public', 'sass', '_variables.light.generated.scss');
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ const config: ConfigFile = {
|
|||||||
|
|
||||||
'getDashboardByUid',
|
'getDashboardByUid',
|
||||||
'getLibraryElementByUid',
|
'getLibraryElementByUid',
|
||||||
|
|
||||||
'getResourceDependencies',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'../public/app/features/preferences/api/user/endpoints.gen.ts': {
|
'../public/app/features/preferences/api/user/endpoints.gen.ts': {
|
||||||
@@ -41,45 +39,25 @@ const config: ConfigFile = {
|
|||||||
apiImport: 'baseAPI',
|
apiImport: 'baseAPI',
|
||||||
filterEndpoints: ['getUserPreferences', 'updateUserPreferences', 'patchUserPreferences'],
|
filterEndpoints: ['getUserPreferences', 'updateUserPreferences', 'patchUserPreferences'],
|
||||||
},
|
},
|
||||||
'../public/app/api/clients/iam/v0alpha1/endpoints.gen.ts': {
|
'../public/app/features/iam/api/endpoints.gen.ts': {
|
||||||
schemaFile: '../data/openapi/iam.grafana.app-v0alpha1.json',
|
schemaFile: '../data/openapi/iam.grafana.app-v0alpha1.json',
|
||||||
apiFile: '../public/app/api/clients/iam/v0alpha1/baseAPI.ts',
|
apiFile: '../public/app/features/iam/api/api.ts',
|
||||||
|
apiImport: 'iamApi',
|
||||||
filterEndpoints: ['getDisplayMapping'],
|
filterEndpoints: ['getDisplayMapping'],
|
||||||
|
exportName: 'generatedIamApi',
|
||||||
|
flattenArg: false,
|
||||||
tag: true,
|
tag: true,
|
||||||
},
|
},
|
||||||
'../public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts': {
|
'../public/app/features/provisioning/api/endpoints.gen.ts': {
|
||||||
apiFile: '../public/app/api/clients/provisioning/v0alpha1/baseAPI.ts',
|
apiFile: '../public/app/features/provisioning/api/baseAPI.ts',
|
||||||
schemaFile: '../data/openapi/provisioning.grafana.app-v0alpha1.json',
|
schemaFile: '../data/openapi/provisioning.grafana.app-v0alpha1.json',
|
||||||
|
apiImport: 'baseAPI',
|
||||||
filterEndpoints,
|
filterEndpoints,
|
||||||
|
argSuffix: 'Arg',
|
||||||
|
responseSuffix: 'Response',
|
||||||
tag: true,
|
tag: true,
|
||||||
hooks: 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,
|
|
||||||
},
|
|
||||||
// PLOP_INJECT_API_CLIENT - Used by the API client generator
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,7 @@ content_security_policy_template = """require-trusted-types-for 'script'; script
|
|||||||
enable_frontend_sandbox_for_plugins = sandbox-app-test,sandbox-test-datasource,sandbox-test-panel
|
enable_frontend_sandbox_for_plugins = sandbox-app-test,sandbox-test-datasource,sandbox-test-panel
|
||||||
|
|
||||||
[feature_toggles]
|
[feature_toggles]
|
||||||
publicDashboards=true
|
enable = publicDashboards
|
||||||
grafanaAPIServer=true
|
|
||||||
queryLibrary=true
|
|
||||||
queryService=true
|
|
||||||
|
|
||||||
[environment]
|
|
||||||
stack_id = 12345
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
allow_loading_unsigned_plugins=grafana-extensionstest-app,grafana-extensionexample1-app,grafana-extensionexample2-app,grafana-extensionexample3-app,grafana-e2etest-datasource
|
allow_loading_unsigned_plugins=grafana-extensionstest-app,grafana-extensionexample1-app,grafana-extensionexample2-app,grafana-extensionexample3-app,grafana-e2etest-datasource
|
||||||
@@ -25,6 +19,3 @@ max_open_conn = 2
|
|||||||
[smtp]
|
[smtp]
|
||||||
enabled = true
|
enabled = true
|
||||||
host = localhost:7777
|
host = localhost:7777
|
||||||
|
|
||||||
[cloud_migration]
|
|
||||||
developer_mode = true ; Enable developer mode to use in-memory implementations of 3rdparty services needed.
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
|
|
||||||
. scripts/grafana-server/variables
|
. scripts/grafana-server/variables
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
set -eo pipefail
|
set -eo pipefail
|
||||||
|
|
||||||
. scripts/grafana-server/variables
|
. scripts/grafana-server/variables
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
|
|
||||||
DEFAULT_RUNDIR=scripts/grafana-server/tmp
|
DEFAULT_RUNDIR=scripts/grafana-server/tmp
|
||||||
RUNDIR=${RUNDIR:-$DEFAULT_RUNDIR}
|
RUNDIR=${RUNDIR:-$DEFAULT_RUNDIR}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
set -eo pipefail
|
set -eo pipefail
|
||||||
|
|
||||||
. scripts/grafana-server/variables
|
. scripts/grafana-server/variables
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PackageJson from '@npmcli/package-json';
|
import PackageJson from '@npmcli/package-json';
|
||||||
import { mkdir } from 'node:fs/promises';
|
import { mkdir } from 'node:fs/promises';
|
||||||
|
import { join, dirname } from 'node:path';
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
||||||
@@ -7,17 +8,18 @@ try {
|
|||||||
const pkgJson = await PackageJson.load(cwd);
|
const pkgJson = await PackageJson.load(cwd);
|
||||||
const cjsIndex = pkgJson.content.publishConfig?.main ?? pkgJson.content.main;
|
const cjsIndex = pkgJson.content.publishConfig?.main ?? pkgJson.content.main;
|
||||||
const esmIndex = pkgJson.content.publishConfig?.module ?? pkgJson.content.module;
|
const esmIndex = pkgJson.content.publishConfig?.module ?? pkgJson.content.module;
|
||||||
const typesIndex = pkgJson.content.publishConfig?.types ?? pkgJson.content.types;
|
const cjsTypes = pkgJson.content.publishConfig?.types ?? pkgJson.content.types;
|
||||||
|
const esmTypes = `./${join(dirname(esmIndex), 'index.d.mts')}`;
|
||||||
|
|
||||||
const exports = {
|
const exports = {
|
||||||
'./package.json': './package.json',
|
'./package.json': './package.json',
|
||||||
'.': {
|
'.': {
|
||||||
import: {
|
import: {
|
||||||
types: typesIndex,
|
types: esmTypes,
|
||||||
default: esmIndex,
|
default: esmIndex,
|
||||||
},
|
},
|
||||||
require: {
|
require: {
|
||||||
types: typesIndex,
|
types: cjsTypes,
|
||||||
default: cjsIndex,
|
default: cjsIndex,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -31,17 +33,9 @@ try {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for @grafana/i18n so eslint-plugin can be imported by consumers
|
|
||||||
if (pkgJson.content.name === '@grafana/i18n') {
|
|
||||||
exports['./eslint-plugin'] = {
|
|
||||||
import: './dist/eslint/index.cjs',
|
|
||||||
require: './dist/eslint/index.cjs',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pkgJson.update({
|
pkgJson.update({
|
||||||
main: cjsIndex,
|
main: cjsIndex,
|
||||||
types: typesIndex,
|
types: cjsTypes,
|
||||||
module: esmIndex,
|
module: esmIndex,
|
||||||
exports,
|
exports,
|
||||||
});
|
});
|
||||||
@@ -58,12 +52,12 @@ try {
|
|||||||
...pkgJson.content.exports,
|
...pkgJson.content.exports,
|
||||||
[`./${aliasName}`]: {
|
[`./${aliasName}`]: {
|
||||||
import: {
|
import: {
|
||||||
types: typesIndex.replace('index', aliasName),
|
types: esmTypes.replace('index', aliasName),
|
||||||
default: esmIndex.replace('index', aliasName),
|
default: esmIndex.replace('index', aliasName),
|
||||||
},
|
},
|
||||||
require: {
|
require: {
|
||||||
types: typesIndex.replace('index', aliasName),
|
types: cjsTypes.replace('index', aliasName),
|
||||||
default: cjsIndex.replace('index', aliasName),
|
default: cjsTypes.replace('index', aliasName),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -86,7 +80,7 @@ async function createAliasPackageJsonFiles(packageJsonContent, aliasName) {
|
|||||||
const pkgJson = await PackageJson.create(pkgJsonPath, {
|
const pkgJson = await PackageJson.create(pkgJsonPath, {
|
||||||
data: {
|
data: {
|
||||||
name: pkgName,
|
name: pkgName,
|
||||||
types: `../dist/types/${aliasName}.d.ts`,
|
types: `../dist/cjs/${aliasName}.d.cts`,
|
||||||
main: `../dist/cjs/${aliasName}.cjs`,
|
main: `../dist/cjs/${aliasName}.cjs`,
|
||||||
module: `../dist/esm/${aliasName}.mjs`,
|
module: `../dist/esm/${aliasName}.mjs`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
# RTK Query API Client Generator
|
|
||||||
|
|
||||||
This generator automates the process of creating RTK Query API clients for Grafana's API groups. It replaces the manual steps outlined in the [main API documentation](../../public/app/api/README.md).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn generate:api-client
|
|
||||||
```
|
|
||||||
|
|
||||||
The CLI will prompt for:
|
|
||||||
|
|
||||||
1. **Enterprise or OSS API** - Whether this is an Enterprise or OSS API. This affects paths and build commands.
|
|
||||||
2. **API group name** - The basic name for the API (e.g., `dashboard`)
|
|
||||||
3. **API group** - The full API group name (defaults to `<group-name>.grafana.app`)
|
|
||||||
4. **API version** - The API version (e.g., `v0alpha1`)
|
|
||||||
5. **Reducer path** - The Redux reducer path (defaults to `<group-name>API`). This will also be used as the API's named export.
|
|
||||||
6. **Endpoints** - Optional comma-separated list of endpoints to include (e.g., `createDashboard,updateDashboard`). If not provided, all endpoints will be included.
|
|
||||||
|
|
||||||
## What It Does
|
|
||||||
|
|
||||||
The generator automates the following:
|
|
||||||
|
|
||||||
1. Creates the `baseAPI.ts` file for the API group
|
|
||||||
2. Updates the appropriate generate script to include the API client
|
|
||||||
- `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
|
|
||||||
5. Formats all generated files using Prettier and ESLint
|
|
||||||
6. Automatically runs the appropriate command to generate endpoints from the OpenAPI schema
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
- The generator is optimized for Kubernetes-style APIs, as it requires Kubernetes resource details. For legacy APIs, manual adjustments may be needed.
|
|
||||||
- It expects processed OpenAPI specifications to exist in the `openapi_snapshots` directory
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Missing OpenAPI Schema
|
|
||||||
|
|
||||||
If an error about a missing OpenAPI schema appears, check that:
|
|
||||||
|
|
||||||
1. The API group and version exist in the backend
|
|
||||||
2. The `TestIntegrationOpenAPIs` test has been run to generate the schema (step 1 in the [main API documentation](../../public/app/api/README.md)).
|
|
||||||
3. The schema file exists at `data/openapi/<group>-<version>.json`
|
|
||||||
|
|
||||||
### Validation Errors
|
|
||||||
|
|
||||||
- API group must include `.grafana.app`
|
|
||||||
- Version must be in format `v0alpha1`, `v1beta2`, etc.
|
|
||||||
- Reducer path must end with `API`
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { execSync } from 'child_process';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
type PlopActionFunction = (
|
|
||||||
answers: Record<string, unknown>,
|
|
||||||
config?: Record<string, unknown>
|
|
||||||
) => string | Promise<string>;
|
|
||||||
|
|
||||||
// Helper to remove quotes from operation IDs
|
|
||||||
export const removeQuotes = (str: string | unknown) => {
|
|
||||||
if (typeof str !== 'string') {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
return str.replace(/^['"](.*)['"]$/, '$1');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatEndpoints = () => (endpointsInput: string | string[]) => {
|
|
||||||
if (Array.isArray(endpointsInput)) {
|
|
||||||
return endpointsInput.map((op) => `'${removeQuotes(op)}'`).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle string input (comma-separated)
|
|
||||||
if (typeof endpointsInput === 'string') {
|
|
||||||
const endpointsArray = endpointsInput
|
|
||||||
.split(',')
|
|
||||||
.map((id) => id.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
return endpointsArray.map((op) => `'${removeQuotes(op)}'`).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
|
|
||||||
return [
|
|
||||||
`${apiClientBasePath}/${groupName}/${version}/baseAPI.ts`,
|
|
||||||
`${apiClientBasePath}/${groupName}/${version}/index.ts`,
|
|
||||||
generateScriptPath,
|
|
||||||
...(isEnterprise ? [] : [`public/app/core/reducers/root.ts`, `public/app/store/configureStore.ts`]),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const runGenerateApis =
|
|
||||||
(basePath: string): PlopActionFunction =>
|
|
||||||
(answers, config) => {
|
|
||||||
try {
|
|
||||||
const isEnterprise = answers.isEnterprise || (config && config.isEnterprise);
|
|
||||||
|
|
||||||
let command;
|
|
||||||
if (isEnterprise) {
|
|
||||||
command = 'yarn process-specs && npx rtk-query-codegen-openapi ./local/generate-enterprise-apis.ts';
|
|
||||||
} else {
|
|
||||||
command = 'yarn generate-apis';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`⏳ Running ${command} to generate endpoints...`);
|
|
||||||
execSync(command, { stdio: 'inherit', cwd: basePath });
|
|
||||||
return '✅ API endpoints generated successfully!';
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
console.error('❌ Failed to generate API endpoints:', errorMessage);
|
|
||||||
return '❌ Failed to generate API endpoints. See error above.';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatFiles =
|
|
||||||
(basePath: string): PlopActionFunction =>
|
|
||||||
(_, config) => {
|
|
||||||
if (!config || !Array.isArray(config.files)) {
|
|
||||||
console.error('Invalid config passed to formatFiles action');
|
|
||||||
return '❌ Formatting failed: Invalid configuration';
|
|
||||||
}
|
|
||||||
|
|
||||||
const filesToFormat = config.files.map((file: string) => path.join(basePath, file));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filesList = filesToFormat.map((file: string) => `"${file}"`).join(' ');
|
|
||||||
|
|
||||||
console.log('🧹 Running ESLint on generated/modified files...');
|
|
||||||
try {
|
|
||||||
execSync(`yarn eslint --fix ${filesList}`, { cwd: basePath });
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
console.warn(`⚠️ Warning: ESLint encountered issues: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🧹 Running Prettier on generated/modified files...');
|
|
||||||
try {
|
|
||||||
// '--ignore-path' is necessary so the gitignored files ('local/' folder) can still be formatted
|
|
||||||
execSync(`yarn prettier --write ${filesList} --ignore-path=./.prettierignore`, { cwd: basePath });
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
console.warn(`⚠️ Warning: Prettier encountered issues: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return '✅ Files linted and formatted successfully!';
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
console.error('⚠️ Warning: Formatting operations failed:', errorMessage);
|
|
||||||
return '⚠️ Warning: Formatting operations failed.';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateGroup = (group: string) => {
|
|
||||||
return group && group.includes('.grafana.app') ? true : 'Group should be in format: name.grafana.app';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateVersion = (version: string) => {
|
|
||||||
return version && /^v\d+[a-z]*\d+$/.test(version) ? true : 'Version should be in format: v0alpha1, v1beta2, etc.';
|
|
||||||
};
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import type { NodePlopAPI, PlopGeneratorConfig } from 'plop';
|
|
||||||
|
|
||||||
import {
|
|
||||||
formatEndpoints,
|
|
||||||
validateGroup,
|
|
||||||
validateVersion,
|
|
||||||
getFilesToFormat,
|
|
||||||
runGenerateApis,
|
|
||||||
formatFiles,
|
|
||||||
// The file extension is necessary to make the imports
|
|
||||||
// work with the '--experimental-strip-types' flag
|
|
||||||
// @ts-ignore
|
|
||||||
} 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, '../..');
|
|
||||||
|
|
||||||
// Register custom action types
|
|
||||||
plop.setActionType('runGenerateApis', runGenerateApis(basePath));
|
|
||||||
plop.setActionType('formatFiles', formatFiles(basePath));
|
|
||||||
|
|
||||||
// Used in templates to format endpoints
|
|
||||||
plop.setHelper('formatEndpoints', formatEndpoints());
|
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
// Using app path, so the imports work on any file level
|
|
||||||
const clientImportPath = isEnterprise ? '../extensions/api/clients' : 'app/api/clients';
|
|
||||||
|
|
||||||
const apiPathPrefix = isEnterprise ? '../public/app/extensions/api/clients' : '../public/app/api/clients';
|
|
||||||
|
|
||||||
const templateData = {
|
|
||||||
...data,
|
|
||||||
apiPathPrefix,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Base actions that are always added
|
|
||||||
const actions: ActionConfig[] = [
|
|
||||||
{
|
|
||||||
type: 'add',
|
|
||||||
path: path.join(basePath, `${apiClientBasePath}/${groupName}/${version}/baseAPI.ts`),
|
|
||||||
templateFile: './templates/baseAPI.ts.hbs',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'modify',
|
|
||||||
path: path.join(basePath, generateScriptPath),
|
|
||||||
pattern: '// PLOP_INJECT_API_CLIENT - Used by the API client generator',
|
|
||||||
templateFile: './templates/config-entry.hbs',
|
|
||||||
data: templateData,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'add',
|
|
||||||
path: path.join(basePath, `${apiClientBasePath}/${groupName}/${version}/index.ts`),
|
|
||||||
templateFile: './templates/index.ts.hbs',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Only add redux reducer and middleware for OSS clients
|
|
||||||
if (!isEnterprise) {
|
|
||||||
actions.push(
|
|
||||||
{
|
|
||||||
type: 'modify',
|
|
||||||
path: path.join(basePath, 'public/app/core/reducers/root.ts'),
|
|
||||||
pattern: '// PLOP_INJECT_IMPORT',
|
|
||||||
template: `import { ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'modify',
|
|
||||||
path: path.join(basePath, 'public/app/core/reducers/root.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'),
|
|
||||||
pattern: '// PLOP_INJECT_MIDDLEWARE',
|
|
||||||
template: `${reducerPath}.middleware,\n // PLOP_INJECT_MIDDLEWARE`,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add formatting and generation actions
|
|
||||||
actions.push(
|
|
||||||
{
|
|
||||||
type: 'formatFiles',
|
|
||||||
files: getFilesToFormat(groupName, version, isEnterprise),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'runGenerateApis',
|
|
||||||
isEnterprise,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generator: PlopGeneratorConfig = {
|
|
||||||
description: 'Generate RTK Query API client for a Grafana API group',
|
|
||||||
prompts: [
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'isEnterprise',
|
|
||||||
message: 'Is this a Grafana Enterprise API?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: 'groupName',
|
|
||||||
message: 'API group name (e.g. dashboard):',
|
|
||||||
validate: (input: string) => (input?.trim() ? true : 'Group name is required'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: 'group',
|
|
||||||
message: 'API group (e.g. dashboard.grafana.app):',
|
|
||||||
default: (answers: { groupName?: string }) => `${answers.groupName}.grafana.app`,
|
|
||||||
validate: validateGroup,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: 'version',
|
|
||||||
message: 'API version (e.g. v0alpha1):',
|
|
||||||
default: 'v0alpha1',
|
|
||||||
validate: validateVersion,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: 'reducerPath',
|
|
||||||
message: 'Reducer path (e.g. dashboardAPIv0alpha1):',
|
|
||||||
default: (answers: { groupName?: string; version?: string }) => `${answers.groupName}API${answers.version}`,
|
|
||||||
validate: (input: string) =>
|
|
||||||
input?.endsWith('API') || input?.match(/API[a-z]\d+[a-z]*\d*$/)
|
|
||||||
? true
|
|
||||||
: 'Reducer path should end with "API" or "API<version>" (e.g. dashboardAPI, dashboardAPIv0alpha1)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: 'endpoints',
|
|
||||||
message: 'Endpoints to include (comma-separated, optional):',
|
|
||||||
validate: () => true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions: function (data) {
|
|
||||||
if (!isPlopData(data)) {
|
|
||||||
throw new Error('Invalid data format received from prompts');
|
|
||||||
}
|
|
||||||
|
|
||||||
return generateRtkApiActions(data);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
plop.setGenerator('rtk-api-client', generator);
|
|
||||||
}
|
|
||||||
@@ -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: () => ({}),
|
|
||||||
});
|
|
||||||
@@ -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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { generatedAPI } from './endpoints.gen';
|
|
||||||
|
|
||||||
export const {{reducerPath}} = generatedAPI.enhanceEndpoints({});
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import type { AddActionConfig, ModifyActionConfig } from 'plop';
|
|
||||||
|
|
||||||
export interface FormatFilesActionConfig {
|
|
||||||
type: 'formatFiles';
|
|
||||||
files: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RunGenerateApisActionConfig {
|
|
||||||
type: 'runGenerateApis';
|
|
||||||
isEnterprise: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Union type of all possible action configs
|
|
||||||
export type ActionConfig = AddActionConfig | ModifyActionConfig | FormatFilesActionConfig | RunGenerateApisActionConfig;
|
|
||||||
|
|
||||||
export interface PlopData {
|
|
||||||
groupName: string;
|
|
||||||
group: string;
|
|
||||||
version: string;
|
|
||||||
reducerPath: string;
|
|
||||||
endpoints: string;
|
|
||||||
isEnterprise: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPlopData(data: unknown): data is PlopData {
|
|
||||||
return typeof data === 'object' && data !== null;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
@@ -69,14 +68,6 @@ module.exports = {
|
|||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
Buffer: ['buffer', 'Buffer'],
|
Buffer: ['buffer', 'Buffer'],
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin({
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
from: 'public/img',
|
|
||||||
to: 'img',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ const browserslist = require('browserslist');
|
|||||||
const { resolveToEsbuildTarget } = require('esbuild-plugin-browserslist');
|
const { resolveToEsbuildTarget } = require('esbuild-plugin-browserslist');
|
||||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||||
const fs = require('fs');
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { DefinePlugin, EnvironmentPlugin } = require('webpack');
|
const { DefinePlugin, EnvironmentPlugin } = require('webpack');
|
||||||
@@ -30,21 +29,6 @@ function getDecoupledPlugins() {
|
|||||||
return packages.filter((pkg) => pkg.dir.includes('plugins/datasource')).map((pkg) => `${pkg.dir}/**`);
|
return packages.filter((pkg) => pkg.dir.includes('plugins/datasource')).map((pkg) => `${pkg.dir}/**`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When linking scenes for development, resolve the path to the src directory for sourcemaps
|
|
||||||
function scenesModule() {
|
|
||||||
const scenesPath = path.resolve('./node_modules/@grafana/scenes');
|
|
||||||
try {
|
|
||||||
const status = fs.lstatSync(scenesPath);
|
|
||||||
if (status.isSymbolicLink()) {
|
|
||||||
console.log(`scenes is linked to local scenes repo`);
|
|
||||||
return path.resolve(scenesPath + '/src');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error checking scenes path: ${error.message}`);
|
|
||||||
}
|
|
||||||
return scenesPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const envConfig = getEnvConfig();
|
const envConfig = getEnvConfig();
|
||||||
|
|
||||||
module.exports = (env = {}) => {
|
module.exports = (env = {}) => {
|
||||||
@@ -68,10 +52,14 @@ module.exports = (env = {}) => {
|
|||||||
// Packages linked for development need react to be resolved from the same location
|
// Packages linked for development need react to be resolved from the same location
|
||||||
react: path.resolve('./node_modules/react'),
|
react: path.resolve('./node_modules/react'),
|
||||||
|
|
||||||
|
// Also Grafana packages need to be resolved from the same location so they share
|
||||||
|
// the same singletons
|
||||||
|
'@grafana/runtime': path.resolve(__dirname, '../../packages/grafana-runtime'),
|
||||||
|
'@grafana/data': path.resolve(__dirname, '../../packages/grafana-data'),
|
||||||
|
|
||||||
// This is required to correctly resolve react-router-dom when linking with
|
// This is required to correctly resolve react-router-dom when linking with
|
||||||
// local version of @grafana/scenes
|
// local version of @grafana/scenes
|
||||||
'react-router-dom': path.resolve('./node_modules/react-router-dom'),
|
'react-router-dom': path.resolve('./node_modules/react-router-dom'),
|
||||||
'@grafana/scenes': scenesModule(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user