Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87621cdfa3 | |||
| 16314a64ae | |||
| 6d4d242b8a | |||
| f3b2e6e24e | |||
| c9725f6353 | |||
| 612f1ded71 | |||
| fa7c455484 | |||
| 2447dfe51c | |||
| ca4382f79b | |||
| d9f7227140 | |||
| 7fb26e69a0 | |||
| b23f94e46f | |||
| 15da43ac62 | |||
| d67ecbc64c | |||
| 2059de205a | |||
| d6050a5675 | |||
| 79a434eb74 |
@@ -44,14 +44,7 @@ local_resource(
|
||||
)
|
||||
|
||||
# --- Docker Compose
|
||||
# define service overrides needed for running with enterprise
|
||||
# this mounts the dev license into the grafana-api service
|
||||
base_config = read_yaml('./docker-compose.yaml')
|
||||
base_volumes = base_config['services']['grafana-api']['volumes']
|
||||
enterprise_overrides = {'services':{'grafana-api': {'volumes': base_volumes + ['../../data/license.jwt:/grafana/data/license.jwt'] }}}
|
||||
|
||||
# check if license exists and apply enterprise overrides if so
|
||||
docker_compose(["./docker-compose.yaml", encode_yaml(enterprise_overrides)]) if os.path.exists("../../data/license.jwt") else docker_compose("./docker-compose.yaml")
|
||||
docker_compose("./docker-compose.yaml")
|
||||
dc_resource("proxy",
|
||||
resource_deps=["grafana-api", "frontend-service"],
|
||||
labels=["services"]
|
||||
@@ -73,7 +66,7 @@ dc_resource("postgres", labels=["misc"])
|
||||
dc_resource("tempo-init", labels=["misc"])
|
||||
|
||||
# paths in tilt files are confusing....
|
||||
# - if tilt is dealing with the path, it is relative to the Tiltfile
|
||||
# - if tilt is dealing the the path, it is relative to the Tiltfile
|
||||
# - if docker is dealing with the path, it is relative to the context
|
||||
docker_build('grafana-fs-dev',
|
||||
# Set the docker context to the root of the repo
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Support running this file from tilt (where the cwd is devenv/frontend-service), or directly from the root
|
||||
if [[ -f build-grafana.sh ]]; then
|
||||
cd ../../
|
||||
fi
|
||||
cd ../../
|
||||
|
||||
echo "Go mod cache: $(go env GOMODCACHE), $(ls -1 $(go env GOMODCACHE) | wc -l) items"
|
||||
echo "Go build cache: $(go env GOCACHE), $(ls -1 $(go env GOCACHE) | wc -l) items"
|
||||
|
||||
# The docker container, even on macOS, is linux, so we need to cross-compile
|
||||
# on macOS hosts to work on linux.
|
||||
@@ -17,16 +17,7 @@ fi
|
||||
# Need to build version into the binary so plugin compatibility works correctly
|
||||
VERSION=$(jq -r .version package.json)
|
||||
|
||||
# Build enterprise if it is linked in
|
||||
EXTRA_TAGS=""
|
||||
if [[ -f pkg/extensions/ext.go ]]; then
|
||||
EXTRA_TAGS="-tags enterprise"
|
||||
fi
|
||||
|
||||
# EXTRA_TAGS is intentionally unquoted to build the command
|
||||
# shellcheck disable=SC2086
|
||||
go build -v \
|
||||
-ldflags "-X main.version=${VERSION}" \
|
||||
-gcflags "all=-N -l" \
|
||||
${EXTRA_TAGS} \
|
||||
-o ./devenv/frontend-service/build/grafana ./pkg/cmd/grafana
|
||||
|
||||
@@ -8,7 +8,6 @@ services:
|
||||
dockerfile: proxy.dockerfile
|
||||
volumes:
|
||||
- ../../public/build:/cdn/public/build
|
||||
- ../../public/app/plugins:/cdn/public/app/plugins
|
||||
- ../../public/fonts:/cdn/public/fonts
|
||||
ports:
|
||||
- '3000:80' # Gateway
|
||||
|
||||
@@ -53,10 +53,6 @@ The following will help you get started working with Elasticsearch and Grafana:
|
||||
|
||||
## Supported Elasticsearch versions
|
||||
|
||||
{{< admonition type="warning" >}}
|
||||
The Elasticsearch data source plugin currently does not support Elastic Cloud Serverless, or any other serverless variant of Elasticsearch.
|
||||
{{< /admonition >}}
|
||||
|
||||
This data source supports these versions of Elasticsearch:
|
||||
|
||||
- ≥ v7.17
|
||||
|
||||
@@ -146,10 +146,6 @@ To simplify syntax and to allow for dynamic components, such as date range filte
|
||||
Use macros in the `SELECT` clause to simplify the creation of time series queries.
|
||||
From the **Data operations** drop-down, choose a macro such as `$\_\_timeGroup` or `$\_\_timeGroupAlias`. Then, select a time column from the **Column** drop-down and a time interval from the **Interval** drop-down. This generates a time-series query based on your selected time grouping.
|
||||
|
||||
{{< admonition type="warning" >}}
|
||||
Time macros (`$__time`, `$__timeFilter`, etc.) don't support time zone parameters in Microsoft SQL Server and always expand to UTC values. If your timestamps aren't stored in UTC (common with `datetime`/`datetime2` types), convert them to UTC in your SQL query using `AT TIME ZONE … AT TIME ZONE 'UTC'` rather than passing a time zone argument to a macro.
|
||||
{{< /admonition >}}
|
||||
|
||||
| **Macro** | **Description** |
|
||||
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `$__time(dateColumn)` | Renames the specified column to `_time`. <br/>Example: `dateColumn AS time` |
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/node": "22.15.0",
|
||||
"@types/prismjs": "1.26.4",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/uuid": "9.0.8",
|
||||
"glob": "10.4.1",
|
||||
@@ -34,8 +34,8 @@
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"rxjs": "7.8.1",
|
||||
"tslib": "2.6.3"
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/node": "22.15.0",
|
||||
"@types/prismjs": "1.26.4",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/uuid": "9.0.8",
|
||||
"glob": "10.4.1",
|
||||
@@ -34,8 +34,8 @@
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"rxjs": "7.8.1",
|
||||
"tslib": "2.6.3"
|
||||
|
||||
+6
-6
@@ -61,7 +61,7 @@
|
||||
"start:noLint": "yarn start -- --env noTsCheck=1 --env noLint=1",
|
||||
"stats": "webpack --mode production --config scripts/webpack/webpack.prod.js --profile --json > compilation-stats.json",
|
||||
"storybook": "yarn workspace @grafana/ui storybook --ci",
|
||||
"storybook:build": "yarn workspace @grafana/ui storybook:build",
|
||||
"storybook:build": "mkdir -p ./packages/grafana-ui/dist/storybook",
|
||||
"themes-schema": "typescript-json-schema ./tsconfig.json NewThemeOptions --include 'packages/grafana-data/src/themes/createTheme.ts' --out public/app/features/theme-playground/schema.generated.json",
|
||||
"themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
|
||||
"themes:usage": "eslint . --ignore-pattern '*.test.ts*' --ignore-pattern '*.spec.ts*' --cache --plugin '@grafana' --rule '{ @grafana/theme-token-usage: \"error\" }'",
|
||||
@@ -141,8 +141,8 @@
|
||||
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.3.0",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/prismjs": "1.26.5",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react-grid-layout": "1.3.5",
|
||||
"@types/react-highlight-words": "0.20.0",
|
||||
"@types/react-resizable": "3.0.8",
|
||||
@@ -240,7 +240,7 @@
|
||||
"prettier": "3.6.2",
|
||||
"prom-client": "^15.1.3",
|
||||
"publint": "^0.3.12",
|
||||
"react-refresh": "0.14.0",
|
||||
"react-refresh": "0.16.0",
|
||||
"react-select-event": "5.5.1",
|
||||
"redux-mock-store": "1.5.5",
|
||||
"rimraf": "6.0.1",
|
||||
@@ -388,9 +388,9 @@
|
||||
"rc-slider": "11.1.9",
|
||||
"rc-tree": "5.13.1",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-diff-viewer-continued": "^3.4.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-draggable": "4.5.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch",
|
||||
|
||||
@@ -91,12 +91,12 @@
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/papaparse": "5.3.16",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"esbuild": "0.25.8",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"rimraf": "6.0.1",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-esbuild": "6.2.1",
|
||||
@@ -104,7 +104,7 @@
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@leeoniya/ufuzzy": "1.0.19",
|
||||
"d3": "^7.8.5",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-use": "17.6.0",
|
||||
"react-virtualized-auto-sizer": "1.0.26",
|
||||
"tinycolor2": "1.6.0",
|
||||
@@ -68,7 +68,7 @@
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.8",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"babel-jest": "29.7.0",
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grafana/assistant": "^0.1.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:package", "type:ui"],
|
||||
"tags": ["type:ui"],
|
||||
"targets": {
|
||||
"build": {}
|
||||
}
|
||||
|
||||
@@ -36,16 +36,16 @@
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/systemjs": "6.15.3",
|
||||
"jest": "^29.6.4",
|
||||
"react": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"ts-jest": "29.4.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
"@reduxjs/toolkit": "2.9.0",
|
||||
"@types/debounce-promise": "3.1.9",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react-highlight-words": "0.20.0",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/semver": "7.7.1",
|
||||
@@ -94,8 +94,8 @@
|
||||
"i18next-parser": "9.3.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-select-event": "5.5.1",
|
||||
"rimraf": "6.0.1",
|
||||
"rollup": "^4.22.4",
|
||||
@@ -105,7 +105,7 @@
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:package", "type:ui"],
|
||||
"tags": ["type:ui"],
|
||||
"targets": {
|
||||
"build": {}
|
||||
}
|
||||
|
||||
@@ -78,12 +78,12 @@
|
||||
"@types/history": "4.7.11",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"esbuild": "0.25.8",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"rimraf": "6.0.1",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-esbuild": "6.2.1",
|
||||
@@ -92,7 +92,7 @@
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:package", "type:ui"],
|
||||
"tags": ["type:ui"],
|
||||
"targets": {
|
||||
"build": {}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
"@react-awesome-query-builder/ui": "6.6.15",
|
||||
"immutable": "5.1.4",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-select": "5.10.2",
|
||||
"react-use": "17.6.0",
|
||||
"react-virtualized-auto-sizer": "1.0.26",
|
||||
@@ -43,8 +43,8 @@
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.8",
|
||||
"@types/systemjs": "6.15.3",
|
||||
"@types/uuid": "10.0.0",
|
||||
|
||||
@@ -167,9 +167,9 @@
|
||||
"@types/mock-raf": "1.0.6",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/prismjs": "1.26.5",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-color": "3.0.13",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react-highlight-words": "0.20.0",
|
||||
"@types/react-transition-group": "4.4.12",
|
||||
"@types/react-window": "1.8.8",
|
||||
@@ -190,8 +190,8 @@
|
||||
"msw": "^2.10.2",
|
||||
"msw-storybook-addon": "^2.0.5",
|
||||
"process": "^0.11.10",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-select-event": "^5.1.0",
|
||||
"rimraf": "6.0.1",
|
||||
"rollup": "^4.22.4",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:package", "type:ui"],
|
||||
"tags": ["type:ui"],
|
||||
"targets": {
|
||||
"build": {}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,6 @@ export const getModalStyles = (theme: GrafanaTheme2) => {
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(0.5),
|
||||
zIndex: 1,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -105,8 +105,8 @@ func GolangContainer(
|
||||
opts *BuildOpts,
|
||||
) (*dagger.Container, error) {
|
||||
os, _ := OSAndArch(distro)
|
||||
// Only use viceroy for all darwin builds
|
||||
if opts.CGOEnabled && os == "darwin" {
|
||||
// Only use viceroy for all darwin and only windows/amd64
|
||||
if opts.CGOEnabled && (os == "darwin" || distro == DistWindowsAMD64) {
|
||||
return ViceroyContainer(d, log, distro, goVersion, viceroyVersion, opts)
|
||||
}
|
||||
|
||||
@@ -125,9 +125,7 @@ func GolangContainer(
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/s390x-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/s390x-linux-musl-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/riscv64-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/x86_64-w64-mingw32-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/x86_64-w64-mingw32-cross.tgz", "-C", "/toolchain"})
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"})
|
||||
}
|
||||
return WithGoEnv(log, container, distro, opts)
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ func BuildOptsStaticS390X(distro Distribution, experiments []string, tags []stri
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOptsStaticRiscv64 builds Grafana statically for the riscv64 arch
|
||||
// BuildOptsStaticS390X builds Grafana statically for the s390x arch
|
||||
func BuildOptsStaticRiscv64(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
|
||||
var (
|
||||
os, _ = OSAndArch(distro)
|
||||
@@ -280,22 +280,6 @@ func BuildOptsStaticRiscv64(distro Distribution, experiments []string, tags []st
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOptsStaticWindows builds Grafana statically for Windows on amd64
|
||||
func BuildOptsStaticWindows(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
|
||||
var (
|
||||
os, _ = OSAndArch(distro)
|
||||
)
|
||||
|
||||
return &GoBuildOpts{
|
||||
CC: "/toolchain/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-gcc",
|
||||
CXX: "/toolchain/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-cpp",
|
||||
ExperimentalFlags: experiments,
|
||||
OS: os,
|
||||
Arch: "amd64",
|
||||
CGOEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
func StdZigBuildOpts(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
|
||||
var (
|
||||
os, arch = OSAndArch(distro)
|
||||
@@ -380,7 +364,7 @@ var DistributionGoOpts = map[Distribution]DistroBuildOptsFunc{
|
||||
|
||||
// Non-Linux distros can have whatever they want in CC and CXX; it'll get overridden
|
||||
// but it's probably not best to rely on that.
|
||||
DistWindowsAMD64: BuildOptsStaticWindows,
|
||||
DistWindowsAMD64: ViceroyBuildOpts,
|
||||
DistWindowsARM64: StdZigBuildOpts,
|
||||
DistDarwinAMD64: ViceroyBuildOpts,
|
||||
DistDarwinARM64: ViceroyBuildOpts,
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
DELETE FROM {{ .Ident .TeamMemberTable }}
|
||||
WHERE uid = {{ .Arg .Command.UID }}
|
||||
@@ -38,7 +38,6 @@ type LegacyIdentityStore interface {
|
||||
ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error)
|
||||
ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error)
|
||||
UpdateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamMemberCommand) (*UpdateTeamMemberResult, error)
|
||||
DeleteTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd DeleteTeamMemberCommand) error
|
||||
}
|
||||
|
||||
var _ LegacyIdentityStore = (*legacySQLStore)(nil)
|
||||
|
||||
@@ -97,12 +97,6 @@ func TestIdentityQueries(t *testing.T) {
|
||||
return &v
|
||||
}
|
||||
|
||||
deleteTeamMember := func(q *DeleteTeamMemberCommand) sqltemplate.SQLTemplate {
|
||||
v := newDeleteTeamMember(nodb, q)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
return &v
|
||||
}
|
||||
|
||||
deleteTeam := func(q *DeleteTeamCommand) sqltemplate.SQLTemplate {
|
||||
v := newDeleteTeam(nodb, q)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
@@ -300,14 +294,6 @@ func TestIdentityQueries(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
sqlDeleteTeamMemberQuery: {
|
||||
{
|
||||
Name: "delete_team_member_basic",
|
||||
Data: deleteTeamMember(&DeleteTeamMemberCommand{
|
||||
UID: "team-member-1",
|
||||
}),
|
||||
},
|
||||
},
|
||||
sqlQueryUserTeamsTemplate: {
|
||||
{
|
||||
Name: "team_1_members_page_1",
|
||||
|
||||
@@ -368,61 +368,6 @@ func (s *legacySQLStore) UpdateTeamMember(ctx context.Context, ns claims.Namespa
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
type DeleteTeamMemberCommand struct {
|
||||
UID string
|
||||
}
|
||||
|
||||
var sqlDeleteTeamMemberQuery = mustTemplate("delete_team_member_query.sql")
|
||||
|
||||
func newDeleteTeamMember(sql *legacysql.LegacyDatabaseHelper, cmd *DeleteTeamMemberCommand) deleteTeamMemberQuery {
|
||||
return deleteTeamMemberQuery{
|
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
|
||||
TeamMemberTable: sql.Table("team_member"),
|
||||
Command: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
type deleteTeamMemberQuery struct {
|
||||
sqltemplate.SQLTemplate
|
||||
TeamMemberTable string
|
||||
Command *DeleteTeamMemberCommand
|
||||
}
|
||||
|
||||
func (r deleteTeamMemberQuery) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *legacySQLStore) DeleteTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd DeleteTeamMemberCommand) error {
|
||||
sql, err := s.sql(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := newDeleteTeamMember(sql, &cmd)
|
||||
if err := req.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error {
|
||||
teamMemberQuery, err := sqltemplate.Execute(sqlDeleteTeamMemberQuery, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute team member template %q: %w", sqlDeleteTeamMemberQuery.Name(), err)
|
||||
}
|
||||
|
||||
_, err = st.Exec(ctx, teamMemberQuery, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete team member: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanMember(rows *sql.Rows) (TeamMember, error) {
|
||||
m := TeamMember{}
|
||||
err := rows.Scan(&m.ID, &m.UID, &m.TeamUID, &m.TeamID, &m.UserUID, &m.UserID, &m.Name, &m.Email, &m.Username, &m.External, &m.Created, &m.Updated, &m.Permission)
|
||||
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
DELETE FROM `grafana`.`team_member`
|
||||
WHERE uid = 'team-member-1'
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
DELETE FROM "grafana"."team_member"
|
||||
WHERE uid = 'team-member-1'
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
DELETE FROM "grafana"."team_member"
|
||||
WHERE uid = 'team-member-1'
|
||||
@@ -125,34 +125,7 @@ func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo re
|
||||
}
|
||||
|
||||
func (l *LegacyBindingStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
if !l.enableAuthnMutation {
|
||||
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete")
|
||||
}
|
||||
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Check if the team binding exists
|
||||
_, err = l.Get(ctx, name, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
err = l.store.DeleteTeamMember(ctx, ns, legacy.DeleteTeamMemberCommand{
|
||||
UID: name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return &iamv0alpha1.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: ns.Value,
|
||||
},
|
||||
}, true, nil
|
||||
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete")
|
||||
}
|
||||
|
||||
func (l *LegacyBindingStore) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
|
||||
@@ -19,9 +19,6 @@ import (
|
||||
//
|
||||
// The usage of strings.ToLower is because the server would convert `FromAlert` to `Fromalert`. So the make matching
|
||||
// easier, we just match all headers in lower case.
|
||||
//
|
||||
// the headers X-Real-IP and X-Forwarded-For are used by the HostedGrafanaACHeaderMiddleware at
|
||||
// https://github.com/grafana/grafana/blob/f191acf8114ab79609fc631e0f01fe2b47371188/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go#L107
|
||||
var expectedHeaders = map[string]string{
|
||||
strings.ToLower(models.FromAlertHeaderName): models.FromAlertHeaderName,
|
||||
strings.ToLower(models.CacheSkipHeaderName): models.CacheSkipHeaderName,
|
||||
@@ -41,7 +38,6 @@ var expectedHeaders = map[string]string{
|
||||
strings.ToLower(queryService.HeaderDashboardTitle): queryService.HeaderDashboardTitle,
|
||||
strings.ToLower(queryService.HeaderPanelTitle): queryService.HeaderPanelTitle,
|
||||
strings.ToLower("X-Real-IP"): "X-Real-IP",
|
||||
strings.ToLower("X-Forwarded-For"): "X-Forwarded-For",
|
||||
}
|
||||
|
||||
func ExtractKnownHeaders(header http.Header) map[string]string {
|
||||
|
||||
@@ -28,7 +28,6 @@ func ConvertToK8sResource(orgID int64, r definitions.Route, version string, name
|
||||
RepeatInterval: optionalPrometheusDurationToString(r.RepeatInterval),
|
||||
Receiver: r.Receiver,
|
||||
},
|
||||
Routes: make([]model.RoutingTreeRoute, 0, len(r.Routes)),
|
||||
}
|
||||
for _, route := range r.Routes {
|
||||
if route == nil {
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authz"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/frontend"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
@@ -47,9 +46,8 @@ func NewModule(opts Options,
|
||||
license licensing.Licensing,
|
||||
moduleRegisterer ModuleRegisterer,
|
||||
storageBackend resource.StorageBackend, // Ensures unified storage backend is initialized
|
||||
hooksService *hooks.HooksService,
|
||||
) (*ModuleServer, error) {
|
||||
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license, moduleRegisterer, storageBackend, hooksService)
|
||||
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license, moduleRegisterer, storageBackend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,7 +70,6 @@ func newModuleServer(opts Options,
|
||||
license licensing.Licensing,
|
||||
moduleRegisterer ModuleRegisterer,
|
||||
storageBackend resource.StorageBackend,
|
||||
hooksService *hooks.HooksService,
|
||||
) (*ModuleServer, error) {
|
||||
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
||||
|
||||
@@ -96,7 +93,6 @@ func newModuleServer(opts Options,
|
||||
license: license,
|
||||
moduleRegisterer: moduleRegisterer,
|
||||
storageBackend: storageBackend,
|
||||
hooksService: hooksService,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
@@ -138,7 +134,6 @@ type ModuleServer struct {
|
||||
|
||||
// moduleRegisterer allows registration of modules provided by other builds (e.g. enterprise).
|
||||
moduleRegisterer ModuleRegisterer
|
||||
hooksService *hooks.HooksService
|
||||
}
|
||||
|
||||
// init initializes the server and its services.
|
||||
@@ -210,7 +205,7 @@ func (s *ModuleServer) Run() error {
|
||||
})
|
||||
|
||||
m.RegisterModule(modules.FrontendServer, func() (services.Service, error) {
|
||||
return frontend.ProvideFrontendService(s.cfg, s.features, s.promGatherer, s.registerer, s.license, s.hooksService)
|
||||
return frontend.ProvideFrontendService(s.cfg, s.features, s.promGatherer, s.registerer, s.license)
|
||||
})
|
||||
|
||||
m.RegisterModule(modules.OperatorServer, s.initOperatorServer)
|
||||
|
||||
@@ -27,8 +27,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/modules"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
@@ -332,10 +330,8 @@ func initModuleServerForTest(
|
||||
apiOpts api.ServerOptions,
|
||||
) testModuleServer {
|
||||
tracer := tracing.InitializeTracerForTest()
|
||||
hooksService := hooks.ProvideService()
|
||||
license := &licensing.OSSLicensingService{}
|
||||
|
||||
ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, license, ProvideNoopModuleRegisterer(), nil, hooksService)
|
||||
ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, nil, ProvideNoopModuleRegisterer(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
conn, err := grpc.NewClient(cfg.GRPCServer.Address,
|
||||
|
||||
@@ -1644,7 +1644,7 @@ func InitializeModuleServer(cfg *setting.Cfg, opts Options, apiOpts api.ServerOp
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, tracingService, ossLicensingService, moduleRegisterer, storageBackend, hooksService)
|
||||
moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, tracingService, ossLicensingService, moduleRegisterer, storageBackend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package ossaccesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@@ -24,8 +22,6 @@ type FolderPermissionsService struct {
|
||||
*resourcepermissions.Service
|
||||
}
|
||||
|
||||
var ErrFolderUnhandledError = errutil.Internal("folder.unhandled-error", errutil.WithPublicMessage("Unhandled folder error"))
|
||||
|
||||
var FolderViewActions = []string{dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead, libraryelements.ActionLibraryPanelsRead, accesscontrol.ActionAlertingSilencesRead}
|
||||
var FolderEditActions = append(FolderViewActions, []string{
|
||||
dashboards.ActionFoldersWrite,
|
||||
@@ -110,16 +106,7 @@ func ProvideFolderPermissions(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case func() bool {
|
||||
var errUtilErr errutil.Error
|
||||
return errors.As(err, &errUtilErr)
|
||||
}():
|
||||
return err
|
||||
case errors.Is(err, dashboards.ErrFolderNotFound):
|
||||
return folder.ErrFolderNotFound.Errorf("folder not found")
|
||||
}
|
||||
return ErrFolderUnhandledError.Errorf("unhandled folder error: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -25,8 +25,8 @@ type OpenFeatureConfig struct {
|
||||
URL *url.URL
|
||||
// HTTPClient is a pre-configured HTTP client (optional, used for GOFF provider)
|
||||
HTTPClient *http.Client
|
||||
// TypedFlags are the feature flags to use with static provider
|
||||
StaticFlags map[string]setting.TypedFeatureFlag
|
||||
// StaticFlags are the feature flags to use with static provider
|
||||
StaticFlags map[string]bool
|
||||
// TargetingKey is used for evaluation context
|
||||
TargetingKey string
|
||||
// ContextAttrs are additional attributes for evaluation context
|
||||
@@ -60,8 +60,7 @@ func InitOpenFeature(config OpenFeatureConfig) error {
|
||||
|
||||
// InitOpenFeatureWithCfg initializes OpenFeature from setting.Cfg
|
||||
func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
|
||||
// Read typed flags from config
|
||||
confFlags, err := setting.ReadTypedFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
|
||||
confFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read feature flags from config: %w", err)
|
||||
}
|
||||
@@ -97,7 +96,7 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
|
||||
func createProvider(
|
||||
providerType string,
|
||||
u *url.URL,
|
||||
staticFlags map[string]setting.TypedFeatureFlag,
|
||||
staticFlags map[string]bool,
|
||||
httpClient *http.Client,
|
||||
) (openfeature.FeatureProvider, error) {
|
||||
if providerType != setting.GOFFProviderType {
|
||||
|
||||
@@ -24,12 +24,12 @@ func CreateStaticEvaluator(cfg *setting.Cfg) (StaticFlagEvaluator, error) {
|
||||
return nil, fmt.Errorf("provider is not a static provider, type %s", setting.StaticProviderType)
|
||||
}
|
||||
|
||||
typedFlags, err := setting.ReadTypedFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
|
||||
staticFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read feature flags from config: %w", err)
|
||||
}
|
||||
|
||||
staticProvider, err := newStaticProvider(typedFlags)
|
||||
staticProvider, err := newStaticProvider(staticFlags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create static provider: %w", err)
|
||||
}
|
||||
@@ -56,7 +56,19 @@ type staticEvaluator struct {
|
||||
}
|
||||
|
||||
func (s *staticEvaluator) EvalFlag(ctx context.Context, flagKey string) (goffmodel.OFREPEvaluateSuccessResponse, error) {
|
||||
return s.evaluateFlagWithTypeDetection(ctx, flagKey)
|
||||
result, err := s.client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx))
|
||||
if err != nil {
|
||||
return goffmodel.OFREPEvaluateSuccessResponse{}, fmt.Errorf("failed to evaluate flag %s: %w", flagKey, err)
|
||||
}
|
||||
resp := goffmodel.OFREPEvaluateSuccessResponse{
|
||||
Key: flagKey,
|
||||
Value: result.Value,
|
||||
Reason: "static provider evaluation result",
|
||||
Variant: result.Variant,
|
||||
Metadata: result.FlagMetadata,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *staticEvaluator) EvalAllFlags(ctx context.Context) (goffmodel.OFREPBulkEvaluateSuccessResponse, error) {
|
||||
@@ -67,70 +79,24 @@ func (s *staticEvaluator) EvalAllFlags(ctx context.Context) (goffmodel.OFREPBulk
|
||||
|
||||
allFlags := make([]goffmodel.OFREPFlagBulkEvaluateSuccessResponse, 0, len(flags))
|
||||
for _, flagKey := range flags {
|
||||
result, err := s.evaluateFlagWithTypeDetection(ctx, flagKey)
|
||||
result, err := s.client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx))
|
||||
if err != nil {
|
||||
s.log.Error("failed to evaluate flag during bulk evaluation", "flagKey", flagKey, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
allFlags = append(allFlags, goffmodel.OFREPFlagBulkEvaluateSuccessResponse{
|
||||
OFREPEvaluateSuccessResponse: result,
|
||||
OFREPEvaluateSuccessResponse: goffmodel.OFREPEvaluateSuccessResponse{
|
||||
Key: flagKey,
|
||||
Value: result.Value,
|
||||
Reason: "static provider evaluation result",
|
||||
Variant: result.Variant,
|
||||
Metadata: result.FlagMetadata,
|
||||
},
|
||||
ErrorCode: string(result.ErrorCode),
|
||||
ErrorDetails: result.ErrorMessage,
|
||||
})
|
||||
}
|
||||
|
||||
return goffmodel.OFREPBulkEvaluateSuccessResponse{Flags: allFlags}, nil
|
||||
}
|
||||
|
||||
// evaluateFlagWithTypeDetection tries different flag types and returns the first successful evaluation result
|
||||
func (s *staticEvaluator) evaluateFlagWithTypeDetection(ctx context.Context, flagKey string) (goffmodel.OFREPEvaluateSuccessResponse, error) {
|
||||
// Try boolean evaluation first for backward compatibility
|
||||
result, err := s.client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx))
|
||||
if err == nil {
|
||||
return goffmodel.OFREPEvaluateSuccessResponse{
|
||||
Key: flagKey,
|
||||
Value: result.Value,
|
||||
Reason: "static provider evaluation result",
|
||||
Variant: result.Variant,
|
||||
Metadata: result.FlagMetadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If boolean evaluation fails, try other types
|
||||
s.log.Debug("boolean evaluation failed, trying other types", "flagKey", flagKey, "error", err)
|
||||
|
||||
// Try string evaluation
|
||||
if stringResult, stringErr := s.client.StringValueDetails(ctx, flagKey, "", openfeature.TransactionContext(ctx)); stringErr == nil {
|
||||
return goffmodel.OFREPEvaluateSuccessResponse{
|
||||
Key: flagKey,
|
||||
Value: stringResult.Value,
|
||||
Reason: "static provider evaluation result",
|
||||
Variant: stringResult.Variant,
|
||||
Metadata: stringResult.FlagMetadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Try number evaluation
|
||||
if numberResult, numberErr := s.client.FloatValueDetails(ctx, flagKey, 0.0, openfeature.TransactionContext(ctx)); numberErr == nil {
|
||||
return goffmodel.OFREPEvaluateSuccessResponse{
|
||||
Key: flagKey,
|
||||
Value: numberResult.Value,
|
||||
Reason: "static provider evaluation result",
|
||||
Variant: numberResult.Variant,
|
||||
Metadata: numberResult.FlagMetadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Try object evaluation
|
||||
if objectResult, objectErr := s.client.ObjectValueDetails(ctx, flagKey, map[string]interface{}{}, openfeature.TransactionContext(ctx)); objectErr == nil {
|
||||
return goffmodel.OFREPEvaluateSuccessResponse{
|
||||
Key: flagKey,
|
||||
Value: objectResult.Value,
|
||||
Reason: "static provider evaluation result",
|
||||
Variant: objectResult.Variant,
|
||||
Metadata: objectResult.FlagMetadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If all evaluations fail, return the original boolean error
|
||||
return goffmodel.OFREPEvaluateSuccessResponse{}, fmt.Errorf("failed to evaluate flag %s: %w", flagKey, err)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package featuremgmt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// inMemoryBulkProvider is a wrapper around memprovider.InMemoryProvider that
|
||||
@@ -33,59 +28,28 @@ func (p *inMemoryBulkProvider) ListFlags() ([]string, error) {
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// newStaticProvider creates a provider with support for different flag types
|
||||
func newStaticProvider(typedFlags map[string]setting.TypedFeatureFlag) (openfeature.FeatureProvider, error) {
|
||||
flags := make(map[string]memprovider.InMemoryFlag, len(standardFeatureFlags)+len(typedFlags))
|
||||
func newStaticProvider(confFlags map[string]bool) (openfeature.FeatureProvider, error) {
|
||||
flags := make(map[string]memprovider.InMemoryFlag, len(standardFeatureFlags))
|
||||
|
||||
// Add standard flags first (these are always boolean)
|
||||
for _, flag := range standardFeatureFlags {
|
||||
enabled := flag.Expression == "true"
|
||||
flags[flag.Name] = createBooleanFlag(flag.Name, enabled)
|
||||
// Add flags from config.ini file
|
||||
for name, value := range confFlags {
|
||||
flags[name] = createInMemoryFlag(name, value)
|
||||
}
|
||||
|
||||
// Add typed flags from config (these can override standard flags)
|
||||
for n, f := range typedFlags {
|
||||
flags[n] = createTypedFlag(n, f.Type, f.Value)
|
||||
// Add standard flags
|
||||
for _, flag := range standardFeatureFlags {
|
||||
if _, exists := flags[flag.Name]; !exists {
|
||||
enabled := flag.Expression == "true"
|
||||
flags[flag.Name] = createInMemoryFlag(flag.Name, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
return newInMemoryBulkProvider(flags), nil
|
||||
}
|
||||
|
||||
type FlagType string
|
||||
|
||||
const (
|
||||
FlagTypeBoolean FlagType = "boolean"
|
||||
FlagTypeString FlagType = "string"
|
||||
FlagTypeNumber FlagType = "number" // TODO: check in OFREP spec
|
||||
FlagTypeObject FlagType = "object"
|
||||
)
|
||||
|
||||
// TypedFlag represents a flag with its type and value
|
||||
type TypedFlag struct {
|
||||
Name string
|
||||
Type FlagType
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func createTypedFlag(name, flagType string, value interface{}) memprovider.InMemoryFlag {
|
||||
switch flagType {
|
||||
case "boolean":
|
||||
return createBooleanFlag(name, value.(bool))
|
||||
case "string":
|
||||
return createStringFlag(name, value.(string))
|
||||
case "number":
|
||||
return createNumberFlag(name, value.(float64))
|
||||
case "object":
|
||||
return createObjectFlag(name, value.(map[string]interface{}))
|
||||
default:
|
||||
// Default to boolean for backward compatibility
|
||||
return createBooleanFlag(name, false)
|
||||
}
|
||||
}
|
||||
|
||||
func createBooleanFlag(name string, value bool) memprovider.InMemoryFlag {
|
||||
func createInMemoryFlag(name string, enabled bool) memprovider.InMemoryFlag {
|
||||
variant := "disabled"
|
||||
if value {
|
||||
if enabled {
|
||||
variant = "enabled"
|
||||
}
|
||||
|
||||
@@ -98,55 +62,3 @@ func createBooleanFlag(name string, value bool) memprovider.InMemoryFlag {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createStringFlag(name string, value string) memprovider.InMemoryFlag {
|
||||
return memprovider.InMemoryFlag{
|
||||
Key: name,
|
||||
DefaultVariant: "default",
|
||||
Variants: map[string]interface{}{
|
||||
"default": value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createNumberFlag(name string, value float64) memprovider.InMemoryFlag {
|
||||
return memprovider.InMemoryFlag{
|
||||
Key: name,
|
||||
DefaultVariant: "default",
|
||||
Variants: map[string]interface{}{
|
||||
"default": value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createObjectFlag(name string, value map[string]interface{}) memprovider.InMemoryFlag {
|
||||
return memprovider.InMemoryFlag{
|
||||
Key: name,
|
||||
DefaultVariant: "default",
|
||||
Variants: map[string]interface{}{
|
||||
"default": value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// parseTypedFlagValue attempts to parse a string value into the appropriate type
|
||||
func parseTypedFlagValue(value string) (interface{}, FlagType, error) {
|
||||
// Try to parse as boolean
|
||||
if boolVal, err := strconv.ParseBool(value); err == nil {
|
||||
return boolVal, FlagTypeBoolean, nil
|
||||
}
|
||||
|
||||
// Try to parse as number
|
||||
if numVal, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return numVal, FlagTypeNumber, nil
|
||||
}
|
||||
|
||||
// Try to parse as JSON object
|
||||
var objVal map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(value), &objVal); err == nil {
|
||||
return objVal, FlagTypeObject, nil
|
||||
}
|
||||
|
||||
// Default to string
|
||||
return value, FlagTypeString, nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func Test_StaticProvider(t *testing.T) {
|
||||
t.Run("empty config loads standard flags", func(t *testing.T) {
|
||||
setup(t, []byte(``))
|
||||
// Check for one of the standard flags
|
||||
feat, err := openfeature.NewClient("").BooleanValueDetails(ctx, stFeatName, !stFeatValue, evalCtx)
|
||||
feat, err := openfeature.NewDefaultClient().BooleanValueDetails(ctx, stFeatName, !stFeatValue, evalCtx)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, stFeatValue == feat.Value)
|
||||
})
|
||||
|
||||
@@ -6,12 +6,8 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@@ -22,38 +18,24 @@ func (s *frontendService) contextMiddleware() web.Middleware {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
span := trace.SpanFromContext(ctx)
|
||||
ctx = setRequestContext(ctx)
|
||||
reqContext := &contextmodel.ReqContext{
|
||||
Context: web.FromContext(ctx),
|
||||
Logger: log.New("context"),
|
||||
}
|
||||
|
||||
// Preserve the original span so the setRequestContext span doesn't get propagated as a parent of the rest of the request
|
||||
ctx = trace.ContextWithSpan(ctx, span)
|
||||
// inject ReqContext in the context
|
||||
ctx = context.WithValue(ctx, ctxkey.Key{}, reqContext)
|
||||
|
||||
// Set the context for the http.Request.Context
|
||||
// This modifies both r and reqContext.Req since they point to the same value
|
||||
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
||||
|
||||
traceID := tracing.TraceIDFromContext(ctx, false)
|
||||
if traceID != "" {
|
||||
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setRequestContext(ctx context.Context) context.Context {
|
||||
ctx, span := tracing.Start(ctx, "setRequestContext")
|
||||
defer span.End()
|
||||
|
||||
reqContext := &contextmodel.ReqContext{
|
||||
Context: web.FromContext(ctx),
|
||||
Logger: log.New("context"),
|
||||
SignedInUser: &user.SignedInUser{},
|
||||
}
|
||||
|
||||
// inject ReqContext in the context
|
||||
ctx = context.WithValue(ctx, ctxkey.Key{}, reqContext)
|
||||
|
||||
// Set the context for the http.Request.Context
|
||||
// This modifies both r and reqContext.Req since they point to the same value
|
||||
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
||||
|
||||
traceID := tracing.TraceIDFromContext(ctx, false)
|
||||
if traceID != "" {
|
||||
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware/requestmeta"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
fswebassets "github.com/grafana/grafana/pkg/services/frontend/webassets"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -51,13 +50,13 @@ type frontendService struct {
|
||||
index *IndexProvider
|
||||
}
|
||||
|
||||
func ProvideFrontendService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, promGatherer prometheus.Gatherer, promRegister prometheus.Registerer, license licensing.Licensing, hooksService *hooks.HooksService) (*frontendService, error) {
|
||||
func ProvideFrontendService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, promGatherer prometheus.Gatherer, promRegister prometheus.Registerer, license licensing.Licensing) (*frontendService, error) {
|
||||
assetsManifest, err := fswebassets.GetWebAssets(cfg, license)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
index, err := NewIndexProvider(cfg, assetsManifest, license, hooksService)
|
||||
index, err := NewIndexProvider(cfg, assetsManifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,11 +11,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -27,7 +24,6 @@ func createTestService(t *testing.T, cfg *setting.Cfg) *frontendService {
|
||||
|
||||
features := featuremgmt.WithFeatures()
|
||||
license := &licensing.OSSLicensingService{}
|
||||
hooksService := hooks.ProvideService()
|
||||
|
||||
var promRegister prometheus.Registerer = prometheus.NewRegistry()
|
||||
promGatherer := promRegister.(*prometheus.Registry)
|
||||
@@ -36,7 +32,7 @@ func createTestService(t *testing.T, cfg *setting.Cfg) *frontendService {
|
||||
cfg.BuildVersion = "10.3.0"
|
||||
}
|
||||
|
||||
service, err := ProvideFrontendService(cfg, features, promGatherer, promRegister, license, hooksService)
|
||||
service, err := ProvideFrontendService(cfg, features, promGatherer, promRegister, license)
|
||||
require.NoError(t, err)
|
||||
|
||||
return service
|
||||
@@ -186,56 +182,3 @@ func TestFrontendService_Middleware(t *testing.T) {
|
||||
mux.ServeHTTP(recorder, req)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFrontendService_IndexHooks(t *testing.T) {
|
||||
publicDir := setupTestWebAssets(t)
|
||||
cfg := &setting.Cfg{
|
||||
HTTPPort: "3000",
|
||||
StaticRootPath: publicDir,
|
||||
BuildVersion: "10.3.0",
|
||||
}
|
||||
|
||||
t.Run("should handle hooks that modify buildInfo fields", func(t *testing.T) {
|
||||
service := createTestService(t, cfg)
|
||||
|
||||
// Add a hook that modifies various buildInfo fields
|
||||
service.index.hooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *contextmodel.ReqContext) {
|
||||
indexData.Settings.BuildInfo.Version = "99.99.99"
|
||||
indexData.Settings.BuildInfo.VersionString = "Custom Edition v99.99.99 (custom)"
|
||||
indexData.Settings.BuildInfo.Edition = "custom-edition"
|
||||
})
|
||||
|
||||
mux := web.New()
|
||||
service.addMiddlewares(mux)
|
||||
service.registerRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
mux.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
body := recorder.Body.String()
|
||||
assert.Contains(t, body, "99.99.99", "Hook should have modified the version")
|
||||
assert.Contains(t, body, "Custom Edition v99.99.99 (custom)", "Hook should have modified the version string")
|
||||
assert.Contains(t, body, "custom-edition", "Hook should have modified the edition")
|
||||
})
|
||||
|
||||
t.Run("should work without any hooks registered", func(t *testing.T) {
|
||||
service := createTestService(t, cfg)
|
||||
|
||||
mux := web.New()
|
||||
service.addMiddlewares(mux)
|
||||
service.registerRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
mux.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
body := recorder.Body.String()
|
||||
assert.Contains(t, body, "<div id=\"reactRoot\"></div>")
|
||||
// The build version comes from setting.BuildVersion (global), not cfg.BuildVersion
|
||||
// So we just check that the page renders successfully
|
||||
assert.Contains(t, body, "window.grafanaBootData")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
import "github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
// This is a copy of dtos.FrontendSettingsDTO with only the fields that the frontend-service
|
||||
// sends, to prevent default values from overriding what comes from the /bootdata call.
|
||||
@@ -23,8 +20,6 @@ type FSFrontendSettings struct {
|
||||
PasswordHint string `json:"passwordHint,omitempty"`
|
||||
AnonymousEnabled bool `json:"anonymousEnabled,omitempty"`
|
||||
|
||||
BuildInfo dtos.FrontendSettingsBuildInfoDTO `json:"buildInfo"`
|
||||
|
||||
GoogleAnalyticsId string `json:"googleAnalyticsId,omitempty"`
|
||||
GoogleAnalytics4Id string `json:"googleAnalytics4Id,omitempty"`
|
||||
GoogleAnalytics4SendManualPageViews bool `json:"GoogleAnalytics4SendManualPageViews,omitempty"`
|
||||
|
||||
@@ -12,18 +12,13 @@ import (
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type IndexProvider struct {
|
||||
log logging.Logger
|
||||
index *template.Template
|
||||
data IndexViewData
|
||||
hooksService *hooks.HooksService
|
||||
log logging.Logger
|
||||
index *template.Template
|
||||
data IndexViewData
|
||||
}
|
||||
|
||||
type IndexViewData struct {
|
||||
@@ -56,14 +51,12 @@ var (
|
||||
htmlTemplates = template.Must(template.New("html").Delims("[[", "]]").ParseFS(templatesFS, `*.html`))
|
||||
)
|
||||
|
||||
func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets, license licensing.Licensing, hooksService *hooks.HooksService) (*IndexProvider, error) {
|
||||
func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets) (*IndexProvider, error) {
|
||||
t := htmlTemplates.Lookup("index.html")
|
||||
if t == nil {
|
||||
return nil, fmt.Errorf("missing index template")
|
||||
}
|
||||
|
||||
logger := logging.DefaultLogger.With("logger", "index-provider")
|
||||
|
||||
// subset of frontend settings needed for the login page
|
||||
// TODO what about enterprise settings here?
|
||||
frontendSettings := FSFrontendSettings{
|
||||
@@ -94,13 +87,13 @@ func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets, li
|
||||
RudderstackWriteKey: cfg.RudderstackWriteKey,
|
||||
TrustedTypesDefaultPolicyEnabled: (cfg.CSPEnabled && strings.Contains(cfg.CSPTemplate, "require-trusted-types-for")) || (cfg.CSPReportOnlyEnabled && strings.Contains(cfg.CSPReportOnlyTemplate, "require-trusted-types-for")),
|
||||
VerifyEmailEnabled: cfg.VerifyEmailEnabled,
|
||||
BuildInfo: getBuildInfo(license, cfg),
|
||||
}
|
||||
|
||||
defaultUser := dtos.CurrentUser{}
|
||||
|
||||
return &IndexProvider{
|
||||
log: logger,
|
||||
index: t,
|
||||
hooksService: hooksService,
|
||||
log: logging.DefaultLogger.With("logger", "index-provider"),
|
||||
index: t,
|
||||
data: IndexViewData{
|
||||
AppTitle: "Grafana",
|
||||
AppSubUrl: cfg.AppSubURL, // Based on the request?
|
||||
@@ -116,13 +109,13 @@ func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets, li
|
||||
|
||||
Assets: assetsManifest,
|
||||
Settings: frontendSettings,
|
||||
DefaultUser: dtos.CurrentUser{},
|
||||
DefaultUser: defaultUser,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.Request) {
|
||||
ctx, span := tracer.Start(request.Context(), "frontend.index.HandleRequest")
|
||||
_, span := tracer.Start(request.Context(), "frontend.index.HandleRequest")
|
||||
defer span.End()
|
||||
|
||||
if request.Method != "GET" {
|
||||
@@ -149,9 +142,6 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.
|
||||
writer.Header().Set("Content-Security-Policy-Report-Only", policy)
|
||||
}
|
||||
|
||||
reqCtx := contexthandler.FromContext(ctx)
|
||||
p.runIndexDataHooks(reqCtx, &data)
|
||||
|
||||
writer.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||
writer.WriteHeader(200)
|
||||
if err := p.index.Execute(writer, &data); err != nil {
|
||||
@@ -161,43 +151,3 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.
|
||||
panic(fmt.Sprintf("Error rendering index\n %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *IndexProvider) runIndexDataHooks(reqCtx *contextmodel.ReqContext, data *IndexViewData) {
|
||||
// Create a dummy struct to pass to the hooks, and then extract the data back out from it
|
||||
legacyIndexViewData := dtos.IndexViewData{
|
||||
Settings: &dtos.FrontendSettingsDTO{
|
||||
BuildInfo: data.Settings.BuildInfo,
|
||||
},
|
||||
}
|
||||
|
||||
p.hooksService.RunIndexDataHooks(&legacyIndexViewData, reqCtx)
|
||||
|
||||
data.Settings.BuildInfo = legacyIndexViewData.Settings.BuildInfo
|
||||
}
|
||||
|
||||
func getBuildInfo(license licensing.Licensing, cfg *setting.Cfg) dtos.FrontendSettingsBuildInfoDTO {
|
||||
version := setting.BuildVersion
|
||||
commit := setting.BuildCommit
|
||||
commitShort := getShortCommitHash(setting.BuildCommit, 10)
|
||||
buildstamp := setting.BuildStamp
|
||||
versionString := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, version, commitShort)
|
||||
|
||||
buildInfo := dtos.FrontendSettingsBuildInfoDTO{
|
||||
Version: version,
|
||||
VersionString: versionString,
|
||||
Commit: commit,
|
||||
CommitShort: commitShort,
|
||||
Buildstamp: buildstamp,
|
||||
Edition: license.Edition(),
|
||||
Env: cfg.Env,
|
||||
}
|
||||
|
||||
return buildInfo
|
||||
}
|
||||
|
||||
func getShortCommitHash(commitHash string, maxLength int) string {
|
||||
if len(commitHash) > maxLength {
|
||||
return commitHash[:maxLength]
|
||||
}
|
||||
return commitHash
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingTriage) {
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Triage", SubTitle: "Triage alerts", Id: "alert-triage", Url: s.cfg.AppSubURL + "/alerting/triage", Icon: "medkit", IsNew: true,
|
||||
Text: "Triage", SubTitle: "Triage alerts", Id: "alert-triage", Url: s.cfg.AppSubURL + "/alerting/triage", Icon: "medkit",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
func EmbeddedContactPointToGrafanaIntegrationConfig(e *definitions.EmbeddedContactPoint) (alertingModels.IntegrationConfig, error) {
|
||||
func EmbeddedContactPointToGrafanaIntegrationConfig(e definitions.EmbeddedContactPoint) (alertingModels.IntegrationConfig, error) {
|
||||
data, err := e.Settings.MarshalJSON()
|
||||
if err != nil {
|
||||
return alertingModels.IntegrationConfig{}, err
|
||||
|
||||
@@ -151,7 +151,7 @@ func (ecp *ContactPointService) CreateContactPoint(
|
||||
contactPoint apimodels.EmbeddedContactPoint,
|
||||
provenance models.Provenance,
|
||||
) (apimodels.EmbeddedContactPoint, error) {
|
||||
if err := ValidateContactPoint(ctx, &contactPoint, ecp.encryptionService.GetDecryptedValue); err != nil {
|
||||
if err := ValidateContactPoint(ctx, contactPoint, ecp.encryptionService.GetDecryptedValue); err != nil {
|
||||
return apimodels.EmbeddedContactPoint{}, fmt.Errorf("%w: %s", ErrValidation, err.Error())
|
||||
}
|
||||
|
||||
@@ -243,20 +243,14 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in
|
||||
if contactPoint.Settings == nil {
|
||||
return fmt.Errorf("%w: %s", ErrValidation, "settings should not be empty")
|
||||
}
|
||||
iType, err := alertingNotify.IntegrationTypeFromString(contactPoint.Type)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
|
||||
}
|
||||
typeSchema, ok := alertingNotify.GetSchemaVersionForIntegration(iType, schema.V1)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: failed to get secret keys for contact point type %s", ErrValidation, contactPoint.Type)
|
||||
}
|
||||
|
||||
// patch integration with the secrets from the existing version
|
||||
rawContactPoint, err := ecp.getContactPointDecrypted(ctx, orgID, contactPoint.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
typeSchema, ok := alertingNotify.GetSchemaVersionForIntegration(schema.IntegrationType(contactPoint.Type), schema.V1)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: failed to get secret keys for contact point type %s", ErrValidation, contactPoint.Type)
|
||||
}
|
||||
for _, secretPath := range typeSchema.GetSecretFieldsPaths() {
|
||||
secretKey := secretPath.String()
|
||||
secretValue := contactPoint.Settings.Get(secretKey).MustString()
|
||||
@@ -266,7 +260,7 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in
|
||||
}
|
||||
|
||||
// validate merged values
|
||||
if err := ValidateContactPoint(ctx, &contactPoint, ecp.encryptionService.GetDecryptedValue); err != nil {
|
||||
if err := ValidateContactPoint(ctx, contactPoint, ecp.encryptionService.GetDecryptedValue); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
|
||||
}
|
||||
|
||||
@@ -518,12 +512,7 @@ groupLoop:
|
||||
return oldReceiverName, fullRemoval, newReceiverCreated
|
||||
}
|
||||
|
||||
func ValidateContactPoint(ctx context.Context, e *apimodels.EmbeddedContactPoint, decryptFunc alertingNotify.GetDecryptedValueFn) error {
|
||||
iType, err := alertingNotify.IntegrationTypeFromString(e.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.Type = string(iType)
|
||||
func ValidateContactPoint(ctx context.Context, e apimodels.EmbeddedContactPoint, decryptFunc alertingNotify.GetDecryptedValueFn) error {
|
||||
integration, err := EmbeddedContactPointToGrafanaIntegrationConfig(e)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/grafana/alerting/notify"
|
||||
"github.com/grafana/alerting/notify/notifytest"
|
||||
"github.com/grafana/alerting/receivers/schema"
|
||||
"github.com/grafana/alerting/receivers/slack"
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -147,21 +146,6 @@ func TestIntegrationContactPointService(t *testing.T) {
|
||||
require.ErrorIs(t, err, ErrValidation)
|
||||
})
|
||||
|
||||
t.Run("create accepts contact point with type in different cases", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(t, secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
newCp.Type = "Slack"
|
||||
|
||||
created, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, slack.Type, created.Type)
|
||||
|
||||
got, err := sut.GetContactPoints(context.Background(), cpsQueryWithName(1, newCp.Name), redactedUser)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 1)
|
||||
assert.EqualValues(t, slack.Type, got[0].Type)
|
||||
})
|
||||
|
||||
t.Run("update rejects contact points with no settings", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(t, secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
@@ -198,22 +182,6 @@ func TestIntegrationContactPointService(t *testing.T) {
|
||||
require.ErrorIs(t, err, ErrValidation)
|
||||
})
|
||||
|
||||
t.Run("update accepts contact points with type in another case", func(t *testing.T) {
|
||||
sut := createContactPointServiceSut(t, secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
newCp.Type = "Slack"
|
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := sut.GetContactPoints(context.Background(), cpsQueryWithName(1, newCp.Name), redactedUser)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 1)
|
||||
assert.EqualValues(t, slack.Type, got[0].Type)
|
||||
})
|
||||
|
||||
t.Run("update renames references when group is renamed", func(t *testing.T) {
|
||||
cfg := createEncryptedConfig(t, secretsService)
|
||||
store := fakes.NewFakeAlertmanagerConfigStore(cfg)
|
||||
|
||||
@@ -95,7 +95,7 @@ func (config *ReceiverV1) mapToModel(name string) (definitions.EmbeddedContactPo
|
||||
}
|
||||
// As the values are not encrypted when coming from disk files,
|
||||
// we can simply return the fallback for validation.
|
||||
err := provisioning.ValidateContactPoint(context.Background(), &cp, func(_ context.Context, _ map[string][]byte, _, fallback string) string {
|
||||
err := provisioning.ValidateContactPoint(context.Background(), cp, func(_ context.Context, _ map[string][]byte, _, fallback string) string {
|
||||
return fallback
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
@@ -46,78 +45,3 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str
|
||||
}
|
||||
return featureToggles, nil
|
||||
}
|
||||
|
||||
// TypedFeatureFlag represents a flag with its type and value
|
||||
type TypedFeatureFlag struct {
|
||||
Type string `json:"type"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
// ReadTypedFeatureTogglesFromInitFile reads feature flags with support for different types
|
||||
func ReadTypedFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]TypedFeatureFlag, error) {
|
||||
typedFlags := make(map[string]TypedFeatureFlag, 10)
|
||||
|
||||
// parse the comma separated list of values in `enable` key
|
||||
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
||||
for _, feature := range util.SplitString(featuresTogglesStr) {
|
||||
typedFlags[feature] = TypedFeatureFlag{
|
||||
Type: "boolean",
|
||||
Value: true,
|
||||
}
|
||||
}
|
||||
|
||||
// read all the other keys under [feature_toggles] section
|
||||
for _, v := range featureTogglesSection.Keys() {
|
||||
if v.Name() == "enable" {
|
||||
continue
|
||||
}
|
||||
|
||||
value := v.Value()
|
||||
|
||||
// try to determine the type of flag value
|
||||
flagType, parsedValue, err := parseTypedFlagValue(value)
|
||||
if err != nil {
|
||||
// upon failure, default to boolean for backward compatibility
|
||||
if boolVal, boolErr := strconv.ParseBool(value); boolErr == nil {
|
||||
typedFlags[v.Name()] = TypedFeatureFlag{
|
||||
Type: "boolean",
|
||||
Value: boolVal,
|
||||
}
|
||||
} else {
|
||||
// treat as string if even parsing as boolean fails
|
||||
typedFlags[v.Name()] = TypedFeatureFlag{
|
||||
Type: "string",
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
typedFlags[v.Name()] = TypedFeatureFlag{
|
||||
Type: flagType,
|
||||
Value: parsedValue,
|
||||
}
|
||||
}
|
||||
}
|
||||
return typedFlags, nil
|
||||
}
|
||||
|
||||
// parseTypedFlagValue attempts to parse a string value
|
||||
// into the appropriate type - bool, float, object
|
||||
// defaults to string
|
||||
func parseTypedFlagValue(value string) (string, interface{}, error) {
|
||||
if boolVal, err := strconv.ParseBool(value); err == nil {
|
||||
return "boolean", boolVal, nil
|
||||
}
|
||||
|
||||
// TODO: probably int is needed as well
|
||||
|
||||
if numVal, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return "number", numVal, nil
|
||||
}
|
||||
|
||||
var objVal map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(value), &objVal); err == nil {
|
||||
return "object", objVal, nil
|
||||
}
|
||||
|
||||
return "string", value, nil
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestIntegrationTeamBindings(t *testing.T) {
|
||||
}
|
||||
|
||||
func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper, team *unstructured.Unstructured, user *unstructured.Unstructured) {
|
||||
t.Run("should create/update/get/delete team binding using the new APIs", func(t *testing.T) {
|
||||
t.Run("should create/update/get team binding using the new APIs", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
@@ -137,18 +137,6 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
require.Equal(t, "member", fetchedSpec["permission"])
|
||||
require.Equal(t, false, fetchedSpec["external"])
|
||||
require.Equal(t, createdUID, fetched.GetName())
|
||||
|
||||
// Delete the team binding
|
||||
err = teamBindingClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the team binding is deleted
|
||||
_, err = teamBindingClient.Resource.Get(ctx, createdUID, metav1.GetOptions{})
|
||||
require.Error(t, err)
|
||||
var statusErr *errors.StatusError
|
||||
require.ErrorAs(t, err, &statusErr)
|
||||
require.Equal(t, int32(404), statusErr.ErrStatus.Code)
|
||||
require.Contains(t, statusErr.ErrStatus.Message, "not found")
|
||||
})
|
||||
|
||||
t.Run("should not be able to create team binding when using a user with insufficient permissions", func(t *testing.T) {
|
||||
|
||||
@@ -127,7 +127,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, backend.DownstreamErrorf("unsupported query type: '%s' for query with refID '%s'", q.QueryType, q.RefID)
|
||||
return nil, fmt.Errorf("unsupported query type: '%s' for query with refID '%s'", q.QueryType, q.RefID)
|
||||
}
|
||||
|
||||
if res != nil {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Drawer, Stack, Text } from '@grafana/ui';
|
||||
import { Button, Drawer, Stack } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { ManagerKind } from 'app/features/apiserver/types';
|
||||
import { BulkDeleteProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkDeleteProvisionedResource';
|
||||
@@ -152,12 +152,7 @@ export function BrowseActions({ folderDTO }: Props) {
|
||||
{/* bulk delete */}
|
||||
{showBulkDeleteProvisionedResource && (
|
||||
<Drawer
|
||||
title={
|
||||
// Heading levels should only increase by one (a11y)
|
||||
<Text variant="h3" element="h2">
|
||||
{t('browse-dashboards.action.bulk-delete-provisioned-resources', 'Bulk Delete Provisioned Resources')}
|
||||
</Text>
|
||||
}
|
||||
title={t('browse-dashboards.action.bulk-delete-provisioned-resources', 'Bulk Delete Provisioned Resources')}
|
||||
onClose={() => setShowBulkDeleteProvisionedResource(false)}
|
||||
size="md"
|
||||
>
|
||||
@@ -174,12 +169,7 @@ export function BrowseActions({ folderDTO }: Props) {
|
||||
{/* bulk move */}
|
||||
{showBulkMoveProvisionedResource && (
|
||||
<Drawer
|
||||
title={
|
||||
// Heading levels should only increase by one (a11y)
|
||||
<Text variant="h3" element="h2">
|
||||
{t('browse-dashboards.action.bulk-move-provisioned-resources', 'Bulk Move Provisioned Resources')}
|
||||
</Text>
|
||||
}
|
||||
title={t('browse-dashboards.action.bulk-move-provisioned-resources', 'Bulk Move Provisioned Resources')}
|
||||
onClose={() => setShowBulkMoveProvisionedResource(false)}
|
||||
size="md"
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useState } from 'react';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem, Text } from '@grafana/ui';
|
||||
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
|
||||
import { Permissions } from 'app/core/components/AccessControl/Permissions';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { RepoType } from 'app/features/provisioning/Wizard/types';
|
||||
@@ -180,11 +180,7 @@ export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props)
|
||||
)}
|
||||
{showDeleteProvisionedFolderDrawer && (
|
||||
<Drawer
|
||||
title={
|
||||
<Text variant="h3" element="h2">
|
||||
{t('browse-dashboards.action.delete-provisioned-folder', 'Delete provisioned folder')}
|
||||
</Text>
|
||||
}
|
||||
title={t('browse-dashboards.action.delete-provisioned-folder', 'Delete provisioned folder')}
|
||||
subtitle={folder.title}
|
||||
onClose={() => setShowDeleteProvisionedFolderDrawer(false)}
|
||||
>
|
||||
@@ -196,11 +192,7 @@ export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props)
|
||||
)}
|
||||
{showMoveProvisionedFolderDrawer && (
|
||||
<Drawer
|
||||
title={
|
||||
<Text variant="h3" element="h2">
|
||||
{t('browse-dashboards.action.move-provisioned-folder', 'Move provisioned folder')}
|
||||
</Text>
|
||||
}
|
||||
title={t('browse-dashboards.action.move-provisioned-folder', 'Move provisioned folder')}
|
||||
subtitle={folder.title}
|
||||
onClose={() => setShowMoveProvisionedFolderDrawer(false)}
|
||||
>
|
||||
|
||||
@@ -1,46 +1,26 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Badge } from '@grafana/ui';
|
||||
import { useGetRepositoryQuery } from 'app/api/clients/provisioning/v0alpha1';
|
||||
import { ManagerKind } from 'app/features/apiserver/types';
|
||||
import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types';
|
||||
import { DashboardMeta } from 'app/types/dashboard';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
|
||||
export const ManagedDashboardNavBarBadge = ({ dashboard }: { dashboard: DashboardScene }) => {
|
||||
const kind = dashboard.getManagerKind();
|
||||
const id = dashboard.getManagerIdentity();
|
||||
|
||||
const shouldSkipQuery = !config.featureToggles.provisioning || kind !== ManagerKind.Repo || !id;
|
||||
const { data: repoData } = useGetRepositoryQuery(shouldSkipQuery ? skipToken : { name: id });
|
||||
|
||||
if (!kind) {
|
||||
return null;
|
||||
export default function ManagedDashboardNavBarBadge({ meta }: { meta: DashboardMeta }) {
|
||||
const obj = meta.k8s;
|
||||
if (!obj?.annotations) {
|
||||
return;
|
||||
}
|
||||
|
||||
let text;
|
||||
|
||||
let text = 'Provisioned';
|
||||
const kind = obj.annotations?.[AnnoKeyManagerKind];
|
||||
const id = obj.annotations?.[AnnoKeyManagerIdentity];
|
||||
switch (kind) {
|
||||
case ManagerKind.Terraform:
|
||||
text = t('dashboard-scene.managed-badge.terraform', 'Managed by: Terraform');
|
||||
text = 'Terraform';
|
||||
break;
|
||||
case ManagerKind.Kubectl:
|
||||
text = t('dashboard-scene.managed-badge.kubectl', 'Managed by: Kubectl');
|
||||
text = 'Kubectl';
|
||||
break;
|
||||
case ManagerKind.Plugin:
|
||||
text = t('dashboard-scene.managed-badge.plugin', 'Managed by: Plugin {{id}}', { id });
|
||||
text = `Plugin: ${id}`;
|
||||
break;
|
||||
case ManagerKind.Repo:
|
||||
text = t('dashboard-scene.managed-badge.repository', 'Managed by: Repository {{title}}', {
|
||||
title: repoData?.spec?.title || id,
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown kind ' + kind);
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Badge color="purple" icon="exchange-alt" tooltip={text} key="provisioned-dashboard-button-badge" />;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import { isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
|
||||
import { ManagedDashboardNavBarBadge } from './ManagedDashboardNavBarBadge';
|
||||
import ManagedDashboardNavBarBadge from './ManagedDashboardNavBarBadge';
|
||||
import { LeftActions } from './new-toolbar/LeftActions';
|
||||
import { RightActions } from './new-toolbar/RightActions';
|
||||
import { PublicDashboardBadge } from './new-toolbar/actions/PublicDashboardBadge';
|
||||
@@ -145,7 +145,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
group: 'icon-actions',
|
||||
condition: true,
|
||||
render: () => {
|
||||
return <ManagedDashboardNavBarBadge dashboard={dashboard} key="managed-dashboard-badge" />;
|
||||
return <ManagedDashboardNavBarBadge meta={meta} key="managed-dashboard-badge" />;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { ToolbarButtonRow } from '@grafana/ui';
|
||||
|
||||
import { dynamicDashNavActions } from '../../utils/registerDynamicDashNavAction';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
import { ManagedDashboardNavBarBadge } from '../ManagedDashboardNavBarBadge';
|
||||
|
||||
import { ManagedDashboardBadge } from './actions/ManagedDashboardBadge';
|
||||
import { OpenSnapshotOriginButton } from './actions/OpenSnapshotOriginButton';
|
||||
import { PublicDashboardBadge } from './actions/PublicDashboardBadge';
|
||||
import { StarButton } from './actions/StarButton';
|
||||
@@ -40,7 +40,7 @@ export const LeftActions = ({ dashboard }: { dashboard: DashboardScene }) => {
|
||||
},
|
||||
{
|
||||
key: 'managed-dashboard-badge',
|
||||
component: ManagedDashboardNavBarBadge,
|
||||
component: ManagedDashboardBadge,
|
||||
group: 'actions',
|
||||
condition: dashboard.isManaged() && canEdit,
|
||||
},
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { Badge } from '@grafana/ui';
|
||||
import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types';
|
||||
|
||||
import { ToolbarActionProps } from '../types';
|
||||
|
||||
export const ManagedDashboardBadge = ({ dashboard }: ToolbarActionProps) => {
|
||||
if (!dashboard.state.meta.k8s?.annotations) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let text = 'Provisioned';
|
||||
const kind = dashboard.state.meta.k8s.annotations[AnnoKeyManagerKind];
|
||||
const id = dashboard.state.meta.k8s.annotations[AnnoKeyManagerIdentity];
|
||||
|
||||
switch (kind) {
|
||||
case ManagerKind.Terraform:
|
||||
text = 'Terraform';
|
||||
break;
|
||||
case ManagerKind.Kubectl:
|
||||
text = 'Kubectl';
|
||||
break;
|
||||
case ManagerKind.Plugin:
|
||||
text = `Plugin: ${id}`;
|
||||
break;
|
||||
case ManagerKind.Repo:
|
||||
text = 'Repository';
|
||||
break;
|
||||
}
|
||||
|
||||
return <Badge color="purple" icon="exchange-alt" tooltip={text} key="provisioned-dashboard-button-badge" />;
|
||||
};
|
||||
@@ -89,7 +89,7 @@ export const DashboardLibrarySection = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box borderRadius="lg" borderColor="strong" borderStyle="dashed" padding={4} flex={1}>
|
||||
<Box borderColor="strong" borderStyle="dashed" padding={4} flex={1}>
|
||||
<Stack direction="column" alignItems="center" gap={2}>
|
||||
<Text element="h3" textAlignment="center" weight="medium">
|
||||
<Trans i18nKey="dashboard.empty.start-with-suggested-dashboards">
|
||||
|
||||
@@ -608,7 +608,7 @@ function getLogVolumeFieldConfig(level: LogLevel, oneLevelDetected: boolean) {
|
||||
lineColor: color,
|
||||
pointColor: color,
|
||||
fillColor: color,
|
||||
lineWidth: 0,
|
||||
lineWidth: 1,
|
||||
fillOpacity: 100,
|
||||
stacking: {
|
||||
mode: StackingMode.Normal,
|
||||
|
||||
@@ -47,25 +47,6 @@ describe('pluginImporter', () => {
|
||||
expect(result).toEqual({ ...panelPlugin, meta: { ...panelPlugin } });
|
||||
});
|
||||
|
||||
it('should import a panel plugin returning a Promise<PanelPlugin> successfully', async () => {
|
||||
const spy = jest
|
||||
.spyOn(importPluginModule, 'importPluginModule')
|
||||
.mockResolvedValue({ plugin: Promise.resolve({ ...panelPlugin }) });
|
||||
|
||||
const result = await pluginImporter.importPanel({ ...panelPlugin });
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
path: 'public/plugins/test-plugin/module.js',
|
||||
version: '1.0.0',
|
||||
loadingStrategy: 'fetch',
|
||||
pluginId: 'test-plugin',
|
||||
moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b',
|
||||
translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ...panelPlugin, meta: { ...panelPlugin } });
|
||||
});
|
||||
|
||||
it('should set correct loading strategy', async () => {
|
||||
const spy = jest
|
||||
.spyOn(importPluginModule, 'importPluginModule')
|
||||
|
||||
@@ -48,8 +48,7 @@ const panelPluginPostImport: PostImportStrategy<PanelPlugin, PanelPluginMeta> =
|
||||
const pluginExports = await module;
|
||||
|
||||
if (pluginExports.plugin) {
|
||||
// pluginExports.plugin can either be a Promise<PanelPlugin> or a PanelPlugin
|
||||
const plugin: PanelPlugin = await pluginExports.plugin;
|
||||
const plugin: PanelPlugin = pluginExports.plugin;
|
||||
plugin.meta = meta;
|
||||
pluginsCache.set(meta.id, plugin);
|
||||
return plugin;
|
||||
|
||||
@@ -162,7 +162,7 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
<FormPrompt onDiscard={reset} confirmRedirect={isDirty} />
|
||||
<Stack direction="column" gap={2}>
|
||||
<Field noMargin label={t('provisioning.config-form.label-repository-type', 'Repository type')}>
|
||||
<Input id="repository-type" value={getRepositoryTypeConfig(type)?.label || type} disabled />
|
||||
<Input value={getRepositoryTypeConfig(type)?.label || type} disabled />
|
||||
</Field>
|
||||
<Field
|
||||
noMargin
|
||||
@@ -274,7 +274,7 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
/>
|
||||
</Field>
|
||||
<Field noMargin label={gitFields.pathConfig.label} description={gitFields.pathConfig.description}>
|
||||
<Input id="repository-path" {...register('path')} />
|
||||
<Input {...register('path')} />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -153,8 +153,7 @@ export default function GettingStarted({ items }: Props) {
|
||||
<Stack direction="column" gap={6} wrap="wrap">
|
||||
<Stack gap={10} alignItems="center">
|
||||
<div className={styles.imageContainer}>
|
||||
{/* decorative img, use empty str to skip alt*/}
|
||||
<img src={provisioningSvg} className={styles.image} alt="" />
|
||||
<img src={provisioningSvg} className={styles.image} alt={'Grafana provisioning'} />
|
||||
</div>
|
||||
<FeaturesList
|
||||
hasRequiredFeatures={hasRequiredFeatures}
|
||||
|
||||
@@ -79,7 +79,7 @@ export function JobContent({ jobType, job, isFinishedJob = false, onStatusChange
|
||||
<Stack direction="column" alignItems="center">
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
<Spinner size={24} />
|
||||
<Text element="h3" variant="h5" color="secondary">
|
||||
<Text element="h5" color="secondary">
|
||||
{message ?? state ?? t('provisioning.job-status.starting', 'Starting...')}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function RepositoryActions({ repository }: RepositoryActionsProps) {
|
||||
return (
|
||||
<Stack wrap="wrap">
|
||||
{isReadOnlyRepo && <Badge color="darkgrey" text={t('folder-repo.read-only-badge', 'Read only')} />}
|
||||
<StatusBadge repo={repository} displayOnly />
|
||||
<StatusBadge repo={repository} />
|
||||
{repoHref && (
|
||||
<Button variant="secondary" icon={providerIcon} onClick={() => window.open(repoHref, '_blank')}>
|
||||
<Trans i18nKey="provisioning.repository-actions.source-code">Source code</Trans>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { css } from '@emotion/css';
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface ProgressBarProps {
|
||||
@@ -25,14 +24,7 @@ const ProgressBar = ({ progress, topBottomSpacing }: ProgressBarProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
aria-label={t('provisioning.shared.progress-bar.aria-label', 'Progress Bar')}
|
||||
role="progressbar"
|
||||
aria-valuenow={progress}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={shouldAnimate ? styles.fillerAnimated : styles.filler} style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -27,17 +27,15 @@ export function RepositoryTypeCards() {
|
||||
<Stack direction="row" gap={1} wrap>
|
||||
{gitProviders.map((config) => (
|
||||
<Card key={config.type} href={`${CONNECT_URL}/${config.type}`} className={styles.card} noMargin>
|
||||
<Card.Heading>
|
||||
<Stack gap={2} alignItems="center">
|
||||
<RepoIcon type={config.type} />
|
||||
<Trans
|
||||
i18nKey="provisioning.repository-type-cards.configure-with-provider"
|
||||
values={{ provider: config.label }}
|
||||
>
|
||||
Configure with {'{{ provider }}'}
|
||||
</Trans>
|
||||
</Stack>
|
||||
</Card.Heading>
|
||||
<Stack gap={2} alignItems="center">
|
||||
<RepoIcon type={config.type} />
|
||||
<Trans
|
||||
i18nKey="provisioning.repository-type-cards.configure-with-provider"
|
||||
values={{ provider: config.label }}
|
||||
>
|
||||
Configure with {'{{ provider }}'}
|
||||
</Trans>
|
||||
</Stack>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -55,23 +53,21 @@ export function RepositoryTypeCards() {
|
||||
<Stack direction="row" gap={1} wrap>
|
||||
{otherProviders.map((config) => (
|
||||
<Card key={config.type} href={`${CONNECT_URL}/${config.type}`} className={styles.card} noMargin>
|
||||
<Card.Heading>
|
||||
<Stack gap={2} alignItems="center">
|
||||
<RepoIcon type={config.type} />
|
||||
{config.type === 'local' ? (
|
||||
<Trans i18nKey="provisioning.repository-type-cards.configure-file">
|
||||
Configure file provisioning
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="provisioning.repository-type-cards.configure-with-provider"
|
||||
values={{ provider: config.label }}
|
||||
>
|
||||
Configure with {'{{ provider }}'}
|
||||
</Trans>
|
||||
)}
|
||||
</Stack>
|
||||
</Card.Heading>
|
||||
<Stack gap={2} alignItems="center">
|
||||
<RepoIcon type={config.type} />
|
||||
{config.type === 'local' ? (
|
||||
<Trans i18nKey="provisioning.repository-type-cards.configure-file">
|
||||
Configure file provisioning
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="provisioning.repository-type-cards.configure-with-provider"
|
||||
values={{ provider: config.label }}
|
||||
>
|
||||
Configure with {'{{ provider }}'}
|
||||
</Trans>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -7,13 +7,9 @@ import { PROVISIONING_URL } from '../constants';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
repo?: Repository;
|
||||
displayOnly?: boolean; // if true, disable click action and cursor will be default
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Displays a status badge for the given provisioned repository.
|
||||
*/
|
||||
export function StatusBadge({ repo, displayOnly = false }: StatusBadgeProps) {
|
||||
export function StatusBadge({ repo }: StatusBadgeProps) {
|
||||
if (!repo) {
|
||||
return null;
|
||||
}
|
||||
@@ -90,12 +86,10 @@ export function StatusBadge({ repo, displayOnly = false }: StatusBadgeProps) {
|
||||
color={color}
|
||||
icon={icon}
|
||||
text={text}
|
||||
style={{ cursor: displayOnly ? 'default' : 'pointer' }}
|
||||
style={{ cursor: 'pointer' }}
|
||||
tooltip={tooltip}
|
||||
onClick={() => {
|
||||
if (!displayOnly) {
|
||||
locationService.push(`${PROVISIONING_URL}/${repo.metadata?.name}/?tab=overview`);
|
||||
}
|
||||
locationService.push(`${PROVISIONING_URL}/${repo.metadata?.name}/?tab=overview`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"lodash": "4.17.21",
|
||||
"monaco-editor": "0.34.1",
|
||||
"prismjs": "1.30.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-select": "5.10.2",
|
||||
"react-use": "17.6.0",
|
||||
"rxjs": "7.8.2",
|
||||
@@ -36,8 +36,8 @@
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/prismjs": "1.26.5",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"i18next-parser": "9.3.0",
|
||||
"jest": "29.7.0",
|
||||
"react-select-event": "5.5.1",
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"lodash": "4.17.21",
|
||||
"monaco-editor": "0.34.1",
|
||||
"prismjs": "1.30.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-select": "5.10.2",
|
||||
"react-use": "17.6.0",
|
||||
"rxjs": "7.8.2",
|
||||
@@ -37,8 +37,8 @@
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/prismjs": "1.26.5",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"jest": "29.7.0",
|
||||
"react-select-event": "5.5.1",
|
||||
"ts-node": "10.9.2",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"@grafana/sql": "12.3.0-pre",
|
||||
"@grafana/ui": "12.3.0-pre",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"rxjs": "7.8.2",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
@@ -24,7 +24,7 @@
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react": "19.0.10",
|
||||
"jest": "29.7.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"lodash": "4.17.21",
|
||||
"monaco-editor": "0.34.1",
|
||||
"prismjs": "1.30.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-use": "17.6.0",
|
||||
"rxjs": "7.8.2",
|
||||
"tslib": "2.8.1"
|
||||
@@ -29,8 +29,8 @@
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/prismjs": "1.26.5",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"css-loader": "7.1.2",
|
||||
"jest": "29.7.0",
|
||||
"style-loader": "4.0.0",
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"d3-random": "^3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"micro-memoize": "^4.1.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-select": "5.10.2",
|
||||
"react-use": "17.6.0",
|
||||
"rxjs": "7.8.2",
|
||||
@@ -31,8 +31,8 @@
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/uuid": "10.0.0",
|
||||
"jest": "29.7.0",
|
||||
"ts-node": "10.9.2",
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"@grafana/ui": "workspace:*",
|
||||
"lodash": "4.17.21",
|
||||
"logfmt": "^1.3.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-select": "5.10.2",
|
||||
"react-window": "1.8.11",
|
||||
"rxjs": "7.8.2",
|
||||
@@ -32,8 +32,8 @@
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/logfmt": "^1.2.3",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/uuid": "10.0.0",
|
||||
"jest": "29.7.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { DataQueryResponse, Field, FieldType, QueryResultMetaNotice, QueryResultMetaStat } from '@grafana/data';
|
||||
import { DataQueryResponse, Field, FieldType, QueryResultMetaStat } from '@grafana/data';
|
||||
|
||||
import { cloneQueryResponse, combineResponses } from './mergeResponses';
|
||||
import { getMockFrames } from './mocks/frames';
|
||||
@@ -76,7 +76,6 @@ describe('combineResponses', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -155,7 +154,6 @@ describe('combineResponses', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -201,7 +199,6 @@ describe('combineResponses', () => {
|
||||
length: 4,
|
||||
meta: {
|
||||
type: 'timeseries-multi',
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -247,7 +244,6 @@ describe('combineResponses', () => {
|
||||
length: 4,
|
||||
meta: {
|
||||
type: 'timeseries-multi',
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -428,7 +424,6 @@ describe('combineResponses', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -504,7 +499,6 @@ describe('combineResponses', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -580,7 +574,6 @@ describe('combineResponses', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -646,84 +639,6 @@ describe('combineResponses', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('combine notices', () => {
|
||||
const { metricFrameA } = getMockFrames();
|
||||
const makeResponse = (notices?: QueryResultMetaNotice[]): DataQueryResponse => ({
|
||||
data: [
|
||||
{
|
||||
...metricFrameA,
|
||||
meta: {
|
||||
...metricFrameA.meta,
|
||||
notices,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
it('combines notices from both frames', () => {
|
||||
const responseA = makeResponse([{ severity: 'warning', text: 'Warning from frame A' }]);
|
||||
const responseB = makeResponse([{ severity: 'info', text: 'Info from frame B' }]);
|
||||
|
||||
expect(combineResponses(responseA, responseB).data[0].meta?.notices).toStrictEqual([
|
||||
{ severity: 'warning', text: 'Warning from frame A' },
|
||||
{ severity: 'info', text: 'Info from frame B' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('deduplicates identical notices', () => {
|
||||
const responseA = makeResponse([{ severity: 'warning', text: 'Same warning' }]);
|
||||
const responseB = makeResponse([{ severity: 'warning', text: 'Same warning' }]);
|
||||
|
||||
expect(combineResponses(responseA, responseB).data[0].meta?.notices).toStrictEqual([
|
||||
{ severity: 'warning', text: 'Same warning' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps notices with same text but different severity', () => {
|
||||
const responseA = makeResponse([{ severity: 'warning', text: 'Message' }]);
|
||||
const responseB = makeResponse([{ severity: 'info', text: 'Message' }]);
|
||||
|
||||
expect(combineResponses(responseA, responseB).data[0].meta?.notices).toStrictEqual([
|
||||
{ severity: 'warning', text: 'Message' },
|
||||
{ severity: 'info', text: 'Message' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles one frame with notices and one without', () => {
|
||||
const responseA = makeResponse([{ severity: 'warning', text: 'Warning message' }]);
|
||||
const responseB = makeResponse();
|
||||
|
||||
expect(combineResponses(responseA, responseB).data[0].meta?.notices).toStrictEqual([
|
||||
{ severity: 'warning', text: 'Warning message' },
|
||||
]);
|
||||
|
||||
expect(combineResponses(responseB, responseA).data[0].meta?.notices).toStrictEqual([
|
||||
{ severity: 'warning', text: 'Warning message' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns empty array when neither frame has notices', () => {
|
||||
const responseA = makeResponse();
|
||||
const responseB = makeResponse();
|
||||
expect(combineResponses(responseA, responseB).data[0].meta?.notices).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('filters out null values from notices arrays', () => {
|
||||
const responseA = makeResponse([
|
||||
{ severity: 'warning', text: 'Valid warning' },
|
||||
null as unknown as QueryResultMetaNotice, // Simulating the bug scenario
|
||||
]);
|
||||
const responseB = makeResponse([{ severity: 'info', text: 'Valid info' }]);
|
||||
|
||||
const result = combineResponses(responseA, responseB).data[0].meta?.notices;
|
||||
expect(result).toStrictEqual([
|
||||
{ severity: 'warning', text: 'Valid warning' },
|
||||
{ severity: 'info', text: 'Valid info' },
|
||||
]);
|
||||
expect(result).not.toContainEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not combine frames with different refId', () => {
|
||||
const { metricFrameA, metricFrameB } = getMockFrames();
|
||||
metricFrameA.refId = 'A';
|
||||
@@ -815,7 +730,6 @@ describe('combineResponses', () => {
|
||||
length: 4,
|
||||
meta: {
|
||||
type: 'timeseries-multi',
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -882,7 +796,6 @@ describe('combineResponses', () => {
|
||||
length: 4,
|
||||
meta: {
|
||||
type: 'timeseries-multi',
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -930,7 +843,6 @@ describe('mergeFrames', () => {
|
||||
length: 4,
|
||||
meta: {
|
||||
type: 'timeseries-multi',
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -980,7 +892,6 @@ describe('mergeFrames', () => {
|
||||
length: 3,
|
||||
meta: {
|
||||
type: 'timeseries-multi',
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -1026,7 +937,6 @@ describe('mergeFrames', () => {
|
||||
length: 4,
|
||||
meta: {
|
||||
type: 'timeseries-multi',
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -1106,7 +1016,6 @@ describe('mergeFrames', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -1189,7 +1098,6 @@ describe('mergeFrames', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -1221,7 +1129,6 @@ describe('mergeFrames', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -1301,7 +1208,6 @@ describe('mergeFrames', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
@@ -1336,7 +1242,6 @@ describe('mergeFrames', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 22 }],
|
||||
},
|
||||
length: 2,
|
||||
@@ -1362,7 +1267,6 @@ describe('mergeFrames', () => {
|
||||
custom: {
|
||||
frameType: 'LabeledTimeValues',
|
||||
},
|
||||
notices: [],
|
||||
stats: [
|
||||
{
|
||||
displayName: 'Summary: total bytes processed',
|
||||
|
||||
@@ -7,59 +7,24 @@ import {
|
||||
Field,
|
||||
FieldType,
|
||||
LoadingState,
|
||||
QueryResultMetaNotice,
|
||||
QueryResultMetaStat,
|
||||
shallowCompare,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { LOADING_FRAME_NAME } from './querySplitting';
|
||||
|
||||
function getFrameKey(frame: DataFrame): string | undefined {
|
||||
// Metric range query data
|
||||
if (frame.meta?.type === DataFrameType.TimeSeriesMulti) {
|
||||
const field = frame.fields.find((f) => f.type === FieldType.number);
|
||||
if (!field) {
|
||||
throw new Error(`Unable to find number field on sharded dataframe!`);
|
||||
}
|
||||
let key = '';
|
||||
if (frame.refId) {
|
||||
key += frame.refId;
|
||||
}
|
||||
if (frame.name) {
|
||||
key += frame.name;
|
||||
}
|
||||
if (field.labels) {
|
||||
key += JSON.stringify(field.labels);
|
||||
}
|
||||
return key !== '' ? key : undefined;
|
||||
}
|
||||
return frame.refId ?? frame.name;
|
||||
}
|
||||
|
||||
export function combineResponses(currentResponse: DataQueryResponse | null, newResponse: DataQueryResponse) {
|
||||
if (!currentResponse) {
|
||||
return cloneQueryResponse(newResponse);
|
||||
}
|
||||
|
||||
const currentResponseLabelsMap = new Map<string, DataFrame>();
|
||||
currentResponse.data.forEach((frame: DataFrame) => {
|
||||
const key = getFrameKey(frame);
|
||||
// It is expected that all frames contain a refId or a name, but since the type allows for it
|
||||
// we need to account for possibly undefined cases.
|
||||
if (key) {
|
||||
currentResponseLabelsMap.set(key, frame);
|
||||
}
|
||||
});
|
||||
|
||||
newResponse.data.forEach((newFrame: DataFrame) => {
|
||||
let currentFrame: DataFrame | undefined = undefined;
|
||||
const key = getFrameKey(newFrame);
|
||||
if (key !== undefined && currentResponseLabelsMap.has(key)) {
|
||||
currentFrame = currentResponseLabelsMap.get(key);
|
||||
mergeFrames(currentFrame!, newFrame);
|
||||
} else {
|
||||
newResponse.data.forEach((newFrame) => {
|
||||
const currentFrame = currentResponse.data.find((frame) => shouldCombine(frame, newFrame));
|
||||
if (!currentFrame) {
|
||||
currentResponse.data.push(cloneDataFrame(newFrame));
|
||||
return;
|
||||
}
|
||||
mergeFrames(currentFrame, newFrame);
|
||||
});
|
||||
|
||||
const mergedErrors = [...(currentResponse.errors ?? []), ...(newResponse.errors ?? [])];
|
||||
@@ -198,7 +163,6 @@ export function mergeFrames(dest: DataFrame, source: DataFrame) {
|
||||
dest.meta = {
|
||||
...dest.meta,
|
||||
stats: getCombinedMetadataStats(dest.meta?.stats ?? [], source.meta?.stats ?? []),
|
||||
notices: getCombinedNotices(dest.meta?.notices ?? [], source.meta?.notices ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -290,27 +254,6 @@ function getCombinedMetadataStats(
|
||||
return stats;
|
||||
}
|
||||
|
||||
function getCombinedNotices(
|
||||
destNotices: QueryResultMetaNotice[],
|
||||
sourceNotices: QueryResultMetaNotice[]
|
||||
): QueryResultMetaNotice[] {
|
||||
// Combine notices from both frames and filter out null/undefined values
|
||||
const allNotices = [...destNotices, ...sourceNotices].filter(
|
||||
(notice): notice is QueryResultMetaNotice => notice != null
|
||||
);
|
||||
|
||||
// Deduplicate notices based on text to avoid showing the same warning twice
|
||||
const uniqueNotices = allNotices.reduce((acc: QueryResultMetaNotice[], notice) => {
|
||||
const exists = acc.some((n) => n.severity === notice.severity && n.text === notice.text);
|
||||
if (!exists) {
|
||||
acc.push(notice);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return uniqueNotices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clones a DataQueryResponse
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@grafana/sql": "12.3.0-pre",
|
||||
"@grafana/ui": "12.3.0-pre",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"rxjs": "7.8.2",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react": "19.0.10",
|
||||
"i18next-parser": "9.3.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"@grafana/sql": "12.3.0-pre",
|
||||
"@grafana/ui": "12.3.0-pre",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"rxjs": "7.8.2",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
@@ -24,7 +24,7 @@
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react": "19.0.10",
|
||||
"jest": "29.7.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"@grafana/ui": "12.3.0-pre",
|
||||
"lodash": "4.17.21",
|
||||
"monaco-editor": "0.34.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-use": "17.6.0",
|
||||
"rxjs": "7.8.2",
|
||||
"tslib": "2.8.1"
|
||||
@@ -24,8 +24,8 @@
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"jest": "29.7.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"lru-cache": "11.2.2",
|
||||
"monaco-editor": "0.34.1",
|
||||
"prismjs": "1.30.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-select": "5.10.2",
|
||||
"react-use": "17.6.0",
|
||||
"rxjs": "7.8.2",
|
||||
@@ -47,8 +47,8 @@
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/prismjs": "1.26.5",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/uuid": "10.0.0",
|
||||
"glob": "11.0.3",
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-select": "5.10.2",
|
||||
"react-use": "17.6.0",
|
||||
"rxjs": "7.8.2",
|
||||
@@ -27,8 +27,8 @@
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"jest": "29.7.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
|
||||
@@ -11910,11 +11910,6 @@
|
||||
"next": "Další",
|
||||
"previous": "Předchozí"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Dokončený krok",
|
||||
"label-current-step": "Aktuální krok",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "Weiter",
|
||||
"previous": "Zurück"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Abgeschlossener Schritt",
|
||||
"label-current-step": "Aktueller Schritt",
|
||||
|
||||
@@ -5986,12 +5986,6 @@
|
||||
"usage-count_one": "Used on {{count}} dashboards",
|
||||
"usage-count_other": "Used on {{count}} dashboards"
|
||||
},
|
||||
"managed-badge": {
|
||||
"kubectl": "Managed by: Kubectl",
|
||||
"plugin": "Managed by: Plugin {{id}}",
|
||||
"repository": "Managed by: Repository {{title}}",
|
||||
"terraform": "Managed by: Terraform"
|
||||
},
|
||||
"move-provisioned-dashboard-form": {
|
||||
"api-error": "Failed to move dashboard",
|
||||
"cancel-action": "Cancel",
|
||||
@@ -11816,11 +11810,6 @@
|
||||
"next": "Next",
|
||||
"previous": "Previous"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": "Progress Bar"
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Completed step",
|
||||
"label-current-step": "Current step",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "Siguiente",
|
||||
"previous": "Anterior"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Paso completado",
|
||||
"label-current-step": "Paso actual",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "Suivant",
|
||||
"previous": "Précédent"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Étape terminée",
|
||||
"label-current-step": "Étape actuelle",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "Következő",
|
||||
"previous": "Előző"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Befejezett lépés",
|
||||
"label-current-step": "Jelenlegi lépés",
|
||||
|
||||
@@ -11760,11 +11760,6 @@
|
||||
"next": "Berikutnya",
|
||||
"previous": "Sebelumnya"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Langkah selesai",
|
||||
"label-current-step": "Langkah saat ini",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "Avanti",
|
||||
"previous": "Precedente"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Passaggio completato",
|
||||
"label-current-step": "Passaggio corrente",
|
||||
|
||||
@@ -11760,11 +11760,6 @@
|
||||
"next": "次へ",
|
||||
"previous": "前へ"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "完了したステップ",
|
||||
"label-current-step": "現在のステップ",
|
||||
|
||||
@@ -11760,11 +11760,6 @@
|
||||
"next": "다음",
|
||||
"previous": "이전"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "완료된 단계",
|
||||
"label-current-step": "현재 단계",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "Volgende",
|
||||
"previous": "Vorige"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Voltooide stap",
|
||||
"label-current-step": "Huidige stap",
|
||||
|
||||
@@ -11910,11 +11910,6 @@
|
||||
"next": "Dalej",
|
||||
"previous": "Wstecz"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Krok ukończony",
|
||||
"label-current-step": "Aktualny krok",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "Avançar",
|
||||
"previous": "Anterior"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Etapa concluída",
|
||||
"label-current-step": "Etapa atual",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "Seguinte",
|
||||
"previous": "Anterior"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Passo concluído",
|
||||
"label-current-step": "Passo atual",
|
||||
|
||||
@@ -11910,11 +11910,6 @@
|
||||
"next": "Далее",
|
||||
"previous": "Назад"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Выполненный шаг",
|
||||
"label-current-step": "Текущий шаг",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "Nästa",
|
||||
"previous": "Föregående"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Slutfört steg",
|
||||
"label-current-step": "Nuvarande steg",
|
||||
|
||||
@@ -11810,11 +11810,6 @@
|
||||
"next": "İleri",
|
||||
"previous": "Önceki"
|
||||
},
|
||||
"shared": {
|
||||
"progress-bar": {
|
||||
"aria-label": ""
|
||||
}
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Tamamlanan adım",
|
||||
"label-current-step": "Mevcut adım",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user