From 503bc2ba66824daa3ac467bb38f3993b90c69f69 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Mon, 3 Mar 2025 23:04:53 -0700 Subject: [PATCH 01/25] K8s: Indexing: Fix v2 (#100683) --- .../store/kind/dashboard/dashboard.go | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pkg/services/store/kind/dashboard/dashboard.go b/pkg/services/store/kind/dashboard/dashboard.go index 76a62c506df..3229acbe80d 100644 --- a/pkg/services/store/kind/dashboard/dashboard.go +++ b/pkg/services/store/kind/dashboard/dashboard.go @@ -211,15 +211,27 @@ func readDashboardIter(iter *jsoniter.Iterator, lookup DatasourceLookup) (*Dashb } case "annotations": - for sub := iter.ReadObject(); sub != ""; sub = iter.ReadObject() { - if sub == "list" { - for iter.ReadArray() { - v := iter.Read() - logf("[dash.anno] %v\n", v) - } - } else { - iter.Skip() + switch iter.WhatIsNext() { + case jsoniter.ArrayValue: + // dashboards v2 is an array + for iter.ReadArray() { + v := iter.Read() + logf("[dash.anno] %v\n", v) } + case jsoniter.ObjectValue: + // dashboards v0/v1 are an object + for sub := iter.ReadObject(); sub != ""; sub = iter.ReadObject() { + if sub == "list" { + for iter.ReadArray() { + v := iter.Read() + logf("[dash.anno] %v\n", v) + } + } else { + iter.Skip() + } + } + default: + iter.Skip() } case "templating": From 859c12bbb4d6bde63a11dd5b7f53d1bd82bd2b38 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Tue, 4 Mar 2025 09:55:41 +0100 Subject: [PATCH 02/25] Grafana UI: Expose unstable entrypoint (#97080) * feat(grafana-ui): build unstable entrypoint for experimental components * feat(plugins): expose grafana/ui/unstable * build(grafana-ui): add rollup plugin to create alias package.json for unstable entrypoint * build(packages): rewrite prepare npm script to generate alias packagejson files * chore(packages): use relative paths in publishConfig for exports generation * chore(frontend): move npmcli/package-json package to root package.json * revert(grafana-ui): remove rollup plugin for generating alias package.json files * chore(grafana-ui): clean up unstable directory postpack to prevent yarn lock issues * build(packages): fix scope for pkgName usage * feat(packages): create separate cjs and esm builds that validate with arethetypeswrong cli * chore(yarn): refresh lock file * fix(packages): make sure alias package.jsons point to existing files --- .github/CODEOWNERS | 2 +- .gitignore | 1 + package.json | 1 + packages/grafana-data/package.json | 8 +- packages/grafana-e2e-selectors/package.json | 10 +-- packages/grafana-flamegraph/package.json | 8 +- packages/grafana-icons/package.json | 6 +- packages/grafana-prometheus/package.json | 8 +- packages/grafana-runtime/package.json | 8 +- packages/grafana-schema/package.json | 8 +- packages/grafana-ui/package.json | 12 +-- packages/grafana-ui/rollup.config.ts | 25 ++++++ packages/rollup.config.parts.ts | 18 ++-- .../plugins/loader/sharedDependencies.ts | 1 + scripts/prepare-npm-package.js | 84 +++++++++++++++++++ scripts/prepare-packagejson.js | 21 ----- scripts/validate-npm-packages.sh | 14 ++-- yarn.lock | 27 +++++- 18 files changed, 193 insertions(+), 69 deletions(-) create mode 100644 scripts/prepare-npm-package.js delete mode 100644 scripts/prepare-packagejson.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7f6fd0c4f91..d2fe4b77837 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -620,7 +620,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /scripts/import_many_dashboards.sh @torkelo /scripts/mixin-check.sh @bergquist /scripts/openapi3/ @grafana/grafana-operator-experience-squad -/scripts/prepare-packagejson.js @grafana/frontend-ops +/scripts/prepare-npm-package.js @grafana/frontend-ops /scripts/protobuf-check.sh @grafana/plugins-platform-backend /scripts/stripnulls.sh @grafana/grafana-as-code /scripts/tag_release.sh @grafana/grafana-developer-enablement-squad diff --git a/.gitignore b/.gitignore index c043094a016..1a8be100668 100644 --- a/.gitignore +++ b/.gitignore @@ -148,6 +148,7 @@ pkg/services/quota/quotaimpl/storage/storage.json # Ignoring frontend packages specifics /packages/grafana-ui/.yarn/.cache /packages/grafana-ui/.storybook/static +/packages/grafana-ui/unstable /packages/**/dist /packages/**/compiled /packages/**/.rpt2_cache diff --git a/package.json b/package.json index 1660de6b891..1c9393c4bd9 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@grafana/plugin-e2e": "1.17.1", "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", + "@npmcli/package-json": "^5.2.0", "@playwright/test": "1.50.1", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", "@react-types/button": "3.10.2", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 61124640e72..6ae5450bf1f 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -16,9 +16,9 @@ "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "main": "dist/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/cjs/index.d.cts", "access": "public" }, "files": [ @@ -31,7 +31,7 @@ "build": "tsc -p ./tsconfig.build.json && 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-packagejson.js", + "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json" }, "dependencies": { diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 739e36cfec8..dc8a0541bcf 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -19,9 +19,9 @@ "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "main": "dist/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/cjs/index.d.cts", "access": "public" }, "files": [ @@ -35,7 +35,7 @@ "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-packagejson.js", + "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json" }, "devDependencies": { @@ -51,7 +51,7 @@ }, "dependencies": { "@grafana/tsconfig": "^2.0.0", - "semver": "7.7.0", + "semver": "^7.7.0", "tslib": "2.8.1", "typescript": "5.7.3" } diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index f47339c0aa0..9ce7c7e25c5 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -19,9 +19,9 @@ "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "main": "dist/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/cjs/index.d.cts", "access": "public" }, "files": [ @@ -35,7 +35,7 @@ "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-packagejson.js", + "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json" }, "browserslist": [ diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index cb60d576dd5..08bb3641efb 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -15,9 +15,9 @@ "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.mts", "access": "public" }, "files": [ diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 9cf1a0c1100..59690be5c05 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -22,9 +22,9 @@ "./LICENSE_AGPL" ], "publishConfig": { - "main": "dist/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/cjs/index.d.cts", "access": "public" }, "scripts": { @@ -32,7 +32,7 @@ "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-packagejson.js", + "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json" }, "dependencies": { diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 90ca6774646..058e41e4d49 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -17,9 +17,9 @@ "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "main": "dist/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/cjs/index.d.cts", "access": "public" }, "files": [ @@ -33,7 +33,7 @@ "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-packagejson.js", + "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json" }, "dependencies": { diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index a4939ade8a1..3a97767a218 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -16,9 +16,9 @@ "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "main": "dist/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/cjs/index.d.cts", "access": "public" }, "files": [ @@ -32,7 +32,7 @@ "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-packagejson.js", + "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json" }, "devDependencies": { diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 1116e1b2980..4b51ff9386a 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -19,9 +19,9 @@ "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "main": "dist/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/cjs/index.d.cts", "access": "public" }, "files": [ @@ -34,12 +34,12 @@ "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", + "clean": "rimraf ./dist ./compiled ./unstable ./package.tgz", "storybook": "storybook dev -p 9001 -c .storybook --no-open", "storybook:build": "storybook build -o ./dist/storybook -c .storybook", "typecheck": "tsc --emitDeclarationOnly false --noEmit", - "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", - "postpack": "mv package.json.bak package.json" + "prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=unstable node ../../scripts/prepare-npm-package.js", + "postpack": "mv package.json.bak package.json && rimraf ./unstable" }, "browserslist": [ "defaults", diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts index d3cb5c2d8b8..5dcad62f82f 100644 --- a/packages/grafana-ui/rollup.config.ts +++ b/packages/grafana-ui/rollup.config.ts @@ -25,5 +25,30 @@ export default [ ], output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-ui')], }, + { + input: 'src/unstable.ts', + plugins: [ + ...plugins, + svg({ stringify: true }), + copy({ + targets: [{ src: iconSrcPaths, dest: './dist/public/' }], + flatten: false, + }), + ], + output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-ui')], + }, tsDeclarationOutput(pkg), + tsDeclarationOutput(pkg, { + input: './compiled/unstable.d.ts', + output: [ + { + file: './dist/cjs/unstable.d.cts', + format: 'cjs', + }, + { + file: './dist/esm/unstable.d.mts', + format: 'es', + }, + ], + }), ]; diff --git a/packages/rollup.config.parts.ts b/packages/rollup.config.parts.ts index ca805b07ed9..41b760a3acf 100644 --- a/packages/rollup.config.parts.ts +++ b/packages/rollup.config.parts.ts @@ -1,6 +1,6 @@ // This file contains the common parts of the rollup configuration that are shared across multiple packages. import nodeResolve from '@rollup/plugin-node-resolve'; -import { dirname, resolve } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; import { nodeExternals } from 'rollup-plugin-node-externals'; @@ -28,6 +28,7 @@ export function cjsOutput(pkg) { format: 'cjs', sourcemap: true, dir: dirname(pkg.publishConfig.main), + entryFileNames: '[name].cjs', esModule: true, interop: 'compat', }; @@ -39,6 +40,7 @@ export function esmOutput(pkg, pkgName) { format: 'esm', sourcemap: true, dir: dirname(pkg.publishConfig.module), + entryFileNames: '[name].mjs', preserveModules: true, preserveModulesRoot: resolve(projectCwd, `packages/${pkgName}/src`), }; @@ -49,10 +51,16 @@ export function tsDeclarationOutput(pkg, overrides = {}) { return { input: './compiled/index.d.ts', plugins: [dts()], - output: { - file: pkg.publishConfig.types, - format: 'es', - }, + output: [ + { + file: pkg.publishConfig.types, + format: 'cjs', + }, + { + file: join(dirname(pkg.publishConfig.module), 'index.d.mts'), + format: 'es', + }, + ], ...overrides, }; } diff --git a/public/app/features/plugins/loader/sharedDependencies.ts b/public/app/features/plugins/loader/sharedDependencies.ts index 7059274c717..c294491c6ac 100644 --- a/public/app/features/plugins/loader/sharedDependencies.ts +++ b/public/app/features/plugins/loader/sharedDependencies.ts @@ -52,6 +52,7 @@ export const sharedDependenciesMap = { '@grafana/runtime': grafanaRuntime, '@grafana/slate-react': () => import('slate-react'), '@grafana/ui': grafanaUI, + '@grafana/ui/unstable': () => import('@grafana/ui/src/unstable'), '@kusto/monaco-kusto': () => import('@kusto/monaco-kusto'), 'app/core/app_events': { default: appEvents, diff --git a/scripts/prepare-npm-package.js b/scripts/prepare-npm-package.js new file mode 100644 index 00000000000..07559074842 --- /dev/null +++ b/scripts/prepare-npm-package.js @@ -0,0 +1,84 @@ +import PackageJson from '@npmcli/package-json'; +import { mkdir } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; + +const cwd = process.cwd(); + +try { + const pkgJson = await PackageJson.load(cwd); + const cjsIndex = pkgJson.content.publishConfig?.main ?? pkgJson.content.main; + const esmIndex = pkgJson.content.publishConfig?.module ?? pkgJson.content.module; + const cjsTypes = pkgJson.content.publishConfig?.types ?? pkgJson.content.types; + const esmTypes = `./${join(dirname(esmIndex), 'index.d.mts')}`; + + const exports = { + './package.json': './package.json', + '.': { + import: { + types: esmTypes, + default: esmIndex, + }, + require: { + types: cjsTypes, + default: cjsIndex, + }, + }, + }; + + pkgJson.update({ + main: cjsIndex, + types: cjsTypes, + module: esmIndex, + exports, + }); + + await pkgJson.save(); + + // If an alias package name is provided we add an exports entry for the alias + // then generate an additional "nested" package.json for typescript resolution that + // doesn't use the exports property in package.json. + if (process.env.ALIAS_PACKAGE_NAME) { + const aliasName = process.env.ALIAS_PACKAGE_NAME; + pkgJson.update({ + exports: { + ...pkgJson.content.exports, + [`./${aliasName}`]: { + import: { + types: esmTypes.replace('index', aliasName), + default: esmIndex.replace('index', aliasName), + }, + require: { + types: cjsTypes.replace('index', aliasName), + default: cjsTypes.replace('index', aliasName), + }, + }, + }, + files: [...pkgJson.content.files, aliasName], + }); + await pkgJson.save(); + await createAliasPackageJsonFiles(pkgJson.content, aliasName); + } +} catch (e) { + console.error(e); + process.exit(1); +} + +async function createAliasPackageJsonFiles(packageJsonContent, aliasName) { + const pkgName = `${packageJsonContent.name}/${aliasName}`; + try { + console.log(`📦 Writing alias package.json for ${pkgName}.`); + const pkgJsonPath = `${cwd}/${aliasName}`; + await mkdir(pkgJsonPath, { recursive: true }); + const pkgJson = await PackageJson.create(pkgJsonPath, { + data: { + name: pkgName, + types: `../dist/cjs/${aliasName}.d.cts`, + main: `../dist/cjs/${aliasName}.cjs`, + module: `../dist/esm/${aliasName}.mjs`, + }, + }); + await pkgJson.save(); + } catch (error) { + throw new Error(`Error generating package.json for ${pkgName}`, error); + } +} diff --git a/scripts/prepare-packagejson.js b/scripts/prepare-packagejson.js deleted file mode 100644 index cab9eed4c98..00000000000 --- a/scripts/prepare-packagejson.js +++ /dev/null @@ -1,21 +0,0 @@ -const fs = require('fs'); - -const cwd = process.cwd(); -const packageJson = require(`${cwd}/package.json`); - -const newPackageJson = { - ...packageJson, - main: packageJson.publishConfig?.main ?? packageJson.main, -}; - -if (packageJson.publishConfig?.types) { - newPackageJson.types = packageJson.publishConfig.types; -} - -if (packageJson.publishConfig?.module) { - newPackageJson.module = packageJson.publishConfig.module; -} - -try { - fs.writeFileSync(`${cwd}/package.json`, JSON.stringify(newPackageJson, null, 2)); -} catch (e) {} diff --git a/scripts/validate-npm-packages.sh b/scripts/validate-npm-packages.sh index 838dccd91e1..2f0f1764597 100755 --- a/scripts/validate-npm-packages.sh +++ b/scripts/validate-npm-packages.sh @@ -43,14 +43,14 @@ for file in "$ARTIFACTS_DIR"/*.tgz; do fi # Assert commonjs builds - if [ ! -d dist ] || [ ! -f dist/index.js ] || [ ! -f dist/index.d.ts ]; then + if [ ! -d dist ] || [ ! -f dist/cjs/index.cjs ] || [ ! -f dist/cjs/index.d.cts ]; then echo -e "❌ Failed: Missing 'dist' directory or required commonjs files in package $dir_name.\n" exit 1 fi - if [ "$(jq -r '.main' package.json)" != "dist/index.js" ] || \ - [ "$(jq -r '.types' package.json)" != "dist/index.d.ts" ]; then - echo -e "❌ Failed: Incorrect package.json properties in package $dir_name.\n" + if [ "$(jq -r '.main' package.json)" != "./dist/cjs/index.cjs" ] || \ + [ "$(jq -r '.types' package.json)" != "./dist/cjs/index.d.cts" ]; then + echo -e "❌ Failed: Incorrect cjs package.json properties in package $dir_name.\n" exit 1 fi @@ -58,13 +58,13 @@ for file in "$ARTIFACTS_DIR"/*.tgz; do esm_packages=("grafana-data" "grafana-ui" "grafana-runtime" "grafana-e2e-selectors" "grafana-schema") for esm_package in "${esm_packages[@]}"; do if [[ "$dir_name" == "$esm_package" ]]; then - if [ ! -d dist/esm ] || [ ! -f dist/esm/index.js ]; then + if [ ! -d dist/esm ] || [ ! -f dist/esm/index.mjs ]; then echo -e "❌ Failed: Missing 'dist/esm' directory or required esm files in package $dir_name.\n" exit 1 fi - if [ "$(jq -r '.module' package.json)" != "dist/esm/index.js" ]; then - echo -e "❌ Failed: Incorrect package.json properties in package $dir_name.\n" + if [ "$(jq -r '.module' package.json)" != "./dist/esm/index.mjs" ]; then + echo -e "❌ Failed: Incorrect esm package.json properties in package $dir_name.\n" exit 1 fi fi diff --git a/yarn.lock b/yarn.lock index cb307fe09ce..dce63788f1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3110,7 +3110,7 @@ __metadata: rollup-plugin-dts: "npm:^6.1.1" rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" - semver: "npm:7.7.0" + semver: "npm:^7.7.0" tslib: "npm:2.8.1" typescript: "npm:5.7.3" languageName: unknown @@ -5444,6 +5444,21 @@ __metadata: languageName: node linkType: hard +"@npmcli/package-json@npm:^5.2.0": + version: 5.2.1 + resolution: "@npmcli/package-json@npm:5.2.1" + dependencies: + "@npmcli/git": "npm:^5.0.0" + glob: "npm:^10.2.2" + hosted-git-info: "npm:^7.0.0" + json-parse-even-better-errors: "npm:^3.0.0" + normalize-package-data: "npm:^6.0.0" + proc-log: "npm:^4.0.0" + semver: "npm:^7.5.3" + checksum: 10/304a819b93f79a6e0e56cb371961a66d2db72142e310d545ecbbbe4d917025a30601aa8e63a5f0cc28f0fe281c116bdaf79b334619b105a1d027a2b769ecd137 + languageName: node + linkType: hard + "@npmcli/promise-spawn@npm:^7.0.0": version: 7.0.2 resolution: "@npmcli/promise-spawn@npm:7.0.2" @@ -18143,6 +18158,7 @@ __metadata: "@manypkg/get-packages": "npm:^2.2.0" "@msagl/core": "npm:^1.1.19" "@msagl/parser": "npm:^1.1.19" + "@npmcli/package-json": "npm:^5.2.0" "@opentelemetry/api": "npm:1.9.0" "@opentelemetry/exporter-collector": "npm:0.25.0" "@opentelemetry/semantic-conventions": "npm:1.28.0" @@ -28361,6 +28377,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.0": + version: 7.7.1 + resolution: "semver@npm:7.7.1" + bin: + semver: bin/semver.js + checksum: 10/4cfa1eb91ef3751e20fc52e47a935a0118d56d6f15a837ab814da0c150778ba2ca4f1a4d9068b33070ea4273629e615066664c2cfcd7c272caf7a8a0f6518b2c + languageName: node + linkType: hard + "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" From 60827fe4995054e1f4f1cc274a54cb9ca452cc6b Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 4 Mar 2025 10:04:47 +0100 Subject: [PATCH 03/25] Alerting: Return 403 if no datasource access or quota has been exceeded (#101522) --- .../api/api_convert_prometheus_test.go | 59 ++++++++++++++++--- pkg/services/ngalert/api/errors.go | 7 +++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 945a0558365..5fd920438d0 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" @@ -22,6 +23,7 @@ import ( apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" + "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -151,6 +153,32 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { require.NotNil(t, remaining) }) + t.Run("with no access to the datasource should return 403", func(t *testing.T) { + acFake := &acfakes.FakeRuleService{} + srv, _, _, _ := createConvertPrometheusSrv(t, withFakeAccessControlRuleService(acFake)) + + acFake.AuthorizeRuleChangesFunc = func(context.Context, identity.Requester, *store.GroupDelta) error { + return datasources.ErrDataSourceAccessDenied + } + + rc := createRequestCtx() + response := srv.RouteConvertPrometheusPostRuleGroup(rc, "folder", simpleGroup) + require.Equal(t, http.StatusForbidden, response.Status()) + require.Contains(t, string(response.Body()), "data source access denied") + }) + + t.Run("when alert rule quota limit exceeded", func(t *testing.T) { + quotas := &provisioning.MockQuotaChecker{} + quotas.EXPECT().LimitExceeded() + + srv, _, _, _ := createConvertPrometheusSrv(t, withQuotaChecker(quotas)) + + rc := createRequestCtx() + response := srv.RouteConvertPrometheusPostRuleGroup(rc, "folder", simpleGroup) + require.Equal(t, http.StatusForbidden, response.Status()) + require.Contains(t, string(response.Body()), "quota has been exceeded") + }) + t.Run("with valid pause header values should return 202", func(t *testing.T) { testCases := []struct { name string @@ -749,7 +777,9 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) { } type convertPrometheusSrvOptions struct { - provenanceStore provisioning.ProvisioningStore + provenanceStore provisioning.ProvisioningStore + fakeAccessControlRuleService *acfakes.FakeRuleService + quotaChecker *provisioning.MockQuotaChecker } type convertPrometheusSrvOptionsFunc func(*convertPrometheusSrvOptions) @@ -760,11 +790,29 @@ func withProvenanceStore(store provisioning.ProvisioningStore) convertPrometheus } } +func withFakeAccessControlRuleService(service *acfakes.FakeRuleService) convertPrometheusSrvOptionsFunc { + return func(opts *convertPrometheusSrvOptions) { + opts.fakeAccessControlRuleService = service + } +} + +func withQuotaChecker(checker *provisioning.MockQuotaChecker) convertPrometheusSrvOptionsFunc { + return func(opts *convertPrometheusSrvOptions) { + opts.quotaChecker = checker + } +} + func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) { t.Helper() + // By default the quota checker will allow the operation + quotas := &provisioning.MockQuotaChecker{} + quotas.EXPECT().LimitOK() + options := convertPrometheusSrvOptions{ - provenanceStore: fakes.NewFakeProvisioningStore(), + provenanceStore: fakes.NewFakeProvisioningStore(), + fakeAccessControlRuleService: &acfakes.FakeRuleService{}, + quotaChecker: quotas, } for _, opt := range opts { @@ -782,23 +830,20 @@ func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOption } dsCache.DataSources = append(dsCache.DataSources, ds) - quotas := &provisioning.MockQuotaChecker{} - quotas.EXPECT().LimitOK() - folderService := foldertest.NewFakeService() alertRuleService := provisioning.NewAlertRuleService( ruleStore, options.provenanceStore, folderService, - quotas, + options.quotaChecker, &provisioning.NopTransactionManager{}, 60, 10, 100, log.New("test"), &provisioning.NotificationSettingsValidatorProviderFake{}, - &acfakes.FakeRuleService{}, + options.fakeAccessControlRuleService, ) cfg := &setting.UnifiedAlertingSettings{ diff --git a/pkg/services/ngalert/api/errors.go b/pkg/services/ngalert/api/errors.go index a66db1e4fb2..138eb6a3cd1 100644 --- a/pkg/services/ngalert/api/errors.go +++ b/pkg/services/ngalert/api/errors.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" ) var ( @@ -42,5 +43,11 @@ func errorToResponse(err error) response.Response { if errors.Is(err, errFolderAccess) { return toNamespaceErrorResponse(err) } + if errors.Is(err, datasources.ErrDataSourceAccessDenied) { + return ErrResp(http.StatusForbidden, err, "") + } + if errors.Is(err, models.ErrQuotaReached) { + return ErrResp(http.StatusForbidden, err, "") + } return ErrResp(http.StatusInternalServerError, err, "") } From bf9a34f2ca81fdec3c4be74f5e63a9fe6bbf5102 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:45:24 +0000 Subject: [PATCH 04/25] TimePicker: Ability to manually specify quick ranges (#101465) * add quickRanges to scene model transforms * add test for quickRanges transformations * fix lint --- .../src/schema/dashboard/v2alpha0/examples.ts | 12 + .../transformSceneToSaveModel.test.ts.snap | 375 ++++++++++++++++++ .../transformSaveModelSchemaV2ToScene.test.ts | 4 +- .../transformSaveModelSchemaV2ToScene.ts | 4 +- .../transformSaveModelToScene.test.ts | 30 ++ .../transformSaveModelToScene.ts | 4 +- .../transformSceneToSaveModel.test.ts | 27 ++ .../transformSceneToSaveModel.ts | 1 + .../transformSceneToSaveModelSchemaV2.test.ts | 12 + .../transformSceneToSaveModelSchemaV2.ts | 1 + 10 files changed, 465 insertions(+), 5 deletions(-) diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index c12f1cc73f7..fe92defe024 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -18,6 +18,18 @@ export const handyTestingSchema: DashboardV2Spec = { timezone: 'UTC', to: 'now', weekStart: 'monday', + quickRanges: [ + { + display: 'Last 6 hours', + from: 'now-6h', + to: 'now', + }, + { + display: 'Last 3 days', + from: 'now-3d', + to: 'now', + }, + ], }, annotations: [ { diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 585b51daf4e..eaf887a176b 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -1,5 +1,380 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Given a scene with custom quick ranges should save quick ranges to save model 1`] = ` +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana", + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard", + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata", + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "Enabled", + "target": { + "lines": 4, + "refId": "Anno", + "scenarioId": "annotations", + }, + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata", + }, + "enable": false, + "hide": false, + "iconColor": "yellow", + "name": "Disabled", + "target": { + "lines": 5, + "refId": "Anno", + "scenarioId": "annotations", + }, + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata", + }, + "enable": true, + "hide": true, + "iconColor": "dark-purple", + "name": "Hidden", + "target": { + "lines": 6, + "refId": "Anno", + "scenarioId": "annotations", + }, + }, + ], + }, + "editable": true, + "fiscalYearStartMonth": 1, + "graphTooltip": 1, + "id": 1351, + "links": [], + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A", + }, + "description": "This is a simple time series graph", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic", + }, + "custom": { + "fillOpacity": 0, + "gradientMode": "none", + "lineWidth": 2, + }, + }, + "overrides": [], + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0, + }, + "id": 28, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + }, + "tooltip": { + "mode": "single", + "sort": "none", + }, + }, + "targets": [ + { + "alias": "series", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A", + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1, + }, + ], + "title": "Simple time series graph ", + "type": "timeseries", + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8, + }, + "id": 5, + "panels": [], + "title": "Row title", + "type": "row", + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A", + }, + "fieldConfig": { + "defaults": {}, + "overrides": [], + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 9, + }, + "id": 29, + "options": {}, + "targets": [ + { + "alias": "series", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A", + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1, + }, + ], + "title": "panel inside row", + "type": "timeseries", + }, + { + "fieldConfig": { + "defaults": {}, + "overrides": [], + }, + "gridPos": { + "h": 10, + "w": 11, + "x": 12, + "y": 9, + }, + "id": 25, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false, + }, + "content": "content", + "mode": "markdown", + }, + "pluginVersion": "10.2.0-pre", + "title": "Transparent text panel", + "transparent": true, + "type": "text", + }, + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [ + "gdev", + "graph-ng", + "demo", + ], + "templating": { + "list": [ + { + "baseFilters": [], + "datasource": { + "type": "prometheus", + "uid": "wc2AL7L7k", + }, + "filters": [], + "name": "Filters", + "type": "adhoc", + }, + { + "auto": true, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "1m", + "value": "1m", + }, + "name": "intervalVar", + "options": [ + { + "selected": true, + "text": "1m", + "value": "1m", + }, + { + "selected": false, + "text": "10m", + "value": "10m", + }, + { + "selected": false, + "text": "30m", + "value": "30m", + }, + { + "selected": false, + "text": "1h", + "value": "1h", + }, + { + "selected": false, + "text": "6h", + "value": "6h", + }, + { + "selected": false, + "text": "12h", + "value": "12h", + }, + { + "selected": false, + "text": "1d", + "value": "1d", + }, + { + "selected": false, + "text": "7d", + "value": "7d", + }, + { + "selected": false, + "text": "14d", + "value": "14d", + }, + { + "selected": false, + "text": "30d", + "value": "30d", + }, + ], + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "type": "interval", + }, + { + "current": { + "text": [ + "a", + ], + "value": [ + "a", + ], + }, + "includeAll": true, + "multi": true, + "name": "customVar", + "options": [], + "query": "a, b, c", + "type": "custom", + }, + { + "current": { + "text": "gdev-testdata", + "value": "PD8C576611E62080A", + }, + "includeAll": false, + "name": "dsVar", + "options": [], + "query": "grafana-testdata-datasource", + "refresh": 1, + "regex": "", + "type": "datasource", + }, + { + "current": { + "text": "A", + "value": "A", + }, + "definition": "*", + "includeAll": false, + "name": "query0", + "options": [], + "query": { + "query": "*", + "refId": "StandardVariableQuery", + }, + "refresh": 1, + "regex": "", + "type": "query", + }, + { + "current": { + "text": "test", + "value": "test", + }, + "hide": 2, + "name": "constant", + "query": "test", + "skipUrlSync": true, + "type": "constant", + }, + ], + }, + "time": { + "from": "now-5m", + "to": "now", + }, + "timepicker": { + "quick_ranges": [ + { + "display": "Last 6 hours", + "from": "now-6h", + "to": "now", + }, + { + "display": "Last 3 days", + "from": "now-3d", + "to": "now", + }, + ], + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d", + ], + }, + "timezone": "America/New_York", + "title": "Dashboard to load1", + "uid": "nP8rcffGkasd", + "version": 2, + "weekStart": "saturday", +} +`; + exports[`transformSceneToSaveModel Annotations should transform annotations to save model 1`] = ` [ { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index cf8308c90ca..e1bd8b6fe0f 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -131,6 +131,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { expect(dashboardControls).toBeDefined(); expect(dashboardControls.state.refreshPicker.state.intervals).toEqual(time.autoRefreshIntervals); expect(dashboardControls.state.hideTimeControls).toBe(time.hideTimepicker); + expect(dashboardControls.state.timePicker.state.quickRanges).toEqual(dash.timeSettings.quickRanges); // Variables const variables = scene.state?.$variables; @@ -223,9 +224,6 @@ describe('transformSaveModelSchemaV2ToScene', () => { expect(dataLayers.state.annotationLayers[3].state.isEnabled).toBe(dash.annotations[3].spec.enable); expect(dataLayers.state.annotationLayers[3].state.isHidden).toBe(dash.annotations[3].spec.hide); - // To be implemented - // expect(timePicker.state.ranges).toEqual(dash.timeSettings.quickRanges); - // VizPanel const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); expect(vizPanels).toHaveLength(3); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 40b5b01fbf9..43219038d22 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -203,7 +203,9 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo { }); }); +describe('When creating a snapshot dashboard scene', () => { + it('should initialize a dashboard scene with SnapshotVariables', () => { + const dashboard = { + ...defaultDashboard, + title: 'With custom quick ranges', + uid: 'test-uid', + timepicker: { + ...defaultTimePickerConfig, + quick_ranges: [ + { + display: 'Last 6 hours', + from: 'now-6h', + to: 'now', + }, + { + display: 'Last 3 days', + from: 'now-3d', + to: 'now', + }, + ], + }, + }; + + const oldModel = new DashboardModel(dashboard); + const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard); + + expect(scene.state.controls?.state.timePicker.state.quickRanges).toBe(dashboard.timepicker.quick_ranges); + }); +}); + function buildGridItemForTest(saveModel: Partial): { gridItem: DashboardGridItem; vizPanel: VizPanel } { const gridItem = buildGridItemForPanel(new PanelModel(saveModel)); if (gridItem instanceof DashboardGridItem) { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index b271697a87e..cd65a502b7c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -283,7 +283,9 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, $data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }), controls: new DashboardControls({ variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], - timePicker: new SceneTimePicker({}), + timePicker: new SceneTimePicker({ + quickRanges: oldModel.timepicker.quick_ranges, + }), refreshPicker: new SceneRefreshPicker({ refresh: oldModel.refresh, intervals: oldModel.timepicker.refresh_intervals, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index d9576c47fd0..9d6061e3c6f 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -1057,6 +1057,33 @@ describe('transformSceneToSaveModel', () => { }); }); +describe('Given a scene with custom quick ranges', () => { + it('should save quick ranges to save model', () => { + const dashboardWithCustomSettings = { + ...dashboard_to_load1, + timepicker: { + ...dashboard_to_load1.timepicker, + quick_ranges: [ + { + display: 'Last 6 hours', + from: 'now-6h', + to: 'now', + }, + { + display: 'Last 3 days', + from: 'now-3d', + to: 'now', + }, + ], + }, + }; + const scene = transformSaveModelToScene({ dashboard: dashboardWithCustomSettings as DashboardDataDTO, meta: {} }); + const saveModel = transformSceneToSaveModel(scene); + + expect(saveModel).toMatchSnapshot(); + }); +}); + export function buildGridItemFromPanelSchema(panel: Partial) { return buildGridItemForPanel(new PanelModel(panel)); } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 89710e4c7f1..b9f280b4a42 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -100,6 +100,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa refresh_intervals: refreshPicker?.state.intervals, hidden: controlsState?.hideTimeControls, nowDelay: timeRange.UNSAFE_nowDelay, + quick_ranges: controlsState?.timePicker.state.quickRanges, }, defaultTimePickerConfig ); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index 050565bc711..7e438b72b56 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -404,6 +404,18 @@ function getMinimalSceneState(body: DashboardLayoutManager): Partial Date: Tue, 4 Mar 2025 11:56:35 +0100 Subject: [PATCH 05/25] Feature: Introduce subresource integrity checks (SRI) for frontend assets (#100983) * feat(featuremgmt): introduce feature toggle for enabling sri checks * feat(frontend): use assetSriChecks feature toggle to inject integrity hash into script tags * chore(webpack): align sri algorithms across dev and prod builds * docs(featuremgmt): update assetSriChecks to pass CI * docs(featuremgmt): fix more spelling complaints with assetSriChecks * Add crossorigin attribute * chore(webpack): add subresource-integrity plugin * build(webpack): wrap webpack jsonp loader integrity checks in feature flag checks * revert(index.html): remove crossorigin attribute if assertSriChecks is disabled --------- Co-authored-by: Kristian Bremberg --- .../feature-toggles/index.md | 1 + package.json | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 7 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++ pkg/services/featuremgmt/toggles_gen.json | 16 +++++ public/views/index.html | 27 ++++++-- .../plugins/FeatureFlaggedSriPlugin.js | 66 +++++++++++++++++++ scripts/webpack/webpack.dev.js | 1 + scripts/webpack/webpack.prod.js | 8 +++ yarn.lock | 14 ++++ 13 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 scripts/webpack/plugins/FeatureFlaggedSriPlugin.js diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index f40ad6a1766..7ceb0dc3460 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -226,6 +226,7 @@ Experimental features might be changed or removed without prior notice. | `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page | | `newLogsPanel` | Enables the new logs panel in Explore | | `pluginsCDNSyncLoader` | Load plugins from CDN synchronously | +| `assetSriChecks` | Enables SRI checks for Grafana JavaScript assets | ## Development feature toggles diff --git a/package.json b/package.json index 1c9393c4bd9..3708b2b49ac 100644 --- a/package.json +++ b/package.json @@ -248,6 +248,7 @@ "webpack-livereload-plugin": "3.0.2", "webpack-manifest-plugin": "5.0.0", "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "^5.2.0-rc.1", "webpackbar": "^7.0.0", "yaml": "^2.0.0", "yargs": "^17.5.1" diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 87c5ff7c0d1..7e3ac0d6dba 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -254,4 +254,5 @@ export interface FeatureToggles { alertingRuleVersionHistoryRestore?: boolean; newShareReportDrawer?: boolean; rendererDisableAppPluginsPreload?: boolean; + assetSriChecks?: boolean; } diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 13520e2a02b..a201b5e4b23 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -13,6 +13,7 @@ const ( grafanaBackendServicesSquad codeowner = "@grafana/grafana-backend-services-squad" grafanaSearchAndStorageSquad codeowner = "@grafana/search-and-storage" grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" + grafanaFrontendOpsWG codeowner = "@grafana/frontend-ops" grafanaAsCodeSquad codeowner = "@grafana/grafana-as-code" identityAccessTeam codeowner = "@grafana/identity-access-team" grafanaObservabilityLogsSquad codeowner = "@grafana/observability-logs" diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f3b1da31b5c..27da9533003 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1775,6 +1775,13 @@ var ( HideFromDocs: true, FrontendOnly: true, }, + { + Name: "assetSriChecks", + Description: "Enables SRI checks for Grafana JavaScript assets", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendOpsWG, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index fc0b0c8dfb7..ace09d69f56 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -235,3 +235,4 @@ alertingJiraIntegration,experimental,@grafana/alerting-squad,false,false,true alertingRuleVersionHistoryRestore,GA,@grafana/alerting-squad,false,false,true newShareReportDrawer,experimental,@grafana/sharing-squad,false,false,false rendererDisableAppPluginsPreload,experimental,@grafana/sharing-squad,false,false,true +assetSriChecks,experimental,@grafana/frontend-ops,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 56c3a7d53da..d651c7290f6 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -950,4 +950,8 @@ const ( // FlagRendererDisableAppPluginsPreload // Disable pre-loading app plugins when the request is coming from the renderer FlagRendererDisableAppPluginsPreload = "rendererDisableAppPluginsPreload" + + // FlagAssetSriChecks + // Enables SRI checks for Grafana JavaScript assets + FlagAssetSriChecks = "assetSriChecks" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 3f5045123d9..b093cb5dd21 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -598,6 +598,22 @@ "codeowner": "@grafana/grafana-frontend-platform" } }, + { + "metadata": { + "name": "assetSriChecks", + "resourceVersion": "1739984409734", + "creationTimestamp": "2025-02-19T15:56:59Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-02-19 17:00:09.734088 +0000 UTC" + } + }, + "spec": { + "description": "Enables SRI checks for Grafana JavaScript assets", + "stage": "experimental", + "codeowner": "@grafana/frontend-ops", + "frontend": true + } + }, { "metadata": { "name": "authAPIAccessTokenAuth", diff --git a/public/views/index.html b/public/views/index.html index cd539b05c9e..d7c54f4967b 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -355,13 +355,26 @@ [[end]] - [[range $asset := .Assets.JSFiles]] - + [[if .Settings.FeatureToggles.assetSriChecks ]] + [[range $asset := .Assets.JSFiles]] + + [[end]] + [[else]] + [[range $asset := .Assets.JSFiles]] + + [[end]] [[end]]