diff --git a/.betterer.results b/.betterer.results index a9efbf6fe5e..4439c212997 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1400,6 +1400,9 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/hooks/useControlledFieldArray.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "public/app/features/alerting/unified/hooks/useFilteredRules.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/alerting/unified/insights/InsightsMenuButton.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/.citools/src/bra/go.mod b/.citools/src/bra/go.mod index 8a75cdfef83..b37f09f62f9 100644 --- a/.citools/src/bra/go.mod +++ b/.citools/src/bra/go.mod @@ -1,6 +1,6 @@ module bra -go 1.24.3 +go 1.24.4 tool github.com/unknwon/bra diff --git a/.citools/src/cog/go.mod b/.citools/src/cog/go.mod index 1258685195e..44a7a920f35 100644 --- a/.citools/src/cog/go.mod +++ b/.citools/src/cog/go.mod @@ -1,6 +1,6 @@ module cog -go 1.24.3 +go 1.24.4 tool github.com/grafana/cog/cmd/cli diff --git a/.citools/src/cue/go.mod b/.citools/src/cue/go.mod index 31cb9301000..f59e9af9ff6 100644 --- a/.citools/src/cue/go.mod +++ b/.citools/src/cue/go.mod @@ -1,6 +1,6 @@ module cue -go 1.24.3 +go 1.24.4 tool cuelang.org/go/cmd/cue diff --git a/.citools/src/golangci-lint/go.mod b/.citools/src/golangci-lint/go.mod index 9f2b7babff3..bdc3e9bba79 100644 --- a/.citools/src/golangci-lint/go.mod +++ b/.citools/src/golangci-lint/go.mod @@ -1,6 +1,6 @@ module golangci-lint -go 1.24.3 +go 1.24.4 tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint diff --git a/.citools/src/jb/go.mod b/.citools/src/jb/go.mod index 9e0ba331ae7..eec18890a50 100644 --- a/.citools/src/jb/go.mod +++ b/.citools/src/jb/go.mod @@ -1,6 +1,6 @@ module jb -go 1.24.3 +go 1.24.4 tool github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb diff --git a/.citools/src/lefthook/go.mod b/.citools/src/lefthook/go.mod index 2cc1154daf6..85e4a8e5c14 100644 --- a/.citools/src/lefthook/go.mod +++ b/.citools/src/lefthook/go.mod @@ -1,6 +1,6 @@ module lefthook -go 1.24.3 +go 1.24.4 tool github.com/evilmartians/lefthook diff --git a/.citools/src/swagger/go.mod b/.citools/src/swagger/go.mod index 5c4f8438646..33ca8159a8f 100644 --- a/.citools/src/swagger/go.mod +++ b/.citools/src/swagger/go.mod @@ -1,6 +1,6 @@ module swagger -go 1.24.3 +go 1.24.4 tool github.com/go-swagger/go-swagger/cmd/swagger diff --git a/.drone.yml b/.drone.yml index 298efd04173..40e64d7e17f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -25,7 +25,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - ./bin/build verify-drone @@ -75,7 +75,7 @@ steps: - go install github.com/bazelbuild/buildtools/buildifier@latest - buildifier --lint=warn -mode=check -r . depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: lint-starlark trigger: event: @@ -219,7 +219,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -228,21 +228,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m depends_on: - wire-install - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: test-backend - commands: - apk add --update build-base @@ -251,7 +251,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: test-backend-integration trigger: event: @@ -306,7 +306,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - echo $(/usr/bin/github-app-external-token) > /github-app/token @@ -351,16 +351,16 @@ steps: - apk add --update make - make gen-go depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: validate-openapi-spec trigger: event: @@ -437,7 +437,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -447,7 +447,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -456,7 +456,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - yarn install --immutable || yarn install --immutable @@ -507,7 +507,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: rgm-package pull: always volumes: @@ -892,7 +892,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -906,7 +906,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -915,14 +915,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -943,7 +943,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -964,7 +964,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -980,7 +980,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -996,7 +996,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -1012,7 +1012,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -1094,7 +1094,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue trigger: event: @@ -1215,7 +1215,7 @@ steps: && return 1; fi depends_on: - clone-enterprise - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: swagger-gen trigger: event: @@ -1320,7 +1320,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1331,7 +1331,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - clone-enterprise - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1341,14 +1341,14 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - clone-enterprise - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - apk add --update build-base @@ -1356,7 +1356,7 @@ steps: - go test -v -run=^$ -benchmem -timeout=1h -count=8 -bench=. ${GO_PACKAGES} depends_on: - wire-install - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: sqlite-benchmark-integration-tests - commands: - apk add --update build-base @@ -1368,7 +1368,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: postgres-benchmark-integration-tests - commands: - apk add --update build-base @@ -1379,7 +1379,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: mysql-8.0-benchmark-integration-tests trigger: event: @@ -1451,7 +1451,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue trigger: branch: main @@ -1500,7 +1500,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1509,21 +1509,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m depends_on: - wire-install - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: test-backend - commands: - apk add --update build-base @@ -1532,7 +1532,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: test-backend-integration trigger: branch: main @@ -1577,22 +1577,22 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - apk add --update make - make gen-go depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: validate-openapi-spec - commands: - ./bin/build verify-drone @@ -1724,7 +1724,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1734,7 +1734,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1743,7 +1743,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - yarn install --immutable || yarn install --immutable @@ -1793,7 +1793,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: rgm-package pull: always volumes: @@ -2253,7 +2253,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -2267,7 +2267,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2276,14 +2276,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -2304,7 +2304,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -2325,7 +2325,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -2341,7 +2341,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -2357,7 +2357,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -2373,7 +2373,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: remote-alertmanager-integration-tests trigger: branch: main @@ -2641,7 +2641,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2650,21 +2650,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m depends_on: - wire-install - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: test-backend - commands: - apk add --update build-base @@ -2673,7 +2673,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: test-backend-integration trigger: branch: @@ -2716,22 +2716,22 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - apk add --update make - make gen-go depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: validate-openapi-spec trigger: branch: @@ -2810,7 +2810,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -2824,7 +2824,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2833,14 +2833,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -2861,7 +2861,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -2882,7 +2882,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -2898,7 +2898,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -2914,7 +2914,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -2930,7 +2930,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: remote-alertmanager-integration-tests trigger: branch: @@ -3030,7 +3030,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - ./bin/build artifacts docker fetch --edition oss @@ -3160,7 +3160,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - ./bin/build artifacts docker fetch --edition oss @@ -3301,7 +3301,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - ./bin/build artifacts packages --artifacts-editions=oss --tag $${DRONE_TAG} --src-bucket @@ -3393,7 +3393,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - yarn install --immutable || yarn install --immutable @@ -3493,7 +3493,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - depends_on: - compile-build-cmd @@ -3590,7 +3590,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: compile-build-cmd - commands: - ./bin/build publish grafana-com --edition oss ${DRONE_TAG} @@ -3665,7 +3665,7 @@ steps: STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination UBUNTU_BASE: ubuntu:22.04 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: rgm-build pull: always volumes: @@ -3742,7 +3742,7 @@ steps: STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination UBUNTU_BASE: ubuntu:22.04 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: rgm-build pull: always volumes: @@ -3861,7 +3861,7 @@ steps: STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination UBUNTU_BASE: ubuntu:22.04 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: rgm-build pull: always volumes: @@ -4000,7 +4000,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -4009,21 +4009,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m depends_on: - wire-install - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: test-backend - commands: - apk add --update build-base @@ -4032,7 +4032,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: test-backend-integration trigger: cron: @@ -4100,7 +4100,7 @@ steps: STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination UBUNTU_BASE: ubuntu:22.04 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: rgm-build pull: always volumes: @@ -4246,7 +4246,7 @@ steps: STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination UBUNTU_BASE: ubuntu:22.04 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: rgm-publish pull: always volumes: @@ -4376,7 +4376,7 @@ steps: STORYBOOK_DESTINATION: from_secret: rgm_storybook_destination UBUNTU_BASE: ubuntu:22.04 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: rgm-build pull: always volumes: @@ -4502,7 +4502,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -4511,14 +4511,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -4539,7 +4539,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -4560,7 +4560,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -4576,7 +4576,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -4592,7 +4592,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -4608,7 +4608,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.24.3-alpine + image: golang:1.24.4-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -4911,7 +4911,7 @@ steps: - commands: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM docker:27-cli - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine/git:2.40.1 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.24.3-alpine + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.24.4-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22.11.0-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22-bookworm - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 @@ -4949,7 +4949,7 @@ steps: - commands: - trivy --exit-code 1 --severity HIGH,CRITICAL docker:27-cli - trivy --exit-code 1 --severity HIGH,CRITICAL alpine/git:2.40.1 - - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.24.3-alpine + - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.24.4-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL node:22.11.0-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL node:22-bookworm - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 @@ -5206,6 +5206,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: e430c51190ba46dbd426d13f47c13c275f4d05b6b1a26d1d9f67b5a8f09ecb41 +hmac: 7f318d682f431d61add0d60afcb9360f390217961709adeb88c34f11feec35b0 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7e90f5e6e6a..cde4501d4b2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -499,28 +499,28 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/canvas/ @grafana/dataviz-squad /public/app/features/geo/ @grafana/dataviz-squad /public/app/features/visualization/data-hover/ @grafana/dataviz-squad -/public/app/features/commandPalette/ @grafana/grafana-frontend-platform +/public/app/features/commandPalette/ @grafana/grafana-search-navigate-organise /public/app/features/connections/ @grafana/plugins-platform-frontend /public/app/features/correlations/ @grafana/datapro /public/app/features/dashboard/ @grafana/dashboards-squad /public/app/features/dashboard/components/TransformationsEditor/ @grafana/datapro /public/app/features/dashboard-scene/ @grafana/dashboards-squad -/public/app/features/scopes/ @grafana/dashboards-squad +/public/app/features/scopes/ @grafana/grafana-operator-experience-squad /public/app/features/datasources/ @grafana/plugins-platform-frontend /public/app/features/dimensions/ @grafana/dataviz-squad /public/app/features/dataframe-import/ @grafana/dataviz-squad /public/app/features/explore/ @grafana/observability-traces-and-profiling /public/app/features/expressions/ @grafana/grafana-datasources-core-services -/public/app/features/folders/ @grafana/grafana-frontend-platform +/public/app/features/folders/ @grafana/grafana-search-navigate-organise /public/app/features/inspector/ @grafana/dashboards-squad -/public/app/features/invites/ @grafana/grafana-frontend-platform +/public/app/features/invites/ @grafana/sharing-squad /public/app/features/library-panels/ @grafana/dashboards-squad /public/app/features/logs/ @grafana/observability-logs /public/app/features/live/ @grafana/dashboards-squad /public/app/features/apiserver/ @grafana/grafana-app-platform-squad /public/app/features/manage-dashboards/ @grafana/dashboards-squad -/public/app/features/notifications/ @grafana/grafana-frontend-platform -/public/app/features/org/ @grafana/grafana-frontend-platform +/public/app/features/notifications/ @grafana/grafana-search-navigate-organise +/public/app/features/org/ @grafana/grafana-search-navigate-organise /public/app/features/panel/ @grafana/dashboards-squad /public/app/features/playlist/ @grafana/dashboards-squad /public/app/features/plugins/ @grafana/plugins-platform-frontend @@ -528,8 +528,8 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/runtime/ @ryantxu /public/app/features/query/ @grafana/dashboards-squad /public/app/features/sandbox/ @grafana/grafana-frontend-platform -/public/app/features/browse-dashboards/ @grafana/grafana-frontend-platform -/public/app/features/search/ @grafana/grafana-frontend-platform +/public/app/features/browse-dashboards/ @grafana/grafana-search-navigate-organise +/public/app/features/search/ @grafana/grafana-search-navigate-organise /public/app/features/serviceaccounts/ @grafana/identity-squad /public/app/features/teams/ @grafana/access-squad /public/app/features/templating/ @grafana/dashboards-squad @@ -539,16 +539,16 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/users/ @grafana/access-squad /public/app/features/variables/ @grafana/dashboards-squad /public/app/features/preferences/ @grafana/grafana-frontend-platform -/public/app/features/bookmarks/ @grafana/grafana-frontend-platform +/public/app/features/bookmarks/ @grafana/grafana-search-navigate-organise /public/app/plugins/panel/alertlist/ @grafana/alerting-frontend -/public/app/plugins/panel/annolist/ @grafana/grafana-frontend-platform +/public/app/plugins/panel/annolist/ @grafana/dashboards-squad /public/app/plugins/panel/barchart/ @grafana/dataviz-squad /public/app/plugins/panel/bargauge/ @grafana/dataviz-squad -/public/app/plugins/panel/dashlist/ @grafana/grafana-frontend-platform +/public/app/plugins/panel/dashlist/ @grafana/grafana-search-navigate-organise /public/app/plugins/panel/debug/ @ryantxu /public/app/plugins/panel/datagrid/ @grafana/dataviz-squad /public/app/plugins/panel/gauge/ @grafana/dataviz-squad -/public/app/plugins/panel/gettingstarted/ @grafana/grafana-frontend-platform +/public/app/plugins/panel/gettingstarted/ @grafana/grafana-search-navigate-organise /public/app/plugins/panel/heatmap/ @grafana/dataviz-squad /public/app/plugins/panel/histogram/ @grafana/dataviz-squad /public/app/plugins/panel/logs/ @grafana/observability-logs @@ -567,12 +567,12 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/plugins/panel/canvas/ @grafana/dataviz-squad /public/app/plugins/panel/candlestick/ @grafana/dataviz-squad /public/app/plugins/panel/live/ @grafana/dashboards-squad -/public/app/plugins/panel/news/ @grafana/grafana-frontend-platform +/public/app/plugins/panel/news/ @grafana/dataviz-squad /public/app/plugins/panel/stat/ @grafana/dataviz-squad -/public/app/plugins/panel/text/ @grafana/grafana-frontend-platform -/public/app/plugins/panel/welcome/ @grafana/grafana-frontend-platform +/public/app/plugins/panel/text/ @grafana/dataviz-squad +/public/app/plugins/panel/welcome/ @grafana/grafana-search-navigate-organise /public/app/plugins/panel/xychart/ @grafana/dataviz-squad -/public/app/routes/ @grafana/grafana-frontend-platform +/public/app/routes/ @grafana/grafana-search-navigate-organise /public/app/store/ @grafana/grafana-frontend-platform /public/app/types/ @grafana/grafana-frontend-platform /public/app/types/alerting.ts @grafana/alerting-frontend @@ -600,7 +600,6 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/explore/NodeGraph/ @grafana/observability-traces-and-profiling /public/app/features/explore/FlameGraph/ @grafana/observability-traces-and-profiling /public/app/features/explore/TraceView/ @grafana/observability-traces-and-profiling -/public/app/features/explore/QueryLibrary/ @grafana/grafana-frontend-platform /public/api-merged.json @grafana/grafana-backend-group /public/api-enterprise-spec.json @grafana/grafana-backend-group @@ -642,7 +641,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /scripts/levitate-parse-json-report.js @grafana/plugins-platform-frontend /scripts/levitate-show-affected-plugins.js @grafana/plugins-platform-frontend /scripts/codemods/explicit-barrel-imports.cjs @grafana/frontend-ops -/scripts/rtk-client-generator/ @grafana/grafana-frontend-platform +/scripts/rtk-client-generator/ @grafana/grafana-search-navigate-organise /scripts/**/generate-transformations* @grafana/datapro /scripts/webpack/ @grafana/frontend-ops @@ -689,6 +688,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /pkg/services/dashboardsnapshots/ @grafana/sharing-squad /pkg/services/publicdashboards/ @grafana/sharing-squad /pkg/services/rendering/ @grafana/sharing-squad +/public/app/features/explore/QueryLibrary/ @grafana/sharing-squad # SSE - Server Side Expressions /pkg/expr/ @grafana/grafana-datasources-core-services @@ -836,7 +836,6 @@ embed.go @grafana/grafana-as-code /.github/workflows/frontend-lint.yml @grafana/grafana-frontend-platform /.github/workflows/analytics-events-report.yml @grafana/grafana-frontend-platform /.github/workflows/pr-e2e-tests.yml @grafana/grafana-developer-enablement-squad -/.github/workflows/run-e2e-suite.yml @grafana/grafana-developer-enablement-squad /.github/workflows/skye-add-to-project.yml @grafana/grafana-frontend-platform /.github/zizmor.yml @grafana/grafana-developer-enablement-squad /.github/license_finder.yaml @bergquist diff --git a/.github/workflows/backend-unit-tests.yml b/.github/workflows/backend-unit-tests.yml index e53be4d1d00..713155d3a45 100644 --- a/.github/workflows/backend-unit-tests.yml +++ b/.github/workflows/backend-unit-tests.yml @@ -74,6 +74,7 @@ jobs: contents: read id-token: write steps: + # Set up repository clone - name: Checkout code uses: actions/checkout@v4 with: @@ -86,18 +87,28 @@ jobs: uses: ./.github/actions/setup-enterprise with: github-app-name: 'grafana-ci-bot' + + # Prepare what we need to upload test results - run: echo "RESULTS_FILE=$(date --rfc-3339=seconds --utc | sed -s 's/ /-/g')_${SHARD/\//_}.xml" >> "$GITHUB_ENV" env: SHARD: ${{ matrix.shard }} - run: go install github.com/jstemmer/go-junit-report/v2@85bf4716ac1f025f2925510a9f5e9f5bb347c009 + + # Run code - name: Generate Go code run: make gen-go - name: Run unit tests env: SHARD: ${{ matrix.shard }} run: | + set -euo pipefail + readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/shard.sh -N"$SHARD")" + # This tee requires pipefail to be set, otherwise `go test`'s exit code is thrown away. + # That means having no `-o pipefail` => failing tests => exit code 0, which is wrong. go test -short -v -timeout=30m "${PACKAGES[@]}" | tee >(go-junit-report -set-exit-code > "$RESULTS_FILE") + + # Upload results to GCS - name: Log in to GCS if: github.repository == 'grafana/grafana' && (success() || failure()) uses: grafana/shared-workflows/actions/login-to-gcs@login-to-gcs-v0.2.0 diff --git a/.github/workflows/detect-breaking-changes-levitate.yml b/.github/workflows/detect-breaking-changes-levitate.yml index 61999c718a9..c793791b6d2 100644 --- a/.github/workflows/detect-breaking-changes-levitate.yml +++ b/.github/workflows/detect-breaking-changes-levitate.yml @@ -314,7 +314,7 @@ jobs: "fields": [ { "type": "mrkdwn", - "text": "*PR:* <${{ github.event.pull_request.html_url }}|#${{ github.event.pull_request.number }}>" + "text": "*PR:* <${{ github.event.pull_request.html_url }}|#${{ github.event.pull_request.number }}>\n\nAuthor: ${{ github.event.pull_request.user.login }}" }, { "type": "mrkdwn", diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index c88813150c5..dc7381d2cfc 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -39,39 +39,98 @@ jobs: retention-days: 1 name: ${{ steps.artifact.outputs.artifact }} path: grafana.tar.gz - e2e-matrix: + + build-e2e-runner: + name: Build E2E test runner + runs-on: ubuntu-latest permissions: contents: read + outputs: + artifact: ${{ steps.artifact.outputs.artifact }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: ${{ !github.event.pull_request.head.repo.fork }} + - name: Build E2E test runner + id: artifact + run: | + # We want a static binary, so we need to set CGO_ENABLED=0 + CGO_ENABLED=0 go build -o ./e2e-runner ./e2e/ + echo "artifact=e2e-runner-${{github.run_number}}" >> "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@v4 + id: upload + with: + retention-days: 1 + name: ${{ steps.artifact.outputs.artifact }} + path: e2e-runner + + run-e2e-tests: + needs: + - build-grafana + - build-e2e-runner + strategy: + fail-fast: false + matrix: + include: + - suite: various-suite + path: e2e/various-suite + - suite: dashboards-suite + path: e2e/dashboards-suite + - suite: smoke-tests-suite + path: e2e/smoke-tests-suite + - suite: panels-suite + path: e2e/panels-suite + - suite: various-suite (old arch) + path: e2e/old-arch/various-suite + flags: --flags="--env dashboardScene=false" + - suite: dashboards-suite (old arch) + path: e2e/old-arch/dashboards-suite + flags: --flags="--env dashboardScene=false" + - suite: smoke-tests-suite (old arch) + path: e2e/old-arch/smoke-tests-suite + flags: --flags="--env dashboardScene=false" + - suite: panels-suite (old arch) + path: e2e/old-arch/panels-suite + flags: --flags="--env dashboardScene=false" name: ${{ matrix.suite }} - strategy: - fail-fast: false - matrix: - suite: - - various-suite - - dashboards-suite - - smoke-tests-suite - - panels-suite - needs: - - build-grafana - uses: ./.github/workflows/run-e2e-suite.yml - with: - package: ${{ needs.build-grafana.outputs.artifact }} - suite: ${{ matrix.suite }} - e2e-matrix-old-arch: + runs-on: ubuntu-latest-8-cores permissions: contents: read - name: ${{ matrix.suite }} (old arch) - strategy: - fail-fast: false - matrix: - suite: - - old-arch/various-suite - - old-arch/dashboards-suite - - old-arch/smoke-tests-suite - - old-arch/panels-suite - needs: - - build-grafana - uses: ./.github/workflows/run-e2e-suite.yml - with: - package: ${{ needs.build-grafana.outputs.artifact }} - suite: ${{ matrix.suite }} + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.build-grafana.outputs.artifact }} + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.build-e2e-runner.outputs.artifact }} + - name: chmod +x + run: chmod +x ./e2e-runner + - name: Run E2E tests + uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e + with: + verb: run + args: go run ./pkg/build/e2e --package=grafana.tar.gz + --suite=${{ matrix.path }} + ${{ matrix.flags }} + - name: Set suite name + id: set-suite-name + if: success() || failure() + env: + SUITE: ${{ matrix.path }} + run: | + echo "suite=$(echo "$SUITE" | sed 's/\//-/g')" >> "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: ${{ steps.set-suite-name.outputs.suite }}-${{ github.run_number }} + path: videos + retention-days: 1 diff --git a/.github/workflows/run-e2e-suite.yml b/.github/workflows/run-e2e-suite.yml deleted file mode 100644 index 0aa6d292abc..00000000000 --- a/.github/workflows/run-e2e-suite.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: e2e suite - -on: - workflow_call: - inputs: - package: - type: string - required: true - suite: - type: string - required: true - -jobs: - main: - runs-on: ubuntu-latest-8-cores - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: actions/download-artifact@v4 - with: - name: ${{ inputs.package }} - - uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e - with: - verb: run - args: go run ./pkg/build/e2e --package=grafana.tar.gz --suite=${{ inputs.suite }} - - name: Set suite name - id: set-suite-name - if: success() || failure() - env: - SUITE: ${{ inputs.suite }} - run: | - echo "suite=$(echo "$SUITE" | sed 's/\//-/g')" >> "$GITHUB_OUTPUT" - - uses: actions/upload-artifact@v4 - if: success() || failure() - with: - name: e2e-${{ steps.set-suite-name.outputs.suite }}-${{github.run_number}} - path: videos - retention-days: 1 diff --git a/.github/zizmor.yml b/.github/zizmor.yml index eac536c3db5..92fe26bf7b4 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -21,6 +21,7 @@ rules: - pr-frontend-unit-tests.yml - pr-test-integration.yml - publish-kinds-release.yml + - pr-e2e-tests.yml dangerous-triggers: ignore: - auto-milestone.yml diff --git a/Dockerfile b/Dockerfile index 966d1753ad3..e7f06079c7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ARG BASE_IMAGE=alpine:3.21 ARG JS_IMAGE=node:22-alpine ARG JS_PLATFORM=linux/amd64 -ARG GO_IMAGE=golang:1.24.3-alpine +ARG GO_IMAGE=golang:1.24.4-alpine # Default to building locally ARG GO_SRC=go-builder diff --git a/Makefile b/Makefile index 9be96ca3248..469706de274 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ include .bingo/Variables.mk include .citools/Variables.mk GO = go -GO_VERSION = 1.24.3 +GO_VERSION = 1.24.4 GO_LINT_FILES ?= $(shell ./scripts/go-workspace/golangci-lint-includes.sh) GO_TEST_FILES ?= $(shell ./scripts/go-workspace/test-includes.sh) SH_FILES ?= $(shell find ./scripts -name *.sh) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index ef3c2710ba5..1045673d51f 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/advisor -go 1.24.3 +go 1.24.4 require ( github.com/grafana/grafana-app-sdk v0.38.2 diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index aac76670d3b..9878a4e39ff 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/alerting/notifications -go 1.24.3 +go 1.24.4 require ( github.com/grafana/grafana-app-sdk v0.38.2 diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index dd46e08e462..846883be4e8 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/dashboard -go 1.24.3 +go 1.24.4 require ( cuelang.org/go v0.11.1 diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 84685631d82..cfe8561a6b8 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/folder -go 1.24.3 +go 1.24.4 require ( github.com/grafana/grafana-app-sdk v0.38.2 diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 4501e755d56..c118cacd1dc 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/iam -go 1.24.3 +go 1.24.4 require ( github.com/grafana/grafana-app-sdk v0.38.2 diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index a82f1b8b0ba..057daa3fef4 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/investigations -go 1.24.3 +go 1.24.4 require ( github.com/grafana/grafana-app-sdk v0.38.2 diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index fd759ee3fdd..59a4f465360 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/playlist -go 1.24.3 +go 1.24.4 require ( github.com/grafana/grafana-app-sdk v0.38.2 diff --git a/devenv/docker/blocks/prometheus_high_card/go.mod b/devenv/docker/blocks/prometheus_high_card/go.mod index 6706105c5cc..32a39000351 100644 --- a/devenv/docker/blocks/prometheus_high_card/go.mod +++ b/devenv/docker/blocks/prometheus_high_card/go.mod @@ -1,6 +1,6 @@ module high-card -go 1.24.3 +go 1.24.4 require github.com/prometheus/client_golang v1.22.0 diff --git a/devenv/docker/blocks/prometheus_utf8/go.mod b/devenv/docker/blocks/prometheus_utf8/go.mod index 8e05b29a4ff..ed5e88f860d 100644 --- a/devenv/docker/blocks/prometheus_utf8/go.mod +++ b/devenv/docker/blocks/prometheus_utf8/go.mod @@ -1,6 +1,6 @@ module utf8-support -go 1.24.3 +go 1.24.4 require ( github.com/prometheus/client_golang v1.22.0 diff --git a/devenv/docker/blocks/stateful_webhook/Dockerfile b/devenv/docker/blocks/stateful_webhook/Dockerfile index 4cac8ca98dc..42a1610a9a2 100644 --- a/devenv/docker/blocks/stateful_webhook/Dockerfile +++ b/devenv/docker/blocks/stateful_webhook/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24.3 +FROM golang:1.24.4 ADD main.go /go/src/webhook/main.go diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index 10f2af5b991..2e1490822fc 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -59,7 +59,7 @@ The following tables list permissions associated with basic and fixed roles. Thi | Grafana Admin | `basic_grafana_admin` | fixed:roles:writer`
`fixed:users:writer`
`fixed:org.users:writer`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:maintainer`
`fixed:licensing:writer`
`fixed:plugins:maintainer`
`fixed:authentication.config:writer`
`fixed:migrationassistant:migrator` | Default [Grafana server administrator](/docs/grafana//administration/roles-and-permissions/#grafana-server-administrators) assignments. | | Admin | `basic_admin` | All roles assigned to Editor and `fixed:reports:writer`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:writer`
`fixed:dashboards.public:writer`
`fixed:folders:writer`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:writer`
`fixed:alerting.provisioning.secrets:reader`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:writer`
`fixed:plugins:writer`
`fixed:library.panels:writer` | Default [Grafana organization administrator](ref:rbac-basic-roles) assignments. | | Editor | `basic_editor` | All roles assigned to Viewer and `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:alerting:writer`
`fixed:library.panels:creator`
`fixed:library.panels:general.writer`
`fixed:alerting.provisioning.status:writer` | Default [Editor](ref:rbac-basic-roles) assignments. | -| Viewer | `basic_viewer` | `fixed:datasources.id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:library.panels:general.reader`
`fixed:folders.general:reader` | Default [Viewer](ref:rbac-basic-roles) assignments. | +| Viewer | `basic_viewer` | `fixed:datasources.id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:library.panels:general.reader`
`fixed:folders.general:reader`
`fixed:datasources.builtin:reader` | Default [Viewer](ref:rbac-basic-roles) assignments. | | No Basic Role | n/a | | Default [No Basic Role](ref:rbac-basic-roles) | ## Fixed role definitions @@ -103,6 +103,7 @@ To learn how to use the roles API to determine the role UUIDs, refer to [Manage | `fixed:datasources:explorer` | `fixed_qDzW9mzx9yM91T5Bi8dHUM2muTw` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | | `fixed:datasources:reader` | `fixed_C2x8IxkiBc1KZVjyYH775T9jNMQ` | `datasources:read`
`datasources:query` | Read and query data sources. | | `fixed:datasources:writer` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`datasources:delete` | Read, query, create, delete, or update a data source. | +| `fixed:datasources.builtin:reader` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | `datasources:read` and `datasources:query` scoped to `datasources:uid:grafana` | An internal role used to grant Viewers access to the builtin example data source in Grafana. | | `fixed:datasources.caching:reader` | `fixed_D2ddpGxJYlw0mbsTS1ek9fj0kj4` | `datasources.caching:read` | Read data source query caching settings. | | `fixed:datasources.caching:writer` | `fixed_JtFjHr7jd7hSqUYcktKvRvIOGRE` | `datasources.caching:read`
`datasources.caching:write` | Enable, disable, or update query caching settings. | | `fixed:datasources.id:reader` | `fixed_entg--fHmDqWY2-69N0ocawK0Os` | `datasources.id:read` | Read the ID of a data source based on its name. | @@ -112,6 +113,7 @@ To learn how to use the roles API to determine the role UUIDs, refer to [Manage | `fixed:folders:creator` | `fixed_gGLRbZGAGB6n9uECqSh_W382RlQ` | `folders:create` | Create folders in the root level. | | `fixed:folders:reader` | `fixed_yeW-5QPeo-i5PZUIUXMlAA97GnQ` | `folders:read`
`dashboards:read` | Read all folders and dashboards. | | `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`folders.permissions:write` | Read, update, and delete all folders and dashboards. Create folders and subfolders. | +| `fixed:folders.general:reader` | `fixed_rSASbkg8DvpG_gTX5s41d7uxRvI` | `folders:read` scoped to `folders:uid:general` | An internal role used to correctly display access to the folder tree for Viewer role. | | `fixed:folders.permissions:reader` | `fixed_E06l4cx0JFm47EeLBE4nmv3pnSo` | `folders.permissions:read` | Read all folder permissions. | | `fixed:folders.permissions:writer` | `fixed_3GAgpQ_hWG8o7-lwNb86_VB37eI` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | | `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | diff --git a/docs/sources/cli.md b/docs/sources/cli.md index 301741d644a..e5e3ac6f2b5 100644 --- a/docs/sources/cli.md +++ b/docs/sources/cli.md @@ -229,7 +229,7 @@ To correct this, use the `--homepath` global option to specify the Grafana defau grafana cli --homepath "/usr/share/grafana" admin reset-admin-password ``` -If you have not lost the admin password, we recommend that you change the user password either in the User Preferences or in the Server Admin > User tab. +If you have not lost the admin password, we recommend that you change the user password either in the User Preferences or in the **Server Admin > User** tab. If you need to set the password in a script, then you can use the [Grafana User API](../developers/http_api/user/#change-password). @@ -241,6 +241,19 @@ If you installed Grafana using Homebrew, you can reset the admin password using /opt/homebrew/opt/grafana/bin/grafana cli --config /opt/homebrew/etc/grafana/grafana.ini --homepath /opt/homebrew/opt/grafana/share/grafana --configOverrides cfg:default.paths.data=/opt/homebrew/var/lib/grafana admin reset-admin-password ``` +#### Reset admin password for Grafana deployed with Grafana Operator and using an external database + +If you deploy Grafana with Grafana Operator and configure Grafana to use an external PostgreSQL or MySQL database, specify both the homepath and configuration file in your command: + +```bash +grafana cli --homepath /usr/share/grafana --config /etc/grafana/grafana.ini admin reset-admin-password +``` + +If you don't do this, the CLI: + +- Won't see the database connection information since it's in a directory outside of the Grafana homepath +- Will configure the default SQLite database in `/var/lib/grafana` and reset that password instead of for your external database + ### Migrate data and encrypt passwords `data-migration` runs a script that migrates or cleans up data in your database. diff --git a/docs/sources/dashboards/build-dashboards/create-dynamic-dashboard/index.md b/docs/sources/dashboards/build-dashboards/create-dynamic-dashboard/index.md index 47c4492204e..6c12e68e724 100644 --- a/docs/sources/dashboards/build-dashboards/create-dynamic-dashboard/index.md +++ b/docs/sources/dashboards/build-dashboards/create-dynamic-dashboard/index.md @@ -77,7 +77,13 @@ refs: # Create and edit dynamic dashboards -{{< docs/experimental product="Dynamic dashboards" featureFlag="dashboardNewLayouts" >}} +{{< admonition type="caution" >}} + +Dynamic dashboards is an [experimental](https://grafana.com/docs/release-life-cycle/) feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog). + +**Do not enable this feature in production environments as it may result in the irreversible loss of data.** + +{{< /admonition >}} Dashboards and panels allow you to show your data in visual form. Each panel needs at least one query to display a visualization. diff --git a/docs/sources/introduction/_index.md b/docs/sources/introduction/_index.md index 9aba0f1cd48..3c71f685351 100644 --- a/docs/sources/introduction/_index.md +++ b/docs/sources/introduction/_index.md @@ -84,7 +84,7 @@ In addition to Grafana, Grafana Labs also provides the following open source pro **Grafana Beyla:** Grafana Beyla is an eBPF-based application auto-instrumentation tool for application observability. eBPF is used to automatically inspect application executables and the OS networking layer as well as capture basic trace spans related to web transactions and Rate-Errors-Duration (RED) metrics for Linux HTTP/S and gRPC services. All data capture occurs without any modifications to application code or configuration. For more information about Grafana Beyla, refer to [Grafana Beyla documentation](/docs/beyla/latest/). -**Grafana Alloy:** Grafana Alloy is a flexible, high performance, vendor-neutral distribution of the [OpenTelemetry][] (OTel) Collector. +**Grafana Alloy:** Grafana Alloy is a flexible, high performance, vendor-neutral distribution of the [OpenTelemetry](https://opentelemetry.io/) (OTel) Collector. It's fully compatible with the most popular open source observability standards such as OpenTelemetry (OTel) and Prometheus. For more information about Grafana Alloy, refer to the [Grafana Alloy documentation](https://grafana.com/docs/alloy/latest/). diff --git a/docs/sources/observability-as-code/foundation-sdk/dashboard-automation.md b/docs/sources/observability-as-code/foundation-sdk/dashboard-automation.md index 30e1df3223e..341fbd2af8c 100644 --- a/docs/sources/observability-as-code/foundation-sdk/dashboard-automation.md +++ b/docs/sources/observability-as-code/foundation-sdk/dashboard-automation.md @@ -164,10 +164,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Set up Go 1.24.3 + - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.24.3 + go-version: 1.24.4 - name: Verify Go version run: go version @@ -208,7 +208,7 @@ This GitHub Action automates the deployment of a Grafana dashboard using the Fou The first few steps: - Check out the repository to access the project code. -- Install Go 1.24.3 using the `actions/setup-go` action. +- Install Go 1.24.4 using the `actions/setup-go` action. - Verify Go is properly installed. ### 2. Download and install `grafanactl` diff --git a/docs/sources/observability-as-code/schema-v2/_index.md b/docs/sources/observability-as-code/schema-v2/_index.md index c86fd130818..d7c2728947b 100644 --- a/docs/sources/observability-as-code/schema-v2/_index.md +++ b/docs/sources/observability-as-code/schema-v2/_index.md @@ -18,7 +18,13 @@ weight: 200 # Dashboard JSON schema v2 -{{< docs/experimental product="Dashboard JSON schema v2" featureFlag="`dashboardNewLayouts`" >}} +{{< admonition type="caution" >}} + +Dashboard JSON schema v2 is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog). + +**Do not enable this feature in production environments as it may result in the irreversible loss of data.** + +{{< /admonition >}} Grafana dashboards are represented as JSON objects that store metadata, panels, variables, and settings. diff --git a/e2e/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e/dashboard-new-layouts/dashboard-group-panels.spec.ts index c74d43fbd8d..f3d74c171b5 100644 --- a/e2e/dashboard-new-layouts/dashboard-group-panels.spec.ts +++ b/e2e/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -5,10 +5,6 @@ describe('Grouping panels', () => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); - after(() => { - e2e.flows.revertAllChanges(); - }); - /* * Rows */ diff --git a/e2e/dashboard-new-layouts/dashboard-outline.spec.ts b/e2e/dashboard-new-layouts/dashboard-outline.spec.ts new file mode 100644 index 00000000000..6068152514c --- /dev/null +++ b/e2e/dashboard-new-layouts/dashboard-outline.spec.ts @@ -0,0 +1,26 @@ +import { e2e } from '../utils'; + +const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; + +describe('Dashboard Outline', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + }); + + it('can use dashboard outline', () => { + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1` }); + + e2e.flows.scenes.toggleEditMode(); + + e2e.components.PanelEditor.Outline.section().click(); + + // Should be able to click Variables item in outline to see add variable button + e2e.components.PanelEditor.Outline.item('Variables').click(); + e2e.components.PanelEditor.ElementEditPane.addVariableButton().should('exist'); + + // Clicking a panel should scroll that panel in view + cy.contains('Dashboard panel 48').should('not.exist'); + e2e.components.PanelEditor.Outline.item('Panel #48').click(); + cy.contains('Dashboard panel 48').should('exist'); + }); +}); diff --git a/e2e/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e/dashboard-new-layouts/dashboards-panel-layouts.spec.ts new file mode 100644 index 00000000000..49f4e1bcc3e --- /dev/null +++ b/e2e/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -0,0 +1,309 @@ +import { e2e } from '../utils'; + +describe('Dashboard', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + }); + + it('can switch to auto grid layout', () => { + e2e.flows.scenes.importV2Dashboard({ title: 'Switch to auto grid' }); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.Panels.Panel.title('New panel').should('have.length', 3); + + e2e.components.OptionsGroup.toggle('grid-layout-category').click(); + + e2e.flows.scenes.selectAutoGridLayout(); + + e2e.components.Panels.Panel.title('New panel').should('have.length', 3); + + const checkInputs = () => { + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('be.visible'); + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns().should('be.visible'); + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('be.visible'); + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen().should('exist'); + }; + + checkInputs(); + + e2e.flows.scenes.saveDashboard(); + cy.reload(); + + e2e.components.Panels.Panel.title('New panel').should('have.length', 3); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + checkInputs(); + }); + + it('can change min column width in auto grid layout', () => { + e2e.flows.scenes.importV2Dashboard({ title: 'Set min column width' }); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.Panels.Panel.title('New panel').should('have.length', 3); + + e2e.components.OptionsGroup.toggle('grid-layout-category').click(); + + e2e.flows.scenes.selectAutoGridLayout(); + + let firstStandardPanelTopOffset = 0; + + // standard min column width will have 1 panel on a second row in edit mode + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + firstStandardPanelTopOffset = el.offset().top; + }); + + e2e.components.Panels.Panel.title('New panel') + .last() + .then((el) => { + expect(el.offset().top).to.be.greaterThan(firstStandardPanelTopOffset); + }); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('be.visible').click(); + cy.get('[id=combobox-option-narrow]').click(); + + const checkOffset = () => { + // narrow min column width will have all panels on the same row + let narrowPanelTopOffset = 0; + + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + narrowPanelTopOffset = el.offset().top; + }); + + e2e.components.Panels.Panel.title('New panel') + .last() + .then((el) => { + expect(el.offset().top).to.eq(narrowPanelTopOffset); + }); + }; + + checkOffset(); + + e2e.flows.scenes.saveDashboard(); + cy.reload(); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('have.value', 'Narrow'); + + checkOffset(); + }); + + it('can change to custom min column width in auto grid layout', () => { + e2e.flows.scenes.importV2Dashboard({ title: 'Set custom min column width' }); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.Panels.Panel.title('New panel').should('have.length', 3); + + e2e.components.OptionsGroup.toggle('grid-layout-category').click(); + + e2e.flows.scenes.selectAutoGridLayout(); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('be.visible').click(); + cy.get('[id=combobox-option-custom]').click(); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth() + .should('be.visible') + .clear() + .type('900') + .blur(); + + cy.wait(100); // cy too fast and executes next command before resizing is done + + // // changing to 900 custom width to have each panel span the whole row to verify offset + e2e.flows.scenes.verifyPanelsStackedVertically(); + + e2e.flows.scenes.saveDashboard(); + cy.reload(); + + e2e.flows.scenes.verifyPanelsStackedVertically(); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth().should('have.value', '900'); + + e2e.flows.scenes.verifyPanelsStackedVertically(); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomMinColumnWidth().should('be.visible').click(); + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('have.value', 'Standard'); + }); + + it('can change max columns in auto grid layout', () => { + e2e.flows.scenes.importV2Dashboard({ title: 'Set max columns' }); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.Panels.Panel.title('New panel').should('have.length', 3); + + e2e.components.OptionsGroup.toggle('grid-layout-category').click(); + + e2e.flows.scenes.selectAutoGridLayout(); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns().should('be.visible').click(); + cy.get('[id=combobox-option-1]').click(); + + // changing to 1 max column to have each panel span the whole row to verify offset + e2e.flows.scenes.verifyPanelsStackedVertically(); + + e2e.flows.scenes.saveDashboard(); + cy.reload(); + + e2e.flows.scenes.verifyPanelsStackedVertically(); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns().should('have.value', '1'); + + e2e.flows.scenes.verifyPanelsStackedVertically(); + }); + + it('can change row height in auto grid layout', () => { + e2e.flows.scenes.importV2Dashboard({ title: 'Set row height' }); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.Panels.Panel.title('New panel').should('have.length', 3); + + e2e.components.OptionsGroup.toggle('grid-layout-category').click(); + + e2e.flows.scenes.selectAutoGridLayout(); + + let regularRowHeight = 0; + + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + regularRowHeight = el.height(); + }); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('be.visible').click(); + cy.get('[id=combobox-option-short]').click(); + + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + expect(el.height()).to.be.lessThan(regularRowHeight); + }); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('be.visible').click(); + cy.get('[id=combobox-option-tall]').click(); + + const checkHeight = () => { + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + expect(el.height()).to.be.greaterThan(regularRowHeight); + }); + }; + + checkHeight(); + + e2e.flows.scenes.saveDashboard(); + cy.reload(); + + checkHeight(); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('have.value', 'Tall'); + + checkHeight(); + }); + + it('can change to custom row height in auto grid layout', () => { + e2e.flows.scenes.importV2Dashboard({ title: 'Set custom row height' }); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.Panels.Panel.title('New panel').should('have.length', 3); + + e2e.components.OptionsGroup.toggle('grid-layout-category').click(); + + e2e.flows.scenes.selectAutoGridLayout(); + + let regularRowHeight = 0; + + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + regularRowHeight = el.height(); + }); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('be.visible').click(); + cy.get('[id=combobox-option-custom]').click(); + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight().clear().type('800').blur(); + cy.wait(100); // cy too fast and executes next command before resizing is done + + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + const elHeight = el.height(); + expect(elHeight).be.closeTo(800, 5); // some flakyness and get 798 sometimes + expect(elHeight).to.be.greaterThan(regularRowHeight); + }); + + e2e.flows.scenes.saveDashboard(); + cy.reload(); + + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + expect(el.height()).be.closeTo(800, 5); // some flakyness and get 798 sometimes + }); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight().should('have.value', '800'); + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomRowHeight().should('be.visible').click(); + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('have.value', 'Standard'); + }); + + it('can change fill screen in auto grid layout', () => { + e2e.flows.scenes.importV2Dashboard({ title: 'Set fill screen' }); + + e2e.components.NavToolbar.editDashboard.editButton().click(); + + e2e.components.Panels.Panel.title('New panel').should('have.length', 3); + + e2e.components.OptionsGroup.toggle('grid-layout-category').click(); + + e2e.flows.scenes.selectAutoGridLayout(); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('be.visible').click(); + cy.get('[id=combobox-option-narrow]').click(); + + let initialHeight = 0; + + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + initialHeight = el.height(); + }); + + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen().click({ force: true }); + + const checkHeight = () => { + e2e.components.Panels.Panel.title('New panel') + .first() + .then((el) => { + expect(el.height()).to.be.greaterThan(initialHeight); + }); + }; + + checkHeight(); + e2e.flows.scenes.saveDashboard(); + cy.reload(); + + checkHeight(); + e2e.components.NavToolbar.editDashboard.editButton().click(); + e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen().should('be.checked'); + + checkHeight(); + }); +}); diff --git a/e2e/main.go b/e2e/main.go new file mode 100644 index 00000000000..130d8d85426 --- /dev/null +++ b/e2e/main.go @@ -0,0 +1,351 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "path" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + "github.com/urfave/cli/v3" +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + if err := Run().Run(ctx, os.Args); err != nil { + cancel() + fmt.Println(err) + os.Exit(1) + } +} + +func Run() *cli.Command { + var suitePath string + + return &cli.Command{ + Name: "e2e", + Usage: "Run the test suite", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "command", + Usage: "Cypress command to run. 'open' can be useful for development (enum: run, open)", + Value: "run", + Validator: func(s string) error { + if s != "run" && s != "open" { + return fmt.Errorf("invalid command: %s, must be 'run' or 'open'", s) + } + return nil + }, + }, + &cli.StringFlag{ + Name: "browser", + Usage: "Browser to run tests with (e.g.: chrome, electron)", + Value: "chrome", + }, + &cli.StringFlag{ + Name: "grafana-base-url", + Usage: "Base URL for Grafana", + Value: "http://localhost:3001", + }, + &cli.BoolFlag{ + Name: "cypress-video", + Usage: "Enable Cypress video recordings", + Value: false, + }, + &cli.BoolFlag{ + Name: "smtp-plugin", + Usage: "Enable SMTP plugin", + Value: false, + }, + &cli.BoolFlag{ + Name: "benchmark-plugin", + Usage: "Enable Benchmark plugin", + Value: false, + }, + &cli.BoolFlag{ + Name: "slowmo", + Usage: "Slow down the test run", + Value: false, + }, + &cli.StringSliceFlag{ + Name: "env", + Usage: "Additional Cypress environment variables to set (format: KEY=VALUE)", + Validator: func(s []string) error { + pattern := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*`) + for _, v := range s { + if !pattern.MatchString(v) { + return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v) + } + } + return nil + }, + }, + &cli.StringSliceFlag{ + Name: "parameters", + Usage: "Additional parameters to pass to the Cypress command (e.g. --headed)", + }, + &cli.DurationFlag{ + Name: "timeout", + Usage: "Timeout for the Cypress command (precision: milliseconds)", + Value: time.Second * 30, + Validator: func(d time.Duration) error { + if d < 0 { + return fmt.Errorf("timeout must be a positive duration") + } + if d.Round(time.Millisecond) != d { + return fmt.Errorf("timeout must be a whole number of milliseconds") + } + return nil + }, + }, + &cli.StringFlag{ + Name: "timezone", + Usage: "Timezone to set for the Cypress run (e.g. 'America/New_York')", + Value: "Pacific/Honolulu", + }, + + &cli.BoolFlag{ + Name: "start-grafana", + Usage: "Start and wait for Grafana before running the tests", + Value: true, + Category: "Grafana Server", + }, + &cli.StringFlag{ + Name: "license-path", + Usage: "Path to the Grafana Enterprise license file (optional; requires --start-grafana)", + Value: "", + TakesFile: true, + Category: "Grafana Server", + }, + &cli.BoolFlag{ + Name: "image-renderer", + Usage: "Install the image renderer plugin (requires --start-grafana)", + Category: "Grafana Server", + }, + + &cli.StringFlag{ + Name: "suite", + Usage: "Path to the suite to run (e.g. './e2e/dashboards-suite')", + TakesFile: true, + Required: true, + Destination: &suitePath, + }, + }, + Action: runAction, + } +} + +func runAction(ctx context.Context, c *cli.Command) error { + suitePath := c.String("suite") + suitePath, err := normalisePath(suitePath) + if err != nil { + return fmt.Errorf("failed to normalise suite path: %w", err) + } + + repoRoot, err := gitRepoRoot(ctx, suitePath) + if err != nil { + return fmt.Errorf("failed to get git repo root: %w", err) + } + + screenshotsFolder := path.Join(suitePath, "screenshots") + videosFolder := path.Join(suitePath, "videos") + fileServerFolder := path.Join(repoRoot, "e2e", "cypress") + fixturesFolder := path.Join(fileServerFolder, "fixtures") + downloadsFolder := path.Join(fileServerFolder, "downloads") + benchmarkPluginResultsFolder := path.Join(suitePath, "benchmark-results") + reporter := path.Join(repoRoot, "e2e", "log-reporter.js") + + env := map[string]string{ + "BENCHMARK_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("benchmark-plugin")), + "SMTP_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("smtp-plugin")), + "BENCHMARK_PLUGIN_RESULTS_FOLDER": benchmarkPluginResultsFolder, + "SLOWMO": "0", + "BASE_URL": c.String("grafana-base-url"), + } + for _, v := range c.StringSlice("env") { + parts := strings.SplitN(v, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v) + } + env[parts[0]] = parts[1] + } + + cypressConfig := map[string]string{ + "screenshotsFolder": screenshotsFolder, + "fixturesFolder": fixturesFolder, + "videosFolder": videosFolder, + "downloadsFolder": downloadsFolder, + "fileServerFolder": fileServerFolder, + "reporter": reporter, + + "specPattern": path.Join(suitePath, "*.spec.ts"), + "defaultCommandTimeout": fmt.Sprintf("%d", c.Duration("timeout").Milliseconds()), + "viewportWidth": "1920", + "viewportHeight": "1080", + "trashAssetsBeforeRuns": "false", + "baseUrl": c.String("grafana-base-url"), + "video": fmt.Sprintf("%t", c.Bool("cypress-video")), + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if c.Bool("start-grafana") { + startServerPath := path.Join(repoRoot, "scripts", "grafana-server", "start-server") + waitForGrafanaPath := path.Join(repoRoot, "scripts", "grafana-server", "wait-for-grafana") + go func() { + var args []string + if c.String("license-path") != "" { + args = append(args, c.String("license-path")) + } + //nolint:gosec + cmd := exec.CommandContext(ctx, startServerPath, args...) + cmd.Dir = repoRoot + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) + if c.Bool("image-renderer") { + cmd.Env = append(cmd.Env, "INSTALL_IMAGE_RENDERER=true") + } + cmd.Stdout = prefixGrafana(os.Stdout) + cmd.Stderr = prefixGrafana(os.Stderr) + cmd.Stdin = nil + + if err := cmd.Run(); err != nil { + fmt.Println("Error running Grafana:", err) + } + }() + + //nolint:gosec + cmd := exec.CommandContext(ctx, waitForGrafanaPath) + cmd.Dir = repoRoot + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) + cmd.Stdout = prefixGrafana(os.Stdout) + cmd.Stderr = prefixGrafana(os.Stderr) + cmd.Stdin = nil + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to wait for Grafana: %w", err) + } + } + + args := []string{"run", "cypress", c.String("command"), + "--env", joinCypressCfg(env), + "--config", joinCypressCfg(cypressConfig), + "--browser", c.String("browser")} + args = append(args, c.StringSlice("parameters")...) + //nolint:gosec + cmd := exec.CommandContext(ctx, "yarn", args...) + cmd.Dir = repoRoot + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) + cmd.Stdout = prefixCypress(os.Stdout) + cmd.Stderr = prefixCypress(os.Stderr) + cmd.Stdin = os.Stdin + return cmd.Run() +} + +func gitRepoRoot(ctx context.Context, dir string) (string, error) { + cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel") + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get git repo root: %w", err) + } + p := strings.TrimSpace(string(out)) + p, err = normalisePath(p) + if err != nil { + return "", fmt.Errorf("failed to normalise git repo root path: %w", err) + } + return p, nil +} + +func normalisePath(p string) (string, error) { + absPath, err := filepath.Abs(p) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + return path.Clean(filepath.ToSlash(absPath)), nil +} + +func joinCypressCfg(cfg map[string]string) string { + config := make([]string, 0, len(cfg)) + for k, v := range cfg { + config = append(config, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(config, ",") +} + +const ( + resetColor = "\033[0m" + yellowColor = "\033[0;33m" + cyanColor = "\033[0;36m" +) + +func prefixCypress(w io.Writer) io.Writer { + if _, ok := os.LookupEnv("CI"); ok { + return w + } + + return newWrappingOutput(cyanColor+"Cypress: ", resetColor, w) +} + +func prefixGrafana(w io.Writer) io.Writer { + if _, ok := os.LookupEnv("CI"); ok { + return w + } + + return newWrappingOutput(yellowColor+"Grafana: ", resetColor, w) +} + +var _ io.Writer = (*wrappingOutput)(nil) + +type wrappingOutput struct { + prefix string + suffix string + mu *sync.Mutex + inner io.Writer + writtenPrefix bool +} + +func newWrappingOutput(prefix, suffix string, inner io.Writer) *wrappingOutput { + return &wrappingOutput{ + prefix: prefix, + suffix: suffix, + mu: &sync.Mutex{}, + inner: inner, + } +} + +func (p *wrappingOutput) Write(b []byte) (int, error) { + p.mu.Lock() + defer p.mu.Unlock() + + for line := range bytes.Lines(b) { + if !p.writtenPrefix { + if _, err := p.inner.Write([]byte(p.prefix)); err != nil { + return 0, err + } + p.writtenPrefix = true + } + if _, err := p.inner.Write(line); err != nil { + return 0, err + } + if bytes.HasSuffix(line, []byte("\n")) { + p.writtenPrefix = false + if _, err := p.inner.Write([]byte(p.suffix)); err != nil { + return 0, err + } + } + } + return len(b), nil +} diff --git a/e2e/run-suite b/e2e/run-suite index f4be6852580..386fbae4f0e 100755 --- a/e2e/run-suite +++ b/e2e/run-suite @@ -26,7 +26,7 @@ declare -A env=( ) testFilesForSingleSuite="*.spec.ts" -rootForEnterpriseSuite="./e2e/extensions-suite" +rootForEnterpriseSuite="./e2e/extensions" rootForOldArch="./e2e/old-arch" rootForKubernetesDashboards="./e2e/dashboards-suite" rootForSearchDashboards="./e2e/dashboards-search-suite" @@ -45,6 +45,7 @@ declare -A cypressConfig=( [trashAssetsBeforeRuns]=false [reporter]=./e2e/log-reporter.js [baseUrl]=${BASE_URL:-"http://$HOST:$PORT"} + [video]=${CYPRESS_VIDEO:-false} ) case "$1" in @@ -70,8 +71,6 @@ case "$1" in "enterprise") echo "Enterprise" env[SMTP_PLUGIN_ENABLED]=true - CLEANUP="rm -rf ./e2e/extensions-suite" - SETUP="cp -Lr ./e2e/extensions ./e2e/extensions-suite" enterpriseSuite=$(basename "${args[1]}") case "$2" in "debug") @@ -87,7 +86,6 @@ case "$1" in ;; esac cypressConfig[specPattern]=$rootForEnterpriseSuite/$enterpriseSuite/*-suite/*.spec.ts - $CLEANUP && $SETUP ;; "") ;; diff --git a/e2e/utils/flows/scenes/editPaneActions.ts b/e2e/utils/flows/scenes/editPaneActions.ts index bba3f40578e..04bb7e8fa05 100644 --- a/e2e/utils/flows/scenes/editPaneActions.ts +++ b/e2e/utils/flows/scenes/editPaneActions.ts @@ -12,6 +12,14 @@ export const selectRowsLayout = () => { clickGroupLayoutButton('Rows'); }; +export const selectCustomGridLayout = () => { + clickGroupLayoutButton('Custom'); +}; + +export const selectAutoGridLayout = () => { + clickGroupLayoutButton('Auto grid'); +}; + const editPaneCopyOrDuplicate = (buttonLabel: string) => { e2e.components.EditPaneHeader.copyDropdown().click(); cy.get('[role="menu"]').within(() => { diff --git a/e2e/utils/flows/scenes/index.ts b/e2e/utils/flows/scenes/index.ts index 9a5562237dc..3a8d6fa55a7 100644 --- a/e2e/utils/flows/scenes/index.ts +++ b/e2e/utils/flows/scenes/index.ts @@ -8,3 +8,4 @@ export * from './saveDashboard'; export * from './importDashboard'; export * from './editPaneActions'; export * from './selectPanel'; +export * from './panelPosition'; diff --git a/e2e/utils/flows/scenes/panelPosition.ts b/e2e/utils/flows/scenes/panelPosition.ts new file mode 100644 index 00000000000..63579422346 --- /dev/null +++ b/e2e/utils/flows/scenes/panelPosition.ts @@ -0,0 +1,14 @@ +import { e2e } from '../..'; + +export const verifyPanelsStackedVertically = () => { + let initialOffset = 0; + e2e.components.Panels.Panel.title('New panel').each((el) => { + if (!initialOffset) { + initialOffset = el.offset().top; + } else { + const elOffset = el.offset().top; + expect(elOffset).to.be.greaterThan(initialOffset); + initialOffset = elOffset; + } + }); +}; diff --git a/go.mod b/go.mod index a505e783ece..6f85332903a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana -go 1.24.3 +go 1.24.4 require ( buf.build/gen/go/parca-dev/parca/connectrpc/go v1.17.0-20240902100956-02fd72488966.1 // @grafana/observability-traces-and-profiling @@ -156,6 +156,7 @@ require ( github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 // @grafana/grafana-backend-group github.com/urfave/cli v1.22.16 // indirect; @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.27.6 // @grafana/grafana-backend-group + github.com/urfave/cli/v3 v3.3.3 // @grafana/grafana-backend-group github.com/wk8/go-ordered-map v1.0.0 // @grafana/grafana-backend-group github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // @grafana/grafana-operator-experience-squad diff --git a/go.sum b/go.sum index 8933e3b7f92..e8c24ae49bc 100644 --- a/go.sum +++ b/go.sum @@ -2442,6 +2442,8 @@ github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= +github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= diff --git a/go.work b/go.work index f9de6e23daa..044c12b9a93 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.24.3 +go 1.24.4 // The `skip:golangci-lint` comment tag is used to exclude the package from the `golangci-lint` GitHub Action. // The module at the root of the repo (`.`) is excluded because ./pkg/... is included manually in the `golangci-lint` configuration. diff --git a/hack/go.mod b/hack/go.mod index 08fa4910871..eb429b002ec 100644 --- a/hack/go.mod +++ b/hack/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/hack -go 1.24.3 +go 1.24.4 require k8s.io/code-generator v0.33.1 diff --git a/package.json b/package.json index 78fc8fe2b01..9897b90f1fe 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.2", "@babel/core": "7.26.10", "@babel/preset-env": "7.26.9", "@babel/runtime": "7.27.0", @@ -229,11 +230,11 @@ "postcss-reporter": "7.1.0", "postcss-scss": "4.0.9", "prettier": "3.4.2", + "publint": "^0.3.12", "react-refresh": "0.14.0", "react-select-event": "5.5.1", "redux-mock-store": "1.5.5", "rimraf": "6.0.1", - "rudder-sdk-js": "2.48.44", "sass": "1.83.4", "sass-loader": "16.0.4", "smtp-tester": "^2.1.0", diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 343dc58efcb..f0e51f64da6 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -606,6 +606,32 @@ export const versionedComponents = { variableLabelInput: { '12.0.0': 'data-testid variable label input', }, + AutoGridLayout: { + minColumnWidth: { + '12.1.0': 'data-testid min column width selector', + }, + customMinColumnWidth: { + '12.1.0': 'data-testid custom min column width input', + }, + clearCustomMinColumnWidth: { + '12.1.0': 'data-testid clear custom min column width input', + }, + maxColumns: { + '12.1.0': 'data-testid max columns selector', + }, + rowHeight: { + '12.1.0': 'data-testid row height selector', + }, + customRowHeight: { + '12.1.0': 'data-testid custom row height input', + }, + clearCustomRowHeight: { + '12.1.0': 'data-testid clear custom row height input', + }, + fillScreen: { + '12.1.0': 'data-testid fill screen switch', + }, + }, }, }, PanelInspector: { diff --git a/packages/grafana-i18n/src/i18n.tsx b/packages/grafana-i18n/src/i18n.tsx index 8aeb19681ec..343061a6c42 100644 --- a/packages/grafana-i18n/src/i18n.tsx +++ b/packages/grafana-i18n/src/i18n.tsx @@ -30,17 +30,20 @@ export async function loadPluginResources(id: string, language: string, loaders? } // exported for testing -export async function initDefaultI18nInstance() { +export function initDefaultI18nInstance() { // If the resources are not an object, we need to initialize the plugin translations if (getI18nInstance().options?.resources && typeof getI18nInstance().options.resources === 'object') { return; } - await getI18nInstance().use(initReactI18next).init({ + const initPromise = getI18nInstance().use(initReactI18next).init({ resources: {}, returnEmptyString: false, lng: DEFAULT_LANGUAGE, // this should be the locale of the phrases in our source JSX }); + tFunc = getI18nInstance().t; + transComponent = (props: TransProps) => ; + return initPromise; } // exported for testing @@ -171,6 +174,7 @@ export function addResourceBundle(language: string, namespace: string, resources } export const t: TFunction = (id: string, defaultMessage: string, values?: Record) => { + initDefaultI18nInstance(); if (!tFunc) { if (process.env.NODE_ENV !== 'test') { console.warn( @@ -189,10 +193,12 @@ export const t: TFunction = (id: string, defaultMessage: string, values?: Record }; export function useTranslate() { + initDefaultI18nInstance(); return { t }; } export function Trans(props: TransProps) { + initDefaultI18nInstance(); const Component = transComponent ?? I18NextTrans; return ; } diff --git a/packages/grafana-i18n/tsconfig.json b/packages/grafana-i18n/tsconfig.json index 998a2dffff5..73494649046 100644 --- a/packages/grafana-i18n/tsconfig.json +++ b/packages/grafana-i18n/tsconfig.json @@ -8,5 +8,5 @@ }, "exclude": ["dist/**/*"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*", "../../public/app/types/*.d.ts"] + "include": ["src/**/*.ts*"] } diff --git a/packages/grafana-sql/src/components/TableSelector.tsx b/packages/grafana-sql/src/components/TableSelector.tsx index 9801f98d12c..fb1bac8e71f 100644 --- a/packages/grafana-sql/src/components/TableSelector.tsx +++ b/packages/grafana-sql/src/components/TableSelector.tsx @@ -38,6 +38,7 @@ export const TableSelector = ({ db, dataset, table, className, onChange, inputId isLoading={state.loading} menuShouldPortal={true} placeholder={state.loading ? 'Loading tables' : 'Select table'} + allowCustomValue={true} /> ); }; diff --git a/packages/grafana-sql/src/components/visual-query-builder/SQLGroupByRow.tsx b/packages/grafana-sql/src/components/visual-query-builder/SQLGroupByRow.tsx index 722a0bad3f9..ef16d8b50d0 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SQLGroupByRow.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SQLGroupByRow.tsx @@ -2,6 +2,7 @@ import { SelectableValue } from '@grafana/data'; import { QueryWithDefaults } from '../../defaults'; import { DB, SQLQuery } from '../../types'; +import { getColumnsWithIndices } from '../../utils/getColumnsWithIndices'; import { useSqlChange } from '../../utils/useSqlChange'; import { GroupByRow } from './GroupByRow'; @@ -15,6 +16,6 @@ interface SQLGroupByRowProps { export function SQLGroupByRow({ fields, query, onQueryChange, db }: SQLGroupByRowProps) { const { onSqlChange } = useSqlChange({ query, onQueryChange, db }); - - return ; + let columnsWithIndices: SelectableValue[] = getColumnsWithIndices(query, fields); + return ; } diff --git a/packages/grafana-sql/src/components/visual-query-builder/SQLOrderByRow.tsx b/packages/grafana-sql/src/components/visual-query-builder/SQLOrderByRow.tsx index 8ef0d6f5356..b20ec915c54 100644 --- a/packages/grafana-sql/src/components/visual-query-builder/SQLOrderByRow.tsx +++ b/packages/grafana-sql/src/components/visual-query-builder/SQLOrderByRow.tsx @@ -2,6 +2,7 @@ import { SelectableValue } from '@grafana/data'; import { QueryWithDefaults } from '../../defaults'; import { DB, SQLQuery } from '../../types'; +import { getColumnsWithIndices } from '../../utils/getColumnsWithIndices'; import { useSqlChange } from '../../utils/useSqlChange'; import { OrderByRow } from './OrderByRow'; @@ -15,26 +16,6 @@ type SQLOrderByRowProps = { export function SQLOrderByRow({ fields, query, onQueryChange, db }: SQLOrderByRowProps) { const { onSqlChange } = useSqlChange({ query, onQueryChange, db }); - let columnsWithIndices: SelectableValue[] = []; - - if (fields) { - const options = query.sql?.columns?.map((c, i) => { - const value = c.name ? `${c.name}(${c.parameters?.map((p) => p.name)})` : c.parameters?.map((p) => p.name); - return { - value, - label: `${i + 1} - ${value}`, - }; - }); - columnsWithIndices = [ - { - value: '', - label: 'Selected columns', - options, - expanded: true, - }, - ...fields, - ]; - } - + let columnsWithIndices: SelectableValue[] = getColumnsWithIndices(query, fields); return ; } diff --git a/packages/grafana-sql/src/utils/getColumnsWithIndices.ts b/packages/grafana-sql/src/utils/getColumnsWithIndices.ts new file mode 100644 index 00000000000..6c7dccbff9e --- /dev/null +++ b/packages/grafana-sql/src/utils/getColumnsWithIndices.ts @@ -0,0 +1,29 @@ +import { SelectableValue } from '@grafana/data'; + +import { SQLQuery } from '../types'; + +export function getColumnsWithIndices(query: SQLQuery, fields: SelectableValue[]): SelectableValue[] { + if (!fields || !query.sql?.columns) { + return fields; + } + + const options = query.sql.columns.map((c, i) => { + const value = c.name + ? `${c.name}(${c.parameters?.map((p) => p.name).join(', ')})` + : c.parameters?.map((p) => p.name).join(', '); + return { + value, + label: `${i + 1} - ${value}`, + }; + }); + + return [ + { + value: '', + label: 'Selected columns', + options, + expanded: true, + }, + ...fields, + ]; +} diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx index e8dcd066de9..5279342e6d6 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarBody.tsx @@ -105,6 +105,7 @@ export const getBodyStyles = (theme: GrafanaTheme2) => { '&:disabled': { color: theme.colors.action.disabledText, + cursor: 'not-allowed', }, }), body: css({ @@ -153,13 +154,18 @@ export const getBodyStyles = (theme: GrafanaTheme2) => { outline: 0, }, - [`${hasActiveSelector}, .react-calendar__tile--active, .react-calendar__tile--active:hover`]: { + [`${hasActiveSelector}, .react-calendar__tile--active`]: { color: theme.colors.primary.contrastText, fontWeight: theme.typography.fontWeightMedium, background: theme.colors.primary.main, border: '0px', }, + '.react-calendar__tile:hover:not(.react-calendar__tile--active):not(.react-calendar__tile--rangeEnd):not(.react-calendar__tile--rangeStart)': + { + backgroundColor: theme.colors.action.hover, + }, + '.react-calendar__tile--rangeEnd, .react-calendar__tile--rangeStart': { padding: 0, border: '0px', diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index b496f90c925..8fca2a719a8 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/aggregator -go 1.24.3 +go 1.24.4 require ( github.com/emicklei/go-restful/v3 v3.11.0 diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index e770bec0c9f..8a9af8a7b6f 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/apimachinery -go 1.24.3 +go 1.24.4 require ( github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb // @grafana/identity-access-team diff --git a/pkg/apis/secret/go.mod b/pkg/apis/secret/go.mod index 8a45e743ff1..d9d4f23ce31 100644 --- a/pkg/apis/secret/go.mod +++ b/pkg/apis/secret/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/apis/secret -go 1.24.3 +go 1.24.4 require ( github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 56de9774c53..9a3a8e3c1ca 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/apiserver -go 1.24.3 +go 1.24.4 require ( github.com/google/go-cmp v0.7.0 diff --git a/pkg/build/daggerbuild/arguments/grafana.go b/pkg/build/daggerbuild/arguments/grafana.go index 00bad617e32..90c03751e4b 100644 --- a/pkg/build/daggerbuild/arguments/grafana.go +++ b/pkg/build/daggerbuild/arguments/grafana.go @@ -217,12 +217,12 @@ func enterpriseDirectory(ctx context.Context, opts *pipeline.ArgumentOpts) (any, return nil, fmt.Errorf("error initializing grafana directory: %w", err) } - src, err := cloneOrMount(ctx, opts.Client, o.EnterpriseDir, o.EnterpriseRepo, o.EnterpriseRef, o.GitHubToken) + clone, err := cloneOrMount(ctx, opts.Client, o.EnterpriseDir, o.EnterpriseRepo, o.EnterpriseRef, o.GitHubToken) if err != nil { - return nil, err + return nil, fmt.Errorf("error cloning or mounting Grafana Enterprise directory: %w", err) } - return InitializeEnterprise(opts.Client, grafanaDir.(*dagger.Directory), src), nil + return InitializeEnterprise(opts.Client, grafanaDir.(*dagger.Directory), clone), nil } var GrafanaDirectoryFlags = []cli.Flag{ diff --git a/pkg/build/e2e/main.go b/pkg/build/e2e/main.go index b97369d2331..9bc35151de8 100644 --- a/pkg/build/e2e/main.go +++ b/pkg/build/e2e/main.go @@ -2,71 +2,173 @@ package main import ( "context" - "flag" "fmt" "log" "os" + "os/signal" + "path" "dagger.io/dagger" + "github.com/urfave/cli/v3" ) func main() { - var ( - ctx = context.Background() - grafanaPath = flag.String("grafana-dir", ".", "Path to cloned grafana repo") - targzPath = flag.String("package", "grafana.tar.gz", "Path to grafana tar.gz package") - suite = flag.String("suite", "", "e2e suite name (used in arg to run-suite script)") - ) - flag.Parse() + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + if err := NewApp().Run(ctx, os.Args); err != nil { + cancel() + fmt.Println(err) + os.Exit(1) + } +} + +func NewApp() *cli.Command { + return &cli.Command{ + Name: "e2e", + Usage: "Run the E2E tests for Grafana", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "suite", + Usage: "E2E test suite path (e.g. e2e/various-suite)", + Validator: mustBeDir("suite"), + TakesFile: true, + Required: true, + }, + + &cli.StringFlag{ + Name: "grafana-dir", + Usage: "Path to the grafana/grafana clone directory", + Value: ".", + Validator: mustBeDir("grafana-dir"), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "package", + Usage: "Path to the grafana tar.gz package", + Value: "grafana.tar.gz", + Validator: mustBeFile("package", false), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "license", + Usage: "Path to the Grafana Enterprise license file (optional)", + Validator: mustBeFile("license", true), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "flags", + Usage: "Flags to pass through to the e2e runner", + }, + &cli.BoolFlag{ + Name: "image-renderer", + Usage: "Install the image renderer plugin", + Value: false, + }, + }, + Action: run, + } +} + +func run(ctx context.Context, cmd *cli.Command) error { + grafanaDir := cmd.String("grafana-dir") + suite := cmd.String("suite") + targzPath := cmd.String("package") + licensePath := cmd.String("license") + imageRenderer := cmd.Bool("image-renderer") + runnerFlags := cmd.String("flags") d, err := dagger.Connect(ctx) if err != nil { - panic(err) + return fmt.Errorf("failed to connect to Dagger: %w", err) } yarnCache := d.CacheVolume("yarn") - log.Println("grafana dir:", *grafanaPath) - log.Println("targz:", *targzPath) + log.Println("grafana dir:", grafanaDir) + log.Println("targz:", targzPath) + log.Println("license path:", licensePath) grafana := d.Host().Directory(".", dagger.HostDirectoryOpts{ - Exclude: []string{".git", "node_modules", "*.tar.gz"}, + Exclude: []string{"node_modules", "*.tar.gz"}, }) + targz := d.Host().File(targzPath) - targz := d.Host().File("grafana.tar.gz") - - svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{ - GrafanaDir: grafana, - GrafanaTarGz: targz, - YarnCache: yarnCache, - }) - if err != nil { - panic(err) + var license *dagger.File + if licensePath != "" { + license = d.Host().File(licensePath) } - videosDir := fmt.Sprintf("/src/e2e/%s/videos", *suite) + svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{ + GrafanaDir: grafana, + GrafanaTarGz: targz, + YarnCache: yarnCache, + License: license, + InstallImageRenderer: imageRenderer, + }) + if err != nil { + return fmt.Errorf("failed to create Grafana service: %w", err) + } + + videosDir := path.Join("/src", suite, "videos") // *spec.ts.mp4 - c := RunSuite(d, svc, grafana, yarnCache, *suite) + c := RunSuite(d, svc, grafana, yarnCache, suite, runnerFlags) c, err = c.Sync(ctx) if err != nil { - log.Fatalf("error running dagger: %s", err) + return fmt.Errorf("failed to run e2e test suite: %w", err) } code, err := c.ExitCode(ctx) if err != nil { - log.Fatalf("error getting exit code: %s", err) + return fmt.Errorf("failed to get exit code of e2e test suite: %w", err) } log.Println("exit code:", code) // No sync error; export the videos dir if _, err := c.Directory(videosDir).Export(ctx, "videos"); err != nil { - log.Fatalf("error getting videos: %s", err) + return fmt.Errorf("failed to export videos directory: %w", err) } if code != 0 { - log.Printf("tests failed: exit code %d", code) + return fmt.Errorf("e2e tests failed with exit code %d", code) } - os.Exit(code) + log.Println("e2e tests completed successfully") + return nil +} + +func mustBeFile(arg string, emptyOk bool) func(string) error { + return func(s string) error { + if s == "" { + if emptyOk { + return nil + } + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if stat.IsDir() { + return cli.Exit(arg+" must be a file, not a directory: "+s, 1) + } + return nil + } +} + +func mustBeDir(arg string) func(string) error { + return func(s string) error { + if s == "" { + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if !stat.IsDir() { + return cli.Exit(arg+" must be a directory: "+s, 1) + } + return nil + } } diff --git a/pkg/build/e2e/run.go b/pkg/build/e2e/run.go index d1577576e97..db42d397c38 100644 --- a/pkg/build/e2e/run.go +++ b/pkg/build/e2e/run.go @@ -6,17 +6,14 @@ import ( "dagger.io/dagger" ) -func RunSuite(d *dagger.Client, svc *dagger.Service, src *dagger.Directory, cache *dagger.CacheVolume, suite string) *dagger.Container { +func RunSuite(d *dagger.Client, svc *dagger.Service, src *dagger.Directory, cache *dagger.CacheVolume, suite, runnerFlags string) *dagger.Container { + command := fmt.Sprintf( + "./e2e-runner --start-grafana=false --cypress-video"+ + " --grafana-base-url http://grafana:3001 --suite %s %s", suite, runnerFlags) + return WithYarnCache(WithGrafanaFrontend(d.Container().From("cypress/included:13.1.0"), src), cache). WithWorkdir("/src"). - WithEnvVariable("HOST", "grafana"). - WithEnvVariable("PORT", "3001"). WithServiceBinding("grafana", svc). WithExec([]string{"yarn", "install", "--immutable"}). - WithExec([]string{ - "/bin/bash", "-c", - fmt.Sprintf("./e2e/run-suite %s true", suite), - }, dagger.ContainerWithExecOpts{ - Expect: dagger.ReturnTypeAny, - }) + WithExec([]string{"/bin/bash", "-c", command}, dagger.ContainerWithExecOpts{Expect: dagger.ReturnTypeAny}) } diff --git a/pkg/build/e2e/service.go b/pkg/build/e2e/service.go index 04ddbf10f7b..95de093ec18 100644 --- a/pkg/build/e2e/service.go +++ b/pkg/build/e2e/service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os" "strings" "dagger.io/dagger" @@ -22,9 +23,11 @@ func NodeImage(version string) string { } type GrafanaServiceOpts struct { - GrafanaDir *dagger.Directory - GrafanaTarGz *dagger.File - YarnCache *dagger.CacheVolume + GrafanaDir *dagger.Directory + GrafanaTarGz *dagger.File + YarnCache *dagger.CacheVolume + License *dagger.File + InstallImageRenderer bool } func Frontend(src *dagger.Directory) *dagger.Directory { @@ -71,8 +74,9 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp WithExec([]string{"yarn", "install", "--immutable"}). WithExec([]string{"yarn", "e2e:plugin:build"}) - svc := d.Container().From("alpine"). - WithExec([]string{"apk", "add", "bash"}). + // _/ubuntu:latest sticks to latest LTS. + // We need ubuntu to support the image renderer plugin, which assumes glibc. + container := d.Container().From("ubuntu:latest"). WithMountedFile("/src/grafana.tar.gz", opts.GrafanaTarGz). WithExec([]string{"mkdir", "-p", "/src/grafana"}). WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). @@ -84,8 +88,30 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp WithEnvVariable("GF_APP_MODE", "development"). WithEnvVariable("GF_SERVER_HTTP_PORT", "3001"). WithEnvVariable("GF_SERVER_ROUTER_LOGGING", "1"). - WithExposedPort(3001). - AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "-x", "scripts/grafana-server/start-server"}}) + WithExposedPort(3001) + + var licenseArg string + if opts.License != nil { + container = container.WithMountedFile("/src/license.jwt", opts.License) + licenseArg = "/src/license.jwt" + } + + if opts.InstallImageRenderer { + container = container.WithEnvVariable("INSTALL_IMAGE_RENDERER", "true"). + WithExec([]string{"apt-get", "update"}). + WithExec([]string{"apt-get", "install", "-y", "ca-certificates"}) + } + + // We add all GF_ environment variables to allow for overriding Grafana configuration. + // It is unlikely the runner has any such otherwise. + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GF_") { + parts := strings.SplitN(env, "=", 2) + container = container.WithEnvVariable(parts[0], parts[1]) + } + } + + svc := container.AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "-x", "scripts/grafana-server/start-server", licenseArg}}) return svc, nil } diff --git a/pkg/build/go.mod b/pkg/build/go.mod index bcbabd1f502..0df8e09498c 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/build -go 1.24.3 +go 1.24.4 // Override docker/docker to avoid: // go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires @@ -82,6 +82,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/google/go-github/v70 v70.0.0 github.com/quasilyte/go-ruleguard/dsl v0.3.22 + github.com/urfave/cli/v3 v3.3.3 ) require ( diff --git a/pkg/build/go.sum b/pkg/build/go.sum index e0002142e22..d6baed9d369 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -251,6 +251,8 @@ github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= +github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= diff --git a/pkg/build/wire/go.mod b/pkg/build/wire/go.mod index 3b1d875b3fd..f2286a2c311 100644 --- a/pkg/build/wire/go.mod +++ b/pkg/build/wire/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/build/wire -go 1.24.3 +go 1.24.4 require ( github.com/google/go-cmp v0.7.0 diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 6eea447e2e1..d56a680547a 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/codegen -go 1.24.3 +go 1.24.4 require ( cuelang.org/go v0.11.1 diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go index 4d760dff930..e19b73dd15e 100644 --- a/pkg/expr/graph.go +++ b/pkg/expr/graph.go @@ -328,14 +328,6 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error { for _, neededVar := range cmdNode.Command.NeedsVars() { neededNode, ok := registry[neededVar] if !ok { - _, ok := cmdNode.Command.(*SQLCommand) - // If the SSE is a SQL expression, and the node can't be found, it might be a CTE table name - // CTEs are calculated during the evaluation of the SQL, so we won't have a node for them - // So we `continue` in order to support CTE functionality - // TODO: remove CTE table names from the list of table names during parsing of the SQL - if ok { - continue - } return fmt.Errorf("unable to find dependent node '%v'", neededVar) } diff --git a/pkg/expr/sql/parser.go b/pkg/expr/sql/parser.go index c65e4b89131..0e401c94a15 100644 --- a/pkg/expr/sql/parser.go +++ b/pkg/expr/sql/parser.go @@ -3,6 +3,7 @@ package sql import ( "fmt" "sort" + "strings" "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/grafana/grafana/pkg/infra/log" @@ -10,7 +11,8 @@ import ( var logger = log.New("sql_expr") -// TablesList returns a list of tables for the sql statement +// TablesList returns a list of tables for the sql statement excluding +// CTEs and the 'dual' table. The list is sorted alphabetically. func TablesList(rawSQL string) ([]string, error) { stmt, err := sqlparser.Parse(rawSQL) if err != nil { @@ -19,10 +21,18 @@ func TablesList(rawSQL string) ([]string, error) { } tables := make(map[string]struct{}) + cteNames := make(map[string]struct{}) walkSubtree := func(node sqlparser.SQLNode) error { err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { switch v := node.(type) { + case *sqlparser.CommonTableExpr: + // Track CTE name from the As field + cteName := v.As.String() + if cteName != "" { + cteNames[strings.ToLower(cteName)] = struct{}{} + } + case *sqlparser.AliasedTableExpr: if tableName, ok := v.Expr.(sqlparser.TableName); ok { tables[tableName.Name.String()] = struct{}{} @@ -49,9 +59,15 @@ func TablesList(rawSQL string) ([]string, error) { // Remove 'dual' table if it exists // This is a special table in MySQL that always returns a single row with a single column // See: https://dev.mysql.com/doc/refman/5.7/en/select.html#:~:text=You%20are%20permitted%20to%20specify%20DUAL%20as%20a%20dummy%20table%20name%20in%20situations%20where%20no%20tables%20are%20referenced - if table != "dual" { - result = append(result, table) + if table == "dual" { + continue } + + // Skip CTEs + if _, ok := cteNames[strings.ToLower(table)]; ok { + continue + } + result = append(result, table) } sort.Strings(result) diff --git a/pkg/expr/sql/parser_test.go b/pkg/expr/sql/parser_test.go index 17235684388..44ac1ccb102 100644 --- a/pkg/expr/sql/parser_test.go +++ b/pkg/expr/sql/parser_test.go @@ -105,7 +105,7 @@ func TestTablesList(t *testing.T) { ) SELECT name, price FROM top_products;`, - expected: []string{"products", "top_products"}, + expected: []string{"products"}, }, { name: "with quote", diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index 78b31cd6f6f..9b80d5ffcbf 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/plugins/codegen -go 1.24.3 +go 1.24.4 replace github.com/grafana/grafana/pkg/codegen => ../../codegen diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index e60338003b9..2854642a28a 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/promlib -go 1.24.3 +go 1.24.4 require ( github.com/grafana/dskit v0.0.0-20250317084829-9cdd36a91f10 diff --git a/pkg/registry/apis/query/parser.go b/pkg/registry/apis/query/parser.go index 965a0a173c3..96089f763d6 100644 --- a/pkg/registry/apis/query/parser.go +++ b/pkg/registry/apis/query/parser.go @@ -167,14 +167,8 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe q, ok := queryRefIDs[refId] if !ok { - _, isSQLCMD := target.Command.(*expr.SQLCommand) - if isSQLCMD { - continue - } else { - target, ok = expressions[refId] - if !ok { - return rsp, makeDependencyError(exp.RefID, refId) - } + if target, ok = expressions[refId]; !ok { + return rsp, makeDependencyError(exp.RefID, refId) } } diff --git a/pkg/registry/apis/query/parser_test.go b/pkg/registry/apis/query/parser_test.go index e3e189f7ab1..f43e5295631 100644 --- a/pkg/registry/apis/query/parser_test.go +++ b/pkg/registry/apis/query/parser_test.go @@ -201,6 +201,46 @@ func TestSqlInputs(t *testing.T) { require.Equal(t, parsedRequestInfo.SqlInputs["B"], struct{}{}) } +func TestSqlCTE(t *testing.T) { + parser := newQueryParser( + expr.NewExpressionQueryReader(featuremgmt.WithFeatures(featuremgmt.FlagSqlExpressions)), + nil, + tracing.InitializeTracerForTest(), + log.NewNopLogger(), + ) + + parsedRequestInfo, err := parser.parseRequest(context.Background(), &query.QueryDataRequest{ + QueryDataRequest: data.QueryDataRequest{ + Queries: []data.DataQuery{ + data.NewDataQuery(map[string]any{ + "refId": "A", + "datasource": &data.DataSourceRef{ + Type: "prometheus", + UID: "local-prom", + }, + }), + data.NewDataQuery(map[string]any{ + "refId": "B", + "datasource": &data.DataSourceRef{ + Type: "__expr__", + UID: "__expr__", + }, + "type": "sql", + "expression": `WITH CTE AS ( + SELECT + Month + FROM A + ) + + SELECT * FROM CTE`, + }), + }, + }, + }) + require.NoError(t, err) + require.Equal(t, parsedRequestInfo.SqlInputs["B"], struct{}{}) +} + func TestGrafanaDS(t *testing.T) { ctx := context.Background() parser := newQueryParser(expr.NewExpressionQueryReader(featuremgmt.WithFeatures()), diff --git a/pkg/semconv/go.mod b/pkg/semconv/go.mod index 40aa5ce47a3..f3a2f4b31dd 100644 --- a/pkg/semconv/go.mod +++ b/pkg/semconv/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/semconv -go 1.24.3 +go 1.24.4 require go.opentelemetry.io/otel v1.35.0 diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 97cf2bd4b3b..e4647a59a6f 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -9,6 +9,7 @@ const ( grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" grafanaFrontendPlatformSquad codeowner = "@grafana/grafana-frontend-platform" + grafanaFrontendSearchNavOrganise codeowner = "@grafana/grafana-search-navigate-organise" grafanaBackendServicesSquad codeowner = "@grafana/grafana-backend-services-squad" grafanaSearchAndStorageSquad codeowner = "@grafana/search-and-storage" grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index bae02255b88..d3c7f88f746 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -393,6 +393,7 @@ func generateDocsMD() string { buf := `--- aliases: - /docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/ + - ../../administration/feature-toggles/ # /docs/grafana/latest/administration/feature-toggles/ description: Learn about feature toggles, which you can enable or disable. title: Configure feature toggles weight: 150 diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index 24b269cdfa5..5141ef89c0a 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -513,8 +513,16 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup( return grafanaGroup, nil } -func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, config apimodels.AlertmanagerUserConfig) response.Response { - return response.Error(501, "Not implemented", nil) +func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, amCfg apimodels.AlertmanagerUserConfig) response.Response { + return response.Error(http.StatusNotImplemented, "Not Implemented", nil) +} + +func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c *contextmodel.ReqContext) response.Response { + return response.Error(http.StatusNotImplemented, "Not Implemented", nil) +} + +func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteAlertmanagerConfig(c *contextmodel.ReqContext) response.Response { + return response.Error(http.StatusNotImplemented, "Not Implemented", nil) } // parseBooleanHeader parses a boolean header value, returning an error if the header diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 9eccfa36d2e..46168688ee6 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -147,8 +147,11 @@ func (api *API) authorize(method, path string) web.Handler { ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus), ) - case http.MethodPost + "/api/convert/api/v1/alerts": + case http.MethodPost + "/api/convert/api/v1/alerts", + http.MethodDelete + "/api/convert/api/v1/alerts": eval = ac.EvalPermission(ac.ActionAlertingNotificationsWrite) + case http.MethodGet + "/api/convert/api/v1/alerts": + eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead) // Alert Instances and Silences diff --git a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go index a414823d62e..aaf31682e85 100644 --- a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go @@ -26,8 +26,10 @@ type ConvertPrometheusApi interface { RouteConvertPrometheusCortexGetRules(*contextmodel.ReqContext) response.Response RouteConvertPrometheusCortexPostRuleGroup(*contextmodel.ReqContext) response.Response RouteConvertPrometheusCortexPostRuleGroups(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusDeleteAlertmanagerConfig(*contextmodel.ReqContext) response.Response RouteConvertPrometheusDeleteNamespace(*contextmodel.ReqContext) response.Response RouteConvertPrometheusDeleteRuleGroup(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusGetAlertmanagerConfig(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetRuleGroup(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetRules(*contextmodel.ReqContext) response.Response @@ -69,6 +71,9 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroup( func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroups(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteConvertPrometheusCortexPostRuleGroups(ctx) } +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteConvertPrometheusDeleteAlertmanagerConfig(ctx) +} func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] @@ -80,6 +85,9 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteRuleGroup(ctx groupParam := web.Params(ctx.Req)[":Group"] return f.handleRouteConvertPrometheusDeleteRuleGroup(ctx, namespaceTitleParam, groupParam) } +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteConvertPrometheusGetAlertmanagerConfig(ctx) +} func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetNamespace(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] @@ -192,6 +200,18 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi, m, ), ) + group.Delete( + toMacaronPath("/api/convert/api/v1/alerts"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodDelete, "/api/convert/api/v1/alerts"), + metrics.Instrument( + http.MethodDelete, + "/api/convert/api/v1/alerts", + api.Hooks.Wrap(srv.RouteConvertPrometheusDeleteAlertmanagerConfig), + m, + ), + ) group.Delete( toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), requestmeta.SetOwner(requestmeta.TeamAlerting), @@ -216,6 +236,18 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi, m, ), ) + group.Get( + toMacaronPath("/api/convert/api/v1/alerts"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/api/v1/alerts"), + metrics.Instrument( + http.MethodGet, + "/api/convert/api/v1/alerts", + api.Hooks.Wrap(srv.RouteConvertPrometheusGetAlertmanagerConfig), + m, + ), + ) group.Get( toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), requestmeta.SetOwner(requestmeta.TeamAlerting), diff --git a/pkg/services/ngalert/api/prometheus_conversion.go b/pkg/services/ngalert/api/prometheus_conversion.go index 332f2b39f14..4006dcb0e10 100644 --- a/pkg/services/ngalert/api/prometheus_conversion.go +++ b/pkg/services/ngalert/api/prometheus_conversion.go @@ -134,3 +134,11 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostAlertmanag return f.svc.RouteConvertPrometheusPostAlertmanagerConfig(ctx, config) } + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response { + return f.svc.RouteConvertPrometheusGetAlertmanagerConfig(ctx) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusDeleteAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response { + return f.svc.RouteConvertPrometheusDeleteAlertmanagerConfig(ctx) +} diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 84979f94fb4..ddbb155712a 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -580,6 +580,12 @@ "properties": { "alertmanager_config": { "$ref": "#/definitions/Config" + }, + "template_files": { + "additionalProperties": { + "type": "string" + }, + "type": "object" } }, "type": "object" @@ -1408,6 +1414,20 @@ "title": "Frames is a slice of Frame pointers.", "type": "array" }, + "GettableAlertmanagerUserConfig": { + "properties": { + "alertmanager_config": { + "type": "string" + }, + "template_files": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "GettableAlertmanagers": { "properties": { "data": { @@ -2038,7 +2058,11 @@ "description": "InhibitRule defines an inhibition rule that mutes alerts that match the\ntarget labels if an alert matching the source labels exists.\nBoth alerts have to have a set of labels being equal.", "properties": { "equal": { - "$ref": "#/definitions/LabelNames" + "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.", + "items": { + "type": "string" + }, + "type": "array" }, "source_match": { "additionalProperties": { @@ -2162,17 +2186,6 @@ "title": "Label is a key/value pair of strings.", "type": "object" }, - "LabelName": { - "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.", - "type": "string" - }, - "LabelNames": { - "items": { - "$ref": "#/definitions/LabelName" - }, - "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.", - "type": "array" - }, "LabelSet": { "additionalProperties": { "$ref": "#/definitions/LabelValue" @@ -3674,6 +3687,7 @@ "type": "object" }, "Route": { + "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "properties": { "active_time_intervals": { "items": { @@ -3715,6 +3729,12 @@ }, "type": "array" }, + "object_matchers": { + "$ref": "#/definitions/ObjectMatchers" + }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, "receiver": { "type": "string" }, @@ -3728,7 +3748,6 @@ "type": "array" } }, - "title": "A Route is a node that contains definitions of how to handle alerts.", "type": "object" }, "RouteExport": { diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go index 813c99e8cbc..c4609cbc5e4 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -3,6 +3,7 @@ package definitions import ( "github.com/prometheus/alertmanager/config" "github.com/prometheus/common/model" + "gopkg.in/yaml.v3" ) // Route for mimirtool @@ -204,7 +205,10 @@ import ( // Route for `mimirtool alertmanager load` // swagger:route POST /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusPostAlertmanagerConfig // -// Load Alertmanager configuration to Grafana and merge it with the existing configuration. +// Load extra Alertmanager configuration to Grafana and merge it with the existing configuration. +// This endpoint allows importing Alertmanager configurations to Grafana. Each configuration is identified by +// a unique identifier and can include merge matchers to select which alerts should be handled by +// this specific configuration. // // Produces: // - application/json @@ -216,6 +220,32 @@ import ( // Extensions: // x-raw-request: true +// Route for `mimirtool alertmanager get` +// swagger:route GET /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusGetAlertmanagerConfig +// +// Get extra Alertmanager configuration from Grafana. +// Returns a specific imported Alertmanager configuration by its identifier. +// +// Produces: +// - application/yaml +// +// Responses: +// 200: GettableAlertmanagerUserConfig +// 403: ForbiddenError + +// Route for `mimirtool alertmanager delete` +// swagger:route DELETE /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusDeleteAlertmanagerConfig +// +// Delete extra Alertmanager configuration from Grafana by its identifier. +// The main Grafana Alertmanager configuration remains unaffected. +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError + // swagger:parameters RouteConvertPrometheusPostRuleGroup RouteConvertPrometheusCortexPostRuleGroup type RouteConvertPrometheusPostRuleGroupParams struct { // in: path @@ -286,11 +316,64 @@ type ConvertPrometheusResponse struct { // swagger:parameters RouteConvertPrometheusPostAlertmanagerConfig type RouteConvertPrometheusPostAlertmanagerConfigParams struct { + // Unique identifier for this Alertmanager configuration. + // This identifier is used to distinguish between different imported configurations. + // in: header + Identifier string `json:"x-grafana-alerting-config-identifier"` + // Comma-separated list of label matchers in 'key=value' format. + // These matchers determine which alerts this configuration should handle. + // For example: 'environment=production,team=backend' will only apply this + // configuration to alerts matching both environment=production AND team=backend. + // in: header + MergeMatchers string `json:"x-grafana-alerting-merge-matchers"` + // Alertmanager configuration including routing rules, receivers, and template files // in:body Body AlertmanagerUserConfig } +// swagger:parameters RouteConvertPrometheusGetAlertmanagerConfig +type RouteConvertPrometheusGetAlertmanagerConfigParams struct { + // Unique identifier for the Alertmanager configuration to retrieve. + // in: header + Identifier string `json:"x-grafana-alerting-config-identifier"` +} + +// swagger:parameters RouteConvertPrometheusDeleteAlertmanagerConfig +type RouteConvertPrometheusDeleteAlertmanagerConfigParams struct { + // Unique identifier for the Alertmanager configuration to delete. + // in: header + Identifier string `json:"x-grafana-alerting-config-identifier"` +} + // swagger:model type AlertmanagerUserConfig struct { - AlertmanagerConfig config.Config `yaml:"alertmanager_config" json:"alertmanager_config"` + AlertmanagerConfig config.Config `yaml:"alertmanager_config" json:"alertmanager_config"` + TemplateFiles map[string]string `yaml:"template_files" json:"template_files"` +} + +func (c *AlertmanagerUserConfig) UnmarshalYAML(value *yaml.Node) error { + // mimirtool sends alertmanager_config as a string + type cortexAlertmanagerUserConfig struct { + TemplateFiles map[string]string `yaml:"template_files" json:"template_files"` + AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"` + } + + var tmp cortexAlertmanagerUserConfig + + if err := value.Decode(&tmp); err != nil { + return err + } + + if err := yaml.Unmarshal([]byte(tmp.AlertmanagerConfig), &c.AlertmanagerConfig); err != nil { + return err + } + c.TemplateFiles = tmp.TemplateFiles + + return nil +} + +// swagger:model +type GettableAlertmanagerUserConfig struct { + AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"` + TemplateFiles map[string]string `yaml:"template_files" json:"template_files"` } diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 455cf5cb50c..62a11b3e3c1 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -580,6 +580,12 @@ "properties": { "alertmanager_config": { "$ref": "#/definitions/Config" + }, + "template_files": { + "additionalProperties": { + "type": "string" + }, + "type": "object" } }, "type": "object" @@ -1408,6 +1414,20 @@ "title": "Frames is a slice of Frame pointers.", "type": "array" }, + "GettableAlertmanagerUserConfig": { + "properties": { + "alertmanager_config": { + "type": "string" + }, + "template_files": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "GettableAlertmanagers": { "properties": { "data": { @@ -2038,7 +2058,11 @@ "description": "InhibitRule defines an inhibition rule that mutes alerts that match the\ntarget labels if an alert matching the source labels exists.\nBoth alerts have to have a set of labels being equal.", "properties": { "equal": { - "$ref": "#/definitions/LabelNames" + "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.", + "items": { + "type": "string" + }, + "type": "array" }, "source_match": { "additionalProperties": { @@ -2162,17 +2186,6 @@ "title": "Label is a key/value pair of strings.", "type": "object" }, - "LabelName": { - "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.", - "type": "string" - }, - "LabelNames": { - "items": { - "$ref": "#/definitions/LabelName" - }, - "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.", - "type": "array" - }, "LabelSet": { "additionalProperties": { "$ref": "#/definitions/LabelValue" @@ -3674,6 +3687,7 @@ "type": "object" }, "Route": { + "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "properties": { "active_time_intervals": { "items": { @@ -3715,6 +3729,12 @@ }, "type": "array" }, + "object_matchers": { + "$ref": "#/definitions/ObjectMatchers" + }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, "receiver": { "type": "string" }, @@ -3728,7 +3748,6 @@ "type": "array" } }, - "title": "A Route is a node that contains definitions of how to handle alerts.", "type": "object" }, "RouteExport": { @@ -6823,10 +6842,91 @@ } }, "/convert/api/v1/alerts": { + "delete": { + "description": "The main Grafana Alertmanager configuration remains unaffected.", + "operationId": "RouteConvertPrometheusDeleteAlertmanagerConfig", + "parameters": [ + { + "description": "Unique identifier for the Alertmanager configuration to delete.", + "in": "header", + "name": "x-grafana-alerting-config-identifier", + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Delete extra Alertmanager configuration from Grafana by its identifier.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "description": "Returns a specific imported Alertmanager configuration by its identifier.", + "operationId": "RouteConvertPrometheusGetAlertmanagerConfig", + "parameters": [ + { + "description": "Unique identifier for the Alertmanager configuration to retrieve.", + "in": "header", + "name": "x-grafana-alerting-config-identifier", + "type": "string" + } + ], + "produces": [ + "application/yaml" + ], + "responses": { + "200": { + "description": "GettableAlertmanagerUserConfig", + "schema": { + "$ref": "#/definitions/GettableAlertmanagerUserConfig" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Get extra Alertmanager configuration from Grafana.", + "tags": [ + "convert_prometheus" + ] + }, "post": { + "description": "This endpoint allows importing Alertmanager configurations to Grafana. Each configuration is identified by\na unique identifier and can include merge matchers to select which alerts should be handled by\nthis specific configuration.", "operationId": "RouteConvertPrometheusPostAlertmanagerConfig", "parameters": [ { + "description": "Unique identifier for this Alertmanager configuration.\nThis identifier is used to distinguish between different imported configurations.", + "in": "header", + "name": "x-grafana-alerting-config-identifier", + "type": "string" + }, + { + "description": "Comma-separated list of label matchers in 'key=value' format.\nThese matchers determine which alerts this configuration should handle.", + "example": "'environment=production,team=backend' will only apply this", + "in": "header", + "name": "x-grafana-alerting-merge-matchers", + "type": "string" + }, + { + "description": "Alertmanager configuration including routing rules, receivers, and template files", "in": "body", "name": "Body", "schema": { @@ -6851,7 +6951,7 @@ } } }, - "summary": "Load Alertmanager configuration to Grafana and merge it with the existing configuration.", + "summary": "Load extra Alertmanager configuration to Grafana and merge it with the existing configuration.", "tags": [ "convert_prometheus" ], diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index f40362f55dd..addee647af0 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1361,17 +1361,65 @@ } }, "/convert/api/v1/alerts": { + "get": { + "description": "Returns a specific imported Alertmanager configuration by its identifier.", + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Get extra Alertmanager configuration from Grafana.", + "operationId": "RouteConvertPrometheusGetAlertmanagerConfig", + "parameters": [ + { + "type": "string", + "description": "Unique identifier for the Alertmanager configuration to retrieve.", + "name": "x-grafana-alerting-config-identifier", + "in": "header" + } + ], + "responses": { + "200": { + "description": "GettableAlertmanagerUserConfig", + "schema": { + "$ref": "#/definitions/GettableAlertmanagerUserConfig" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } + }, "post": { + "description": "This endpoint allows importing Alertmanager configurations to Grafana. Each configuration is identified by\na unique identifier and can include merge matchers to select which alerts should be handled by\nthis specific configuration.", "produces": [ "application/json" ], "tags": [ "convert_prometheus" ], - "summary": "Load Alertmanager configuration to Grafana and merge it with the existing configuration.", + "summary": "Load extra Alertmanager configuration to Grafana and merge it with the existing configuration.", "operationId": "RouteConvertPrometheusPostAlertmanagerConfig", "parameters": [ { + "type": "string", + "description": "Unique identifier for this Alertmanager configuration.\nThis identifier is used to distinguish between different imported configurations.", + "name": "x-grafana-alerting-config-identifier", + "in": "header" + }, + { + "type": "string", + "example": "'environment=production,team=backend' will only apply this", + "description": "Comma-separated list of label matchers in 'key=value' format.\nThese matchers determine which alerts this configuration should handle.", + "name": "x-grafana-alerting-merge-matchers", + "in": "header" + }, + { + "description": "Alertmanager configuration including routing rules, receivers, and template files", "name": "Body", "in": "body", "schema": { @@ -1394,6 +1442,39 @@ } }, "x-raw-request": "true" + }, + "delete": { + "description": "The main Grafana Alertmanager configuration remains unaffected.", + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Delete extra Alertmanager configuration from Grafana by its identifier.", + "operationId": "RouteConvertPrometheusDeleteAlertmanagerConfig", + "parameters": [ + { + "type": "string", + "description": "Unique identifier for the Alertmanager configuration to delete.", + "name": "x-grafana-alerting-config-identifier", + "in": "header" + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } } }, "/convert/prometheus/config/v1/rules": { @@ -4788,6 +4869,12 @@ "properties": { "alertmanager_config": { "$ref": "#/definitions/Config" + }, + "template_files": { + "type": "object", + "additionalProperties": { + "type": "string" + } } } }, @@ -5616,6 +5703,20 @@ "$ref": "#/definitions/Frame" } }, + "GettableAlertmanagerUserConfig": { + "type": "object", + "properties": { + "alertmanager_config": { + "type": "string" + }, + "template_files": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "GettableAlertmanagers": { "type": "object", "properties": { @@ -6247,7 +6348,11 @@ "type": "object", "properties": { "equal": { - "$ref": "#/definitions/LabelNames" + "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.", + "type": "array", + "items": { + "type": "string" + } }, "source_match": { "description": "SourceMatch defines a set of labels that have to equal the given\nvalue for source alerts. Deprecated. Remove before v1.0 release.", @@ -6370,17 +6475,6 @@ } } }, - "LabelName": { - "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.", - "type": "string" - }, - "LabelNames": { - "type": "array", - "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.", - "items": { - "$ref": "#/definitions/LabelName" - } - }, "LabelSet": { "description": "A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet\nmay be fully-qualified down to the point where it may resolve to a single\nMetric in the data store or not. All operations that occur within the realm\nof a LabelSet can emit a vector of Metric entities to which the LabelSet may\nmatch.", "type": "object", @@ -7883,8 +7977,8 @@ } }, "Route": { + "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "type": "object", - "title": "A Route is a node that contains definitions of how to handle alerts.", "properties": { "active_time_intervals": { "type": "array", @@ -7926,6 +8020,12 @@ "type": "string" } }, + "object_matchers": { + "$ref": "#/definitions/ObjectMatchers" + }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, "receiver": { "type": "string" }, diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index 9586e826176..240c62bdc40 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -318,7 +318,6 @@ func (r ruleWithFolder) Fingerprint() fingerprint { writeInt(*rule.PanelID) } writeString(rule.RuleGroup) - writeInt(int64(rule.RuleGroupIndex)) writeString(string(rule.NoDataState)) writeString(string(rule.ExecErrState)) if rule.Record != nil { diff --git a/pkg/services/ngalert/schedule/registry_test.go b/pkg/services/ngalert/schedule/registry_test.go index d4c835cf6f6..9d838556ed7 100644 --- a/pkg/services/ngalert/schedule/registry_test.go +++ b/pkg/services/ngalert/schedule/registry_test.go @@ -152,13 +152,14 @@ func TestRuleWithFolderFingerprint(t *testing.T) { f2 := ruleWithFolder{rule: rule, folderTitle: uuid.NewString()}.Fingerprint() require.NotEqual(t, f, f2) }) - t.Run("Version, Updated, IntervalSeconds, GUID and Annotations should be excluded from fingerprint", func(t *testing.T) { + t.Run("Version, Updated, IntervalSeconds, GUID, Annotations and RuleGroupIndex should be excluded from fingerprint", func(t *testing.T) { cp := models.CopyRule(rule) cp.Version++ cp.Updated = cp.Updated.Add(1 * time.Second) cp.IntervalSeconds++ cp.Annotations = make(map[string]string) cp.Annotations["test"] = "test" + cp.RuleGroupIndex++ cp.GUID = uuid.NewString() f2 := ruleWithFolder{rule: cp, folderTitle: title}.Fingerprint() diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index 268eeea2d72..eead8de614f 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -46,10 +46,11 @@ type CreateTeamCommand struct { } type UpdateTeamCommand struct { - ID int64 - Name string - Email string - OrgID int64 `json:"-"` + ID int64 + Name string + Email string + ExternalUID string + OrgID int64 `json:"-"` } type DeleteTeamCommand struct { @@ -98,7 +99,7 @@ type TeamDTO struct { OrgID int64 `json:"orgId" xorm:"org_id"` Name string `json:"name"` Email string `json:"email"` - ExternalUID string `json:"externalUID"` + ExternalUID string `json:"externalUID" xorm:"external_uid"` IsProvisioned bool `json:"isProvisioned"` AvatarURL string `json:"avatarUrl"` MemberCount int64 `json:"memberCount"` diff --git a/pkg/services/team/teamimpl/store.go b/pkg/services/team/teamimpl/store.go index 8a4b77a97f1..d7502c48fc2 100644 --- a/pkg/services/team/teamimpl/store.go +++ b/pkg/services/team/teamimpl/store.go @@ -111,9 +111,10 @@ func (ss *xormStore) Update(ctx context.Context, cmd *team.UpdateTeamCommand) er } t := team.Team{ - Name: cmd.Name, - Email: cmd.Email, - Updated: time.Now(), + Name: cmd.Name, + Email: cmd.Email, + ExternalUID: cmd.ExternalUID, + Updated: time.Now(), } sess.MustCols("email") diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 568b7826c44..bad7ad32f3a 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -8666,6 +8666,9 @@ "Email": { "type": "string" }, + "ExternalUID": { + "type": "string" + }, "ID": { "type": "integer", "format": "int64" @@ -10647,4 +10650,4 @@ "name": "service_accounts" } ] -} +} \ No newline at end of file diff --git a/public/api-merged.json b/public/api-merged.json index 7b2a63632a9..ae2060e66de 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -12999,6 +12999,12 @@ "properties": { "alertmanager_config": { "$ref": "#/definitions/Config" + }, + "template_files": { + "type": "object", + "additionalProperties": { + "type": "string" + } } } }, @@ -15855,6 +15861,20 @@ } } }, + "GettableAlertmanagerUserConfig": { + "type": "object", + "properties": { + "alertmanager_config": { + "type": "string" + }, + "template_files": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "GettableAlertmanagers": { "type": "object", "properties": { @@ -16709,7 +16729,11 @@ "type": "object", "properties": { "equal": { - "$ref": "#/definitions/LabelNames" + "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.", + "type": "array", + "items": { + "type": "string" + } }, "source_match": { "description": "SourceMatch defines a set of labels that have to equal the given\nvalue for source alerts. Deprecated. Remove before v1.0 release.", @@ -16884,17 +16908,6 @@ } } }, - "LabelName": { - "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.", - "type": "string" - }, - "LabelNames": { - "type": "array", - "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.", - "items": { - "$ref": "#/definitions/LabelName" - } - }, "LabelSet": { "description": "A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet\nmay be fully-qualified down to the point where it may resolve to a single\nMetric in the data store or not. All operations that occur within the realm\nof a LabelSet can emit a vector of Metric entities to which the LabelSet may\nmatch.", "type": "object", @@ -20027,8 +20040,8 @@ } }, "Route": { + "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "type": "object", - "title": "A Route is a node that contains definitions of how to handle alerts.", "properties": { "active_time_intervals": { "type": "array", @@ -20070,6 +20083,12 @@ "type": "string" } }, + "object_matchers": { + "$ref": "#/definitions/ObjectMatchers" + }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, "receiver": { "type": "string" }, @@ -22411,6 +22430,9 @@ "Email": { "type": "string" }, + "ExternalUID": { + "type": "string" + }, "ID": { "type": "integer", "format": "int64" diff --git a/public/app/core/components/OptionsUI/registry.tsx b/public/app/core/components/OptionsUI/registry.tsx index d1c51c96de8..123ede77f89 100644 --- a/public/app/core/components/OptionsUI/registry.tsx +++ b/public/app/core/components/OptionsUI/registry.tsx @@ -87,8 +87,7 @@ export const getAllOptionEditors = () => { name: 'Boolean', description: 'Allows boolean values input', editor(props) { - const { id, ...rest } = props; // Remove id from properties passed into switch - return props.onChange(e.currentTarget.checked)} />; + return props.onChange(e.currentTarget.checked)} />; }, }; diff --git a/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts b/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts index c73b3788b11..5b3c6c97a03 100644 --- a/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts +++ b/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts @@ -1,5 +1,3 @@ -import type { apiOptions, identify, load, page, track } from 'rudder-sdk-js'; // SDK is loaded dynamically from config, so we only import types from the SDK package - import { BuildInfo, CurrentUserDTO } from '@grafana/data'; import { EchoBackend, @@ -12,11 +10,19 @@ import { import { loadScript } from '../../utils'; +type Properties = Record; + +interface RudderstackAPIOptions { + Intercom?: { + user_hash: string; + }; +} + interface Rudderstack { - identify: typeof identify; - load: typeof load; - page: typeof page; - track: typeof track; + identify: (identifier: string, traits: Properties, options?: RudderstackAPIOptions) => void; + load: (writeKey: string, dataPlaneURL: string, options: { configUrl?: string; destSDKBaseURL?: string }) => void; + page: () => void; + track: (eventName: string, properties?: Properties) => void; } declare global { @@ -76,7 +82,7 @@ export class RudderstackBackend implements EchoBackend { +describe('ImportToGMARules', () => { grantUserPermissions([AccessControlAction.AlertingRuleExternalRead, AccessControlAction.AlertingRuleCreate]); testWithFeatureToggles(['alertingImportYAMLUI', 'alertingMigrationUI']); @@ -62,8 +62,9 @@ describe.skip('ImportToGMARules', () => { it('should render datasource options', async () => { const { user } = render(); - // Wait for the data source picker to be ready + // Wait for the data source picker to be ready and enabled const dsPicker = await ui.dsImport.dsPicker.find(); + await waitFor(() => expect(dsPicker).toBeEnabled()); await user.click(dsPicker); await user.click(await ui.dsImport.mimirDsOption.find()); @@ -91,8 +92,12 @@ describe.skip('ImportToGMARules', () => { it('should show confirmation dialog when importing from data source', async () => { const { user } = render(); + // Wait for the data source picker to be enabled + const dsPicker = ui.dsImport.dsPicker.get(); + await waitFor(() => expect(dsPicker).toBeEnabled()); + // Select a data source - await user.click(ui.dsImport.dsPicker.get()); + await user.click(dsPicker); await user.click(await ui.dsImport.mimirDsOption.find()); // Click the import button diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.tsx index 8a7b0a83a9c..b904a5c1427 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.tsx @@ -3,11 +3,14 @@ import { Suspense, lazy } from 'react'; import { config } from '@grafana/runtime'; import RulesFilterV1 from './RulesFilter.v1'; +import { SupportedView } from './RulesViewModeSelector'; const RulesFilterV2 = lazy(() => import('./RulesFilter.v2')); interface RulesFilerProps { onClear?: () => void; + viewMode?: SupportedView; + onViewModeChange?: (viewMode: SupportedView) => void; } const RulesFilter = (props: RulesFilerProps) => { diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx index 4ff5da3f8e1..97fff1abff5 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx @@ -26,7 +26,7 @@ import { alertStateToReadable } from '../../../utils/rules'; import { PopupCard } from '../../HoverCard'; import { MultipleDataSourcePicker } from '../MultipleDataSourcePicker'; -import { RulesViewModeSelector } from './RulesViewModeSelector'; +import { RulesViewModeSelector, SupportedView } from './RulesViewModeSelector'; const RuleTypeOptions: SelectableValue[] = [ { label: 'Alert ', value: PromRuleType.Alerting }, @@ -44,6 +44,8 @@ const canRenderContactPointSelector = contextSrv.hasPermission(AccessControlActi interface RulesFilerProps { onClear?: () => void; + viewMode?: SupportedView; + onViewModeChange?: (viewMode: SupportedView) => void; } const RuleStateOptions = Object.entries(PromAlertingRuleState).map(([key, value]) => ({ @@ -51,7 +53,7 @@ const RuleStateOptions = Object.entries(PromAlertingRuleState).map(([key, value] value, })); -const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => { +const RulesFilter = ({ onClear = () => undefined, viewMode, onViewModeChange }: RulesFilerProps) => { const styles = useStyles2(getStyles); const { pluginsFilterEnabled } = usePluginsFilterStatus(); const { filterState, hasActiveFilters, searchQuery, setSearchQuery, updateFilters } = useRulesFilter(); @@ -317,7 +319,7 @@ const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => { - + {hasActiveFilters && ( diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesViewModeSelector.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesViewModeSelector.tsx index fe8303fdfd1..661507d6ac8 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesViewModeSelector.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesViewModeSelector.tsx @@ -1,3 +1,5 @@ +import { useCallback } from 'react'; + import { SelectableValue } from '@grafana/data'; import { RadioButtonGroup } from '@grafana/ui'; @@ -15,24 +17,58 @@ const ViewOptions: Array> = [ { icon: 'list-ul', label: 'List', value: 'list' }, ]; -function RulesViewModeSelectorV2() { +interface RulesViewModeSelectorV2Props { + viewMode?: SupportedView; + onViewModeChange?: (viewMode: SupportedView) => void; +} + +/** + * Selecting a view mode is no longer a simple toggle relying on the URL query params. + * We now need to check if the current filters are compatible with the grouped view. + * If they are, we show the grouped view by default. + * If they are not, we show the list view. + * Use the complementary {@link useListViewMode} hook to get the current view mode and a handler for changing it. + */ +function RulesViewModeSelectorV2({ viewMode, onViewModeChange }: RulesViewModeSelectorV2Props) { + return ; +} + +export function useListViewMode() { const [queryParams, updateQueryParams] = useURLSearchParams(); - const { hasActiveFilters } = useRulesFilter(); - const wantsListView = queryParams.get('view') === 'list'; + const { activeFilters } = useRulesFilter(); - const selectedViewOption = hasActiveFilters || wantsListView ? 'list' : 'grouped'; + const queryStringView: SupportedView = queryParams.get('view') === 'list' ? 'list' : 'grouped'; - /* If we change to the grouped view, we just remove the "list" and "search" params */ - const handleViewChange = (view: SupportedView) => { - if (view === 'list') { - updateQueryParams({ view }); + const areFiltersGroupedViewCompatible = activeFilters.every( + (filter) => filter === 'groupName' || filter === 'namespace' + ); + const showListView = areFiltersGroupedViewCompatible === false || queryStringView === 'list'; + + const handleViewChange = useCallback( + (view: SupportedView) => { + if (view === 'grouped') { + // When switching to grouped view, preserve filters only if they are grouped-view compatible + if (areFiltersGroupedViewCompatible) { + // Only remove view parameter, keep search (preserve group/namespace filters) + updateQueryParams({ view: undefined }); + } else { + // Clear both view and search (clear all filters) + updateQueryParams({ view: undefined, search: undefined }); + } + } else { + updateQueryParams({ view }); + } trackRulesListViewChange({ view }); - } else { - updateQueryParams({ view: undefined, search: undefined }); - } - }; + }, + [updateQueryParams, areFiltersGroupedViewCompatible] + ); - return ; + const viewMode: SupportedView = showListView ? 'list' : 'grouped'; + + return { + viewMode, + handleViewChange, + }; } const LegacyViewOptions: Array> = [ @@ -66,4 +102,15 @@ function viewParamToLegacyView(viewParam: string | null): LegacySupportedView { return 'grouped'; } -export const RulesViewModeSelector = shouldUseAlertingListViewV2() ? RulesViewModeSelectorV2 : RulesViewModeSelectorV1; +interface RulesViewModeSelectorProps { + viewMode?: SupportedView; + onViewModeChange?: (viewMode: SupportedView) => void; +} + +export function RulesViewModeSelector({ viewMode, onViewModeChange }: RulesViewModeSelectorProps) { + if (shouldUseAlertingListViewV2()) { + return ; + } + + return ; +} diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts index c8123de32c4..7cd831550d2 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts @@ -38,6 +38,10 @@ export function useRulesFilter() { }, [searchQuery]); const hasActiveFilters = useMemo(() => Object.values(filterState).some((filter) => !isEmpty(filter)), [filterState]); + const activeFilters = useMemo(() => { + return chain(filterState).omitBy(isEmpty).keys().filter(isRuleFilterKey).value(); + }, [filterState]); + const updateFilters = useCallback( (newFilter: RulesFilter) => { const newSearchQuery = applySearchFilterToQuery(searchQuery, newFilter); @@ -86,7 +90,7 @@ export function useRulesFilter() { } }, [queryParams, updateFilters, filterState, updateQueryParams]); - return { filterState, hasActiveFilters, searchQuery, setSearchQuery, updateFilters }; + return { filterState, hasActiveFilters, activeFilters, searchQuery, setSearchQuery, updateFilters }; } export const useFilteredRules = (namespaces: CombinedRuleNamespace[], filterState: RulesFilter) => { @@ -356,3 +360,20 @@ const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filterState: Rules return ds?.name && filterState?.dataSourceNames?.includes(ds.name); }); }; + +const RULES_FILTER_KEYS: Set = new Set([ + 'freeFormWords', + 'namespace', + 'groupName', + 'ruleName', + 'ruleState', + 'ruleType', + 'dataSourceNames', + 'labels', + 'ruleHealth', + 'dashboardUid', + 'plugins', + 'contactPoint', +]); + +const isRuleFilterKey = (key: string): key is keyof RulesFilter => RULES_FILTER_KEYS.has(key as keyof RulesFilter); diff --git a/public/app/features/alerting/unified/rule-list/FilterView.tsx b/public/app/features/alerting/unified/rule-list/FilterView.tsx index 69260588c1d..27dd21bda6f 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.tsx @@ -22,14 +22,12 @@ import { RuleWithOrigin, useFilteredRulesIteratorProvider, } from './hooks/useFilteredRulesIterator'; +import { FRONTEND_LIST_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits'; interface FilterViewProps { filterState: RulesFilter; } -const FRONTENT_PAGE_SIZE = 100; -const API_PAGE_SIZE = 2000; - export function FilterView({ filterState }: FilterViewProps) { // ⚠️ We use a key to force the component to unmount and remount when the filter state changes // filterState is a complex object including arrays and is constructed from URL params @@ -79,10 +77,10 @@ function FilterViewResults({ filterState }: FilterViewProps) { * ⚠️ Make sure we are returning / using a "iterator" and not an "iterable" since the iterable is only a blueprint * and the iterator will allow us to exhaust the iterable in a stateful way */ - const { iterable, abortController } = getFilteredRulesIterator(filterState, API_PAGE_SIZE); + const { iterable, abortController } = getFilteredRulesIterator(filterState, getApiGroupPageSize(true)); const rulesBatchIterator = iterable .pipe( - bufferCountOrTime(FRONTENT_PAGE_SIZE, 1000), + bufferCountOrTime(FRONTEND_LIST_PAGE_SIZE, 1000), onFinished(() => setDoneSearching(true)) ) [Symbol.asyncIterator](); @@ -98,7 +96,7 @@ function FilterViewResults({ filterState }: FilterViewProps) { let loadedRulesCount = 0; - while (loadedRulesCount < FRONTENT_PAGE_SIZE) { + while (loadedRulesCount < FRONTEND_LIST_PAGE_SIZE) { const nextRulesBatch = await rulesIterator.next(); if (nextRulesBatch.done) { return; diff --git a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx index 680101129a4..32dcf3d56d0 100644 --- a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx @@ -10,7 +10,7 @@ import { setPrometheusRules } from '../mocks/server/configure'; import { alertingFactory } from '../mocks/server/db'; import { GroupedView } from './GroupedView'; -import { DATA_SOURCE_GROUP_PAGE_SIZE } from './PaginatedDataSourceLoader'; +import { FRONTED_GROUPED_PAGE_SIZE } from './paginationLimits'; setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); @@ -67,7 +67,7 @@ describe('RuleList - GroupedView', () => { const mimirNamespace = await ui.namespace(/test-mimir-namespace/).find(mimirSection); const firstPageGroups = await ui.group(/test-group-([1-9]|[1-3][0-9]|40)/).findAll(mimirNamespace); - expect(firstPageGroups).toHaveLength(DATA_SOURCE_GROUP_PAGE_SIZE); + expect(firstPageGroups).toHaveLength(FRONTED_GROUPED_PAGE_SIZE); expect(firstPageGroups[0]).toHaveTextContent('test-group-1'); expect(firstPageGroups[24]).toHaveTextContent('test-group-25'); expect(firstPageGroups[39]).toHaveTextContent('test-group-40'); @@ -79,7 +79,7 @@ describe('RuleList - GroupedView', () => { const secondPageGroups = await ui.group(/test-group-(4[1-9]|[5-7][0-9]|80)/).findAll(mimirNamespace); - expect(secondPageGroups).toHaveLength(DATA_SOURCE_GROUP_PAGE_SIZE); + expect(secondPageGroups).toHaveLength(FRONTED_GROUPED_PAGE_SIZE); expect(secondPageGroups[0]).toHaveTextContent('test-group-41'); expect(secondPageGroups[24]).toHaveTextContent('test-group-65'); expect(secondPageGroups[39]).toHaveTextContent('test-group-80'); diff --git a/public/app/features/alerting/unified/rule-list/GroupedView.tsx b/public/app/features/alerting/unified/rule-list/GroupedView.tsx index 3458ad6822c..6c1ca667df1 100644 --- a/public/app/features/alerting/unified/rule-list/GroupedView.tsx +++ b/public/app/features/alerting/unified/rule-list/GroupedView.tsx @@ -14,16 +14,32 @@ import { DataSourceSection } from './components/DataSourceSection'; const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi; -export function GroupedView() { +interface GroupedViewProps { + groupFilter?: string; + namespaceFilter?: string; +} + +export function GroupedView({ groupFilter, namespaceFilter }: GroupedViewProps) { const externalRuleSources = useMemo(() => getExternalRulesSources(), []); return ( - + {externalRuleSources.map((ruleSource) => { - return ; + return ( + + ); })} ); @@ -31,13 +47,15 @@ export function GroupedView() { interface DataSourceLoaderProps { rulesSourceIdentifier: DataSourceRulesSourceIdentifier; + groupFilter?: string; + namespaceFilter?: string; } export function GrafanaDataSourceLoader() { return ; } -function DataSourceLoader({ rulesSourceIdentifier }: DataSourceLoaderProps) { +function DataSourceLoader({ rulesSourceIdentifier, groupFilter, namespaceFilter }: DataSourceLoaderProps) { const { data: dataSourceInfo, isLoading, error } = useDiscoverDsFeaturesQuery({ uid: rulesSourceIdentifier.uid }); const { uid, name } = rulesSourceIdentifier; @@ -55,9 +73,10 @@ function DataSourceLoader({ rulesSourceIdentifier }: DataSourceLoaderProps) { return ( ); diff --git a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx index c37e07491f8..df4f033d6f9 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx @@ -1,11 +1,10 @@ -import { css } from '@emotion/css'; import { groupBy, isEmpty } from 'lodash'; import { useEffect, useMemo, useRef } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; -import { Icon, Spinner, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Icon, Spinner, Stack, Text } from '@grafana/ui'; import { DataSourceRuleGroupIdentifier, DataSourceRulesSourceIdentifier, RuleGroup } from 'app/types/unified-alerting'; +import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { groups } from '../utils/navigation'; @@ -15,23 +14,54 @@ import { GroupIntervalIndicator } from './components/GroupIntervalMetadata'; import { ListGroup } from './components/ListGroup'; import { ListSection } from './components/ListSection'; import { LoadMoreButton } from './components/LoadMoreButton'; +import { NoRulesFound } from './components/NoRulesFound'; +import { groupFilter as groupFilterFn } from './hooks/filters'; import { toIndividualRuleGroups, usePrometheusGroupsGenerator } from './hooks/prometheusGroupsGenerator'; import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups'; +import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits'; -export const DATA_SOURCE_GROUP_PAGE_SIZE = 40; - -interface PaginatedDataSourceLoaderProps extends Required> { +interface LoaderProps extends Required> { rulesSourceIdentifier: DataSourceRulesSourceIdentifier; + groupFilter?: string; + namespaceFilter?: string; } -export function PaginatedDataSourceLoader({ rulesSourceIdentifier, application }: PaginatedDataSourceLoaderProps) { - const styles = useStyles2(getStyles); +export function PaginatedDataSourceLoader({ + rulesSourceIdentifier, + application, + groupFilter, + namespaceFilter, +}: LoaderProps) { + const key = `${rulesSourceIdentifier.uid}-${groupFilter}-${namespaceFilter}`; + + // Key is crucial. It resets the generator when filters change. + return ( + + ); +} + +function PaginatedGroupsLoader({ rulesSourceIdentifier, application, groupFilter, namespaceFilter }: LoaderProps) { + // If there are filters, we don't want to populate the cache to avoid performance issues + // Filtering may trigger multiple HTTP requests, which would populate the cache with a lot of groups hurting performance + const hasFilters = Boolean(groupFilter || namespaceFilter); const { uid, name } = rulesSourceIdentifier; - const prometheusGroupsGenerator = usePrometheusGroupsGenerator({ populateCache: true }); + const prometheusGroupsGenerator = usePrometheusGroupsGenerator({ + populateCache: hasFilters ? false : true, + }); + + // If there are no filters we can match one frontend page to one API page. + // However, if there are filters, we need to fetch more groups from the API to populate one frontend page + const apiGroupPageSize = getApiGroupPageSize(hasFilters); const groupsGenerator = useRef( - toIndividualRuleGroups(prometheusGroupsGenerator(rulesSourceIdentifier, DATA_SOURCE_GROUP_PAGE_SIZE)) + toIndividualRuleGroups(prometheusGroupsGenerator(rulesSourceIdentifier, apiGroupPageSize)) ); useEffect(() => { @@ -39,11 +69,21 @@ export function PaginatedDataSourceLoader({ rulesSourceIdentifier, application } return () => { currentGenerator.return(); }; - }, [groupsGenerator]); + }, []); + + const filterFn = useMemo( + () => (group: PromRuleGroupDTO) => + groupFilterFn(group, { + namespace: namespaceFilter, + groupName: groupFilter, + }), + [namespaceFilter, groupFilter] + ); const { isLoading, groups, hasMoreGroups, fetchMoreGroups, error } = useLazyLoadPrometheusGroups( groupsGenerator.current, - DATA_SOURCE_GROUP_PAGE_SIZE + FRONTED_GROUPED_PAGE_SIZE, + filterFn ); const hasNoRules = isEmpty(groups) && !isLoading; @@ -86,13 +126,7 @@ export function PaginatedDataSourceLoader({ rulesSourceIdentifier, application } Loading more groups... )} - {hasNoRules && ( -
- - No rules found - -
- )} + {hasNoRules && }
); @@ -127,9 +161,3 @@ function RuleGroupListItem({ rulesSourceIdentifier, group, namespaceName }: Rule ); } - -const getStyles = (theme: GrafanaTheme2) => ({ - noRules: css({ - margin: theme.spacing(1.5, 0, 0.5, 4), - }), -}); diff --git a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx index c2ef3fa2863..d3f1c62d43a 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef } from 'react'; import { Trans } from '@grafana/i18n'; import { Icon, Spinner, Stack, Text } from '@grafana/ui'; import { GrafanaRuleGroupIdentifier, GrafanaRulesSourceSymbol } from 'app/types/unified-alerting'; -import { GrafanaPromRuleGroupDTO } from 'app/types/unified-alerting-dto'; +import { GrafanaPromRuleGroupDTO, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { FolderActionsButton } from '../components/folder-actions/FolderActionsButton'; import { GrafanaNoRulesCTA } from '../components/rules/NoRulesCTA'; @@ -17,15 +17,39 @@ import { GroupIntervalIndicator } from './components/GroupIntervalMetadata'; import { ListGroup } from './components/ListGroup'; import { ListSection } from './components/ListSection'; import { LoadMoreButton } from './components/LoadMoreButton'; +import { NoRulesFound } from './components/NoRulesFound'; +import { groupFilter as groupFilterFn } from './hooks/filters'; import { toIndividualRuleGroups, useGrafanaGroupsGenerator } from './hooks/prometheusGroupsGenerator'; import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups'; +import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits'; -export const GRAFANA_GROUP_PAGE_SIZE = 40; +interface LoaderProps { + groupFilter?: string; + namespaceFilter?: string; +} -export function PaginatedGrafanaLoader() { - const grafanaGroupsGenerator = useGrafanaGroupsGenerator({ populateCache: true, limitAlerts: 0 }); +export function PaginatedGrafanaLoader({ groupFilter, namespaceFilter }: LoaderProps) { + const key = `${groupFilter}-${namespaceFilter}`; - const groupsGenerator = useRef(toIndividualRuleGroups(grafanaGroupsGenerator(GRAFANA_GROUP_PAGE_SIZE))); + // Key is crucial. It resets the generator when filters change. + return ; +} + +function PaginatedGroupsLoader({ groupFilter, namespaceFilter }: LoaderProps) { + // If there are filters, we don't want to populate the cache to avoid performance issues + // Filtering may trigger multiple HTTP requests, which would populate the cache with a lot of groups hurting performance + const hasFilters = Boolean(groupFilter || namespaceFilter); + + const grafanaGroupsGenerator = useGrafanaGroupsGenerator({ + populateCache: hasFilters ? false : true, + limitAlerts: 0, + }); + + // If there are no filters we can match one frontend page to one API page. + // However, if there are filters, we need to fetch more groups from the API to populate one frontend page + const apiGroupPageSize = getApiGroupPageSize(hasFilters); + + const groupsGenerator = useRef(toIndividualRuleGroups(grafanaGroupsGenerator(apiGroupPageSize))); useEffect(() => { const currentGenerator = groupsGenerator.current; @@ -34,9 +58,19 @@ export function PaginatedGrafanaLoader() { }; }, []); + const filterFn = useMemo( + () => (group: PromRuleGroupDTO) => + groupFilterFn(group, { + namespace: namespaceFilter, + groupName: groupFilter, + }), + [namespaceFilter, groupFilter] + ); + const { isLoading, groups, hasMoreGroups, fetchMoreGroups, error } = useLazyLoadPrometheusGroups( groupsGenerator.current, - GRAFANA_GROUP_PAGE_SIZE + FRONTED_GROUPED_PAGE_SIZE, + filterFn ); const groupsByFolder = useMemo(() => groupBy(groups, 'folderUid'), [groups]); @@ -78,7 +112,9 @@ export function PaginatedGrafanaLoader() { ); })} - {hasNoRules && } + {/* only show the CTA if the user has no rules and this isn't the result of a filter / search query */} + {hasNoRules && !hasFilters && } + {hasNoRules && hasFilters && } {hasMoreGroups && ( // this div will make the button not stretch
diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx index ae5e0e479e4..e29d4c2f65b 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx @@ -7,6 +7,8 @@ import { AccessControlAction } from 'app/types'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions } from '../mocks'; import { alertingFactory } from '../mocks/server/db'; +import { RulesFilter } from '../search/rulesSearchParser'; +import { testWithFeatureToggles } from '../test/test-utils'; import RuleList, { RuleListActions } from './RuleList.v2'; @@ -23,12 +25,18 @@ jest.mock('./GroupedView', () => ({ const ui = { filterView: byTestId('filter-view'), groupedView: byTestId('grouped-view'), + modeSelector: { + grouped: byRole('radio', { name: /grouped/i }), + list: byRole('radio', { name: /list/i }), + }, + searchInput: byTestId('search-query-input'), }; setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); grantUserPermissions([AccessControlAction.AlertingRuleExternalRead]); +testWithFeatureToggles(['alertingListViewV2']); setupMswServer(); @@ -61,8 +69,84 @@ describe('RuleList v2', () => { expect(ui.groupedView.query()).not.toBeInTheDocument(); }); - it('should show list view when a filter is applied', () => { - render(, { historyOptions: { initialEntries: ['/?search=rule:cpu-alert'] } }); + it('should show grouped view when only group filter is applied', () => { + render(, { historyOptions: { initialEntries: ['/?search=group:cpu-usage'] } }); + + expect(ui.groupedView.get()).toBeInTheDocument(); + expect(ui.filterView.query()).not.toBeInTheDocument(); + }); + + it('should show grouped view when only namespace filter is applied', () => { + render(, { historyOptions: { initialEntries: ['/?search=namespace:global'] } }); + + expect(ui.groupedView.get()).toBeInTheDocument(); + expect(ui.filterView.query()).not.toBeInTheDocument(); + }); + + it('should show grouped view when both group and namespace filters are applied', () => { + render(, { historyOptions: { initialEntries: ['/?search=group:cpu-usage namespace:global'] } }); + + expect(ui.groupedView.get()).toBeInTheDocument(); + expect(ui.filterView.query()).not.toBeInTheDocument(); + }); + + it('should show list view when group and namespace filters are combined with other filter types', () => { + render(, { + historyOptions: { initialEntries: ['/?search=group:cpu-usage namespace:global state:firing'] }, + }); + + expect(ui.filterView.get()).toBeInTheDocument(); + expect(ui.groupedView.query()).not.toBeInTheDocument(); + }); + + it('should show grouped view when view parameter is empty', () => { + render(, { historyOptions: { initialEntries: ['/?view='] } }); + + expect(ui.groupedView.get()).toBeInTheDocument(); + expect(ui.filterView.query()).not.toBeInTheDocument(); + }); + + it('should show grouped view when search parameter is empty', () => { + render(, { historyOptions: { initialEntries: ['/?search='] } }); + + expect(ui.groupedView.get()).toBeInTheDocument(); + expect(ui.filterView.query()).not.toBeInTheDocument(); + }); + + it.each<{ filterType: keyof RulesFilter; searchQuery: string }>([ + { filterType: 'freeFormWords', searchQuery: 'cpu alert' }, + { filterType: 'ruleName', searchQuery: 'rule:"cpu 80%"' }, + { filterType: 'ruleState', searchQuery: 'state:firing' }, + { filterType: 'ruleType', searchQuery: 'type:alerting' }, + { filterType: 'dataSourceNames', searchQuery: 'datasource:prometheus' }, + { filterType: 'labels', searchQuery: 'label:team=backend' }, + { filterType: 'ruleHealth', searchQuery: 'health:error' }, + { filterType: 'contactPoint', searchQuery: 'contactPoint:slack' }, + ])('should show list view when %s filter is applied', ({ filterType, searchQuery }) => { + render(, { historyOptions: { initialEntries: [`/?search=${encodeURIComponent(searchQuery)}`] } }); + + expect(ui.filterView.get()).toBeInTheDocument(); + expect(ui.groupedView.query()).not.toBeInTheDocument(); + }); + + it('should show list view when "view=list" URL parameter is present with group filter', () => { + render(, { historyOptions: { initialEntries: ['/?view=list&search=group:cpu-usage'] } }); + + expect(ui.filterView.get()).toBeInTheDocument(); + expect(ui.groupedView.query()).not.toBeInTheDocument(); + }); + + it('should show list view when "view=list" URL parameter is present with namespace filter', () => { + render(, { historyOptions: { initialEntries: ['/?view=list&search=namespace:global'] } }); + + expect(ui.filterView.get()).toBeInTheDocument(); + expect(ui.groupedView.query()).not.toBeInTheDocument(); + }); + + it('should show list view when "view=list" URL parameter is present with both group and namespace filters', () => { + render(, { + historyOptions: { initialEntries: ['/?view=list&search=group:cpu-usage namespace:global'] }, + }); expect(ui.filterView.get()).toBeInTheDocument(); expect(ui.groupedView.query()).not.toBeInTheDocument(); @@ -160,3 +244,47 @@ describe('RuleListActions', () => { expect(ui.menuOptions.newDataSourceRecordingRule.query(menu)).toBeInTheDocument(); }); }); + +describe('RuleList v2 - View switching', () => { + it('should preserve both group and namespace filters when switching from list view to grouped view', async () => { + // Start with list view and both group and namespace filters + const { user } = render(, { + historyOptions: { initialEntries: ['/?view=list&search=group:cpu-usage namespace:global'] }, + }); + expect(ui.filterView.get()).toBeInTheDocument(); + + // Click the "Grouped" view button + const groupedButton = await ui.modeSelector.grouped.find(); + await user.click(groupedButton); + + // Should preserve both filters and switch to grouped view + expect(ui.groupedView.get()).toBeInTheDocument(); + expect(ui.filterView.query()).not.toBeInTheDocument(); + + // Verify filters are preserved + expect(ui.searchInput.get()).toHaveValue('group:cpu-usage namespace:global'); + expect(ui.modeSelector.list.query()).not.toBeChecked(); + }); + + it('should clear all filters when switching from list view to grouped view with group, namespace and other filters', async () => { + // Start with list view with all types of filters + const { user } = render(, { + historyOptions: { + initialEntries: ['/?view=list&search=group:cpu-usage namespace:global state:firing rule:"test"'], + }, + }); + expect(ui.filterView.get()).toBeInTheDocument(); + + // Click the "Grouped" view button + const groupedButton = await ui.modeSelector.grouped.find(); + await user.click(groupedButton); + + // Should clear all filters because other filters are present + expect(ui.groupedView.get()).toBeInTheDocument(); + expect(ui.filterView.query()).not.toBeInTheDocument(); + + // Verify all filters are cleared + expect(ui.searchInput.get()).toHaveValue(''); + expect(ui.modeSelector.list.query()).not.toBeChecked(); + }); +}); diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx index 124e230aeef..f3c79b42b4b 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx @@ -6,10 +6,9 @@ import { Button, Dropdown, Icon, LinkButton, Menu, Stack } from '@grafana/ui'; import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; import RulesFilter from '../components/rules/Filter/RulesFilter'; -import { SupportedView } from '../components/rules/Filter/RulesViewModeSelector'; +import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelector'; import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities'; import { useRulesFilter } from '../hooks/useFilteredRules'; -import { useURLSearchParams } from '../hooks/useURLSearchParams'; import { isAdmin } from '../utils/misc'; import { FilterView } from './FilterView'; @@ -17,16 +16,17 @@ import { GroupedView } from './GroupedView'; import { RuleListPageTitle } from './RuleListPageTitle'; function RuleList() { - const [queryParams] = useURLSearchParams(); - const { filterState, hasActiveFilters } = useRulesFilter(); - - const view: SupportedView = queryParams.get('view') === 'list' ? 'list' : 'grouped'; - const showListView = hasActiveFilters || view === 'list'; + const { filterState } = useRulesFilter(); + const { viewMode, handleViewChange } = useListViewMode(); return ( <> - {}} /> - {showListView ? : } + + {viewMode === 'list' ? ( + + ) : ( + + )} ); } diff --git a/public/app/features/alerting/unified/rule-list/components/NoRulesFound.tsx b/public/app/features/alerting/unified/rule-list/components/NoRulesFound.tsx new file mode 100644 index 00000000000..f8778712dd9 --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/components/NoRulesFound.tsx @@ -0,0 +1,24 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Trans } from '@grafana/i18n'; +import { Text, useStyles2 } from '@grafana/ui'; + +// @TODO I don't like applying the margins to this component here, ideally the parent component should be layouting this. +export const NoRulesFound = () => { + const styles = useStyles2(getStyles); + + return ( +
+ + No rules found + +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + noRules: css({ + margin: theme.spacing(1.5, 0, 0.5, 4), + }), +}); diff --git a/public/app/features/alerting/unified/rule-list/hooks/filters.ts b/public/app/features/alerting/unified/rule-list/hooks/filters.ts index e66ddb559a9..975216e498d 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/filters.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/filters.ts @@ -14,16 +14,20 @@ import { isPluginProvidedRule, prometheusRuleType } from '../../utils/rules'; /** * @returns True if the group matches the filter, false otherwise. Keeps rules intact */ -export function groupFilter(group: PromRuleGroupDTO, filterState: RulesFilter): boolean { +export function groupFilter( + group: PromRuleGroupDTO, + filterState: Pick +): boolean { const { name, file } = group; + const { namespace, groupName } = filterState; // Add fuzzy search for namespace - if (filterState.namespace && !file.toLowerCase().includes(filterState.namespace)) { + if (namespace && !file.toLocaleLowerCase().includes(namespace.toLocaleLowerCase())) { return false; } // Add fuzzy search for group name - if (filterState.groupName && !name.toLowerCase().includes(filterState.groupName)) { + if (groupName && !name.toLocaleLowerCase().includes(groupName.toLocaleLowerCase())) { return false; } diff --git a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts index 5165996b05c..649594f893e 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts @@ -10,6 +10,10 @@ import { PromRulesResponse, prometheusApi } from '../../api/prometheusApi'; const { useLazyGetGroupsQuery, useLazyGetGrafanaGroupsQuery } = prometheusApi; interface UseGeneratorHookOptions { + /** + * Whether to populate the RTKQ cache with the groups. + * Populating cache might harm performance when fetching a lot of groups or fetching multiple pages + */ populateCache?: boolean; limitAlerts?: number; } diff --git a/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx b/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx index df24ed95571..5181e1a1cd9 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx +++ b/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx @@ -16,7 +16,8 @@ import { isLoading as isLoadingState, useAsync } from '../../hooks/useAsync'; */ export function useLazyLoadPrometheusGroups( groupsGenerator: AsyncIterator, - pageSize: number + pageSize: number, + filter?: (group: TGroup) => boolean ) { const [groups, setGroups] = useState([]); const [hasMoreGroups, setHasMoreGroups] = useState(true); @@ -31,7 +32,12 @@ export function useLazyLoadPrometheusGroups( done = true; break; } + const group = generatorResult.value; + if (filter && !filter(group)) { + continue; + } + currentGroups.push(group); } diff --git a/public/app/features/alerting/unified/rule-list/paginationLimits.ts b/public/app/features/alerting/unified/rule-list/paginationLimits.ts new file mode 100644 index 00000000000..41c5293cbde --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/paginationLimits.ts @@ -0,0 +1,9 @@ +export const FRONTEND_LIST_PAGE_SIZE = 100; + +export const FILTERED_GROUPS_API_PAGE_SIZE = 2000; +export const DEFAULT_GROUPS_API_PAGE_SIZE = 40; +export const FRONTED_GROUPED_PAGE_SIZE = DEFAULT_GROUPS_API_PAGE_SIZE; + +export function getApiGroupPageSize(hasFilters: boolean) { + return hasFilters ? FILTERED_GROUPS_API_PAGE_SIZE : DEFAULT_GROUPS_API_PAGE_SIZE; +} diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx index 5db18bd4553..a34fc45f0d6 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx @@ -3,6 +3,7 @@ import { capitalize } from 'lodash'; import React, { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { useTranslate } from '@grafana/i18n'; import { t } from '@grafana/i18n/internal'; import { Button, Combobox, ComboboxOption, Field, InlineSwitch, Input, Stack, useStyles2 } from '@grafana/ui'; @@ -104,9 +105,16 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan className={styles.wideSelector} > {isStandardMinWidth ? ( - + ) : ( setInputRef(ref)} @@ -114,6 +122,7 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan min={50} max={2000} invalid={customMinWidthError} + data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth} suffix={ @@ -130,10 +140,12 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan layoutManager.onMaxColumnCountChanged(parseInt(value, 10))} width={6.5} + data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns} /> @@ -210,9 +222,16 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage className={styles.wideSelector} > {isStandardHeight ? ( - + ) : ( setInputRef(ref)} @@ -220,6 +239,7 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage min={50} max={2000} invalid={customMinWidthError} + data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight} suffix={ @@ -235,7 +256,12 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage )} - layoutManager.onFillScreenChanged(!fillScreen)} /> + layoutManager.onFillScreenChanged(!fillScreen)} + data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen} + /> ); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx index a44652a0fe3..fae5d4a5991 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx @@ -74,6 +74,25 @@ export function ResourceExport({ /> )} + {initialSaveModelVersion === 'v2' && ( + + + onExportModeChange(value)} + /> + + )} {exportMode !== ExportMode.Classic && ( @@ -87,7 +106,9 @@ export function ResourceExport({ /> )} - {(isV2Dashboard || exportMode === ExportMode.Classic) && ( + {(isV2Dashboard || + exportMode === ExportMode.Classic || + (initialSaveModelVersion === 'v2' && exportMode === ExportMode.V1Resource)) && ( diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx new file mode 100644 index 00000000000..fb416a280fb --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx @@ -0,0 +1,333 @@ +import { config } from '@grafana/runtime'; +import { SceneTimeRange } from '@grafana/scenes'; +import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import * as ResponseTransformers from 'app/features/dashboard/api/ResponseTransformers'; +import { DashboardJson } from 'app/features/manage-dashboards/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; + +import { DashboardScene } from '../scene/DashboardScene'; +import * as exporters from '../scene/export/exporters'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; +import * as sceneToV1 from '../serialization/transformSceneToSaveModel'; +import * as sceneToV2 from '../serialization/transformSceneToSaveModelSchemaV2'; + +import { ExportMode } from './ExportButton/ResourceExport'; +import { ShareExportTab } from './ShareExportTab'; + +describe('ShareExportTab', () => { + // Spies to track function calls + let transformV2ToV1Spy: jest.SpyInstance; + let makeExportableV1Spy: jest.SpyInstance; + let transformSceneToV1Spy: jest.SpyInstance; + let transformSceneToV2Spy: jest.SpyInstance; + + beforeEach(() => { + config.featureToggles.kubernetesDashboards = true; + + // Set up spies on the functions we want to track + transformV2ToV1Spy = jest.spyOn(ResponseTransformers, 'transformDashboardV2SpecToV1').mockReturnValue({ + title: 'Transformed V1', + uid: 'transformed-uid', + version: 1, + panels: [], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + timezone: '', + weekStart: '', + fiscalYearStartMonth: 0, + refresh: '', + schemaVersion: 30, + tags: [], + templating: { list: [] }, + } as DashboardDataDTO); + + makeExportableV1Spy = jest.spyOn(exporters, 'makeExportableV1').mockImplementation(async (dashboard) => dashboard); + + transformSceneToV1Spy = jest.spyOn(sceneToV1, 'transformSceneToSaveModel').mockReturnValue({ + title: 'Scene V1', + uid: 'scene-v1-uid', + version: 1, + panels: [], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + timezone: '', + weekStart: '', + fiscalYearStartMonth: 0, + refresh: '', + schemaVersion: 30, + tags: [], + templating: { list: [] }, + } as Dashboard); + + transformSceneToV2Spy = jest.spyOn(sceneToV2, 'transformSceneToSaveModelSchemaV2').mockReturnValue({ + title: 'Scene V2', + annotations: [], + cursorSync: 'Off', + description: '', + editable: true, + elements: {}, + layout: { kind: 'GridLayout', spec: { items: [] } }, + links: [], + liveNow: false, + preload: false, + tags: [], + timeSettings: { + from: 'now-6h', + to: 'now', + autoRefresh: '', + autoRefreshIntervals: [], + hideTimepicker: false, + timezone: '', + weekStart: 'saturday', + fiscalYearStartMonth: 0, + }, + variables: [], + } as DashboardV2Spec); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('V1Resource export mode', () => { + // If V1 dashboard → V1 Resource should export with V1 apiVersion + it('should export V1 dashboard as V1 resource with correct apiVersion', async () => { + const tab = buildV1DashboardScenario(); + tab.setState({ exportMode: ExportMode.V1Resource }); + + const result = await tab.getExportableDashboardJson(); + + // Should use V1 API version + expect(result.json).toMatchObject({ + apiVersion: 'dashboard.grafana.app/v1beta1', + kind: 'Dashboard', + status: {}, + }); + + // Should call transformSceneToV1 (not transform V2→V1) + expect(transformSceneToV1Spy).toHaveBeenCalled(); + expect(transformV2ToV1Spy).not.toHaveBeenCalled(); + + // Should report correct initial version + expect(result.initialSaveModelVersion).toBe('v1'); + }); + + // If V2 dashboard → V1 Resource should auto-transform with V1 apiVersion + it('should auto-transform V2 dashboard to V1 resource with correct apiVersion', async () => { + const tab = buildV2DashboardScenario(); + // user selects V1Resource even though is V2 dashboard + tab.setState({ exportMode: ExportMode.V1Resource }); + + const result = await tab.getExportableDashboardJson(); + + // Should use V1 API version (not V2!) + expect(result.json).toMatchObject({ + apiVersion: 'dashboard.grafana.app/v1beta1', + kind: 'Dashboard', + status: {}, + }); + + // Should auto-transform V2→V1 + expect(transformSceneToV2Spy).toHaveBeenCalled(); // Get V2 spec first + expect(transformV2ToV1Spy).toHaveBeenCalled(); // Then transform to V1 + + // Should report correct initial version + expect(result.initialSaveModelVersion).toBe('v2'); + }); + + // If V2 dashboard → V1 Resource with external sharing should transform and apply external sharing + it('should handle external sharing when transforming V2 to V1', async () => { + const tab = buildV2DashboardScenario(); + tab.setState({ + exportMode: ExportMode.V1Resource, + isSharingExternally: true, + }); + + const result = await tab.getExportableDashboardJson(); + + // Should use V1 API version + expect(result.json).toMatchObject({ + apiVersion: 'dashboard.grafana.app/v1beta1', + kind: 'Dashboard', + status: {}, + }); + + // Should auto-transform V2→V1 + expect(transformSceneToV2Spy).toHaveBeenCalled(); + expect(transformV2ToV1Spy).toHaveBeenCalled(); + + // Should call makeExportableV1 for external sharing + expect(makeExportableV1Spy).toHaveBeenCalled(); + + // Should report correct initial version + expect(result.initialSaveModelVersion).toBe('v2'); + }); + }); + + describe('V2Resource export mode', () => { + // If V2 dashboard → V2 Resource should export with V2 apiVersion + it('should export V2 dashboard as V2 resource with correct apiVersion', async () => { + const tab = buildV2DashboardScenario(); + tab.setState({ exportMode: ExportMode.V2Resource }); + + const result = await tab.getExportableDashboardJson(); + + // Should use V2 API version + expect(result.json).toMatchObject({ + apiVersion: 'dashboard.grafana.app/v2alpha1', + kind: 'Dashboard', + status: {}, + }); + + // Should not call V2→V1 transformation since source is already V2 + expect(transformV2ToV1Spy).not.toHaveBeenCalled(); + + // Should report correct initial version + expect(result.initialSaveModelVersion).toBe('v2'); + }); + }); + + describe('Classic export mode', () => { + // If V1 dashboard → Classic should export plain dashboard JSON + it('should export V1 dashboard in classic format', async () => { + const tab = buildV1DashboardScenario(); + tab.setState({ exportMode: ExportMode.Classic }); + + const result = await tab.getExportableDashboardJson(); + + // Should return plain dashboard JSON (not wrapped in resource) + expect(result.json).toMatchObject({ + title: 'Test Dashboard V1', + uid: 'test-uid-v1', + panels: expect.any(Array), + }); + + // Should NOT have resource wrapper properties + expect(result.json).not.toHaveProperty('apiVersion'); + expect(result.json).not.toHaveProperty('kind'); + expect(result.json).not.toHaveProperty('status'); + + // Should report correct initial version + expect(result.initialSaveModelVersion).toBe('v1'); + }); + }); + + describe('Export mode state management', () => { + // If switching to Classic mode should disable YAML viewing + it('should disable YAML viewing when switching to Classic mode', async () => { + const tab = buildV1DashboardScenario(); + + // Start with YAML viewing enabled + tab.setState({ isViewingYAML: true }); + expect(tab.state.isViewingYAML).toBe(true); + + // Switch to Classic mode + tab.onExportModeChange(ExportMode.Classic); + + // Should disable YAML viewing + expect(tab.state.isViewingYAML).toBe(false); + }); + + // If switching to resource modes should preserve YAML viewing + it('should preserve YAML viewing when switching to resource modes', async () => { + const tab = buildV2DashboardScenario(); + + // Start with YAML viewing enabled + tab.setState({ isViewingYAML: true }); + expect(tab.state.isViewingYAML).toBe(true); + + // Switch to V1Resource mode + tab.onExportModeChange(ExportMode.V1Resource); + expect(tab.state.isViewingYAML).toBe(true); // Should preserve + + // Switch to V2Resource mode + tab.onExportModeChange(ExportMode.V2Resource); + expect(tab.state.isViewingYAML).toBe(true); // Should preserve + }); + }); + + // Helper functions to create test scenarios + function buildV1DashboardScenario(): ShareExportTab { + const mockV1Dashboard: DashboardDataDTO = { + title: 'Test Dashboard V1', + uid: 'test-uid-v1', + version: 1, + panels: [], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + timezone: '', + weekStart: '', + fiscalYearStartMonth: 0, + refresh: '', + schemaVersion: 30, + tags: [], + templating: { list: [] }, + }; + + const tab = new ShareExportTab({}); + const scene = new DashboardScene({ + title: 'Test Dashboard V1', + uid: 'test-uid-v1', + meta: { canEdit: true }, + $timeRange: new SceneTimeRange({}), + body: DefaultGridLayoutManager.fromVizPanels([]), + overlay: tab, + }); + + const mockExportableDashboard: DashboardJson = { + ...mockV1Dashboard, + panels: [], + } as DashboardJson; + scene.serializer.getSaveModel = jest.fn(() => mockV1Dashboard); + scene.serializer.makeExportableExternally = jest.fn(() => Promise.resolve(mockExportableDashboard)); + scene.serializer.apiVersion = 'dashboard.grafana.app/v1beta1'; + scene.getInitialSaveModel = jest.fn(() => mockV1Dashboard); + + return tab; + } + + function buildV2DashboardScenario(): ShareExportTab { + const mockV2Dashboard: DashboardV2Spec = { + title: 'Test Dashboard V2', + annotations: [], + cursorSync: 'Off', + description: 'Test V2 dashboard', + editable: true, + elements: {}, + layout: { kind: 'GridLayout', spec: { items: [] } }, + links: [], + liveNow: false, + preload: false, + tags: [], + timeSettings: { + from: 'now-6h', + to: 'now', + autoRefresh: '', + autoRefreshIntervals: [], + hideTimepicker: false, + timezone: '', + weekStart: 'saturday', + fiscalYearStartMonth: 0, + }, + variables: [], + }; + + const tab = new ShareExportTab({}); + const scene = new DashboardScene({ + title: 'Test Dashboard V2', + uid: 'test-uid-v2', + meta: { canEdit: true }, + $timeRange: new SceneTimeRange({}), + body: DefaultGridLayoutManager.fromVizPanels([]), + overlay: tab, + }); + + scene.serializer.getSaveModel = jest.fn(() => mockV2Dashboard); + scene.serializer.makeExportableExternally = jest.fn(() => Promise.resolve(mockV2Dashboard)); + scene.serializer.apiVersion = 'dashboard.grafana.app/v2alpha1'; + scene.getInitialSaveModel = jest.fn(() => mockV2Dashboard); + + return tab; + } +}); diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index c6878c3fcd4..4cb3d0951ea 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -12,12 +12,15 @@ import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch } from '@grafana/ui'; import { ObjectMeta } from 'app/features/apiserver/types'; +import { transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; +import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1'; import { K8S_V2_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v2'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardJson } from 'app/features/manage-dashboards/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; import { makeExportableV1, makeExportableV2 } from '../scene/export/exporters'; @@ -34,7 +37,7 @@ export interface ExportableResource { apiVersion: string; kind: 'Dashboard'; metadata: DashboardWithAccessInfo['metadata'] | Partial; - spec: Dashboard | DashboardModel | DashboardV2Spec | { error: unknown }; + spec: Dashboard | DashboardModel | DashboardV2Spec | DashboardJson | DashboardDataDTO | { error: unknown }; // A placeholder for now because as code tooling expects it status: {}; } @@ -112,7 +115,12 @@ export class ShareExportTab extends SceneObjectBase impleme const exportable = isSharingExternally ? exportableDashboard : origDashboard; const metadata = getMetadata(scene, Boolean(isSharingExternally)); - if (isDashboardV2Spec(origDashboard) && 'elements' in exportable && initialSaveModelVersion === 'v2') { + if ( + isDashboardV2Spec(origDashboard) && + 'elements' in exportable && + initialSaveModelVersion === 'v2' && + exportMode !== ExportMode.V1Resource + ) { this.setState({ exportMode: ExportMode.V2Resource, }); @@ -131,19 +139,66 @@ export class ShareExportTab extends SceneObjectBase impleme } if (exportMode === ExportMode.V1Resource) { - const spec = transformSceneToSaveModel(scene); + // Check if source is V2 and auto-transform to V1 + if (isDashboardV2Spec(origDashboard) && initialSaveModelVersion === 'v2') { + try { + const spec = transformSceneToSaveModelSchemaV2(scene); + const metadata = getMetadata(scene, Boolean(isSharingExternally)); + const spec1 = transformDashboardV2SpecToV1(spec, { + name: metadata.name ?? '', + generation: metadata.generation ?? 0, + resourceVersion: metadata.resourceVersion ?? '0', + creationTimestamp: metadata.creationTimestamp ?? '', + }); - return { - json: { - apiVersion: scene.serializer.apiVersion ?? '', - kind: 'Dashboard', - metadata, - spec, - status: {}, - }, - initialSaveModelVersion, - hasLibraryPanels: undefined, - }; + let exportableV1: Dashboard | DashboardDataDTO | DashboardJson | { error: unknown }; + if (isSharingExternally) { + const oldModel = new DashboardModel(spec1, undefined, { + getVariablesFromState: () => { + return getVariablesCompatibility(window.__grafanaSceneContext); + }, + }); + exportableV1 = await makeExportableV1(oldModel); + } else { + exportableV1 = spec1; + } + return { + json: { + // Forcing V1 version here to match export mode selection + apiVersion: `${K8S_V1_DASHBOARD_API_CONFIG.group}/${K8S_V1_DASHBOARD_API_CONFIG.version}`, + kind: 'Dashboard', + metadata, + spec: exportableV1, + status: {}, + }, + initialSaveModelVersion, + hasLibraryPanels: undefined, + }; + } catch (err) { + return { + json: { + error: `Failed to convert dashboard to v1. ${err}`, + }, + initialSaveModelVersion, + hasLibraryPanels: undefined, + }; + } + } else { + // Source is already V1, export as-is + const spec = transformSceneToSaveModel(scene); + return { + json: { + // Forcing V1 version here to match export mode selection + apiVersion: `${K8S_V1_DASHBOARD_API_CONFIG.group}/${K8S_V1_DASHBOARD_API_CONFIG.version}`, + kind: 'Dashboard', + metadata, + spec, + status: {}, + }, + initialSaveModelVersion, + hasLibraryPanels: undefined, + }; + } } if (exportMode === ExportMode.V2Resource) { diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx index 51d805667bf..b852f1520cd 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx @@ -273,6 +273,48 @@ describe('LokiQueryBuilderOptions', () => { }); }); }); + + describe('Step validation', () => { + it('considers empty step as valid', async () => { + setup({ expr: 'rate({foo="bar"}[5m]' }); + await userEvent.click(screen.getByRole('button', { name: /Options/ })); + expect(screen.queryByText(/Invalid step/)).not.toBeInTheDocument(); + }); + + it('considers variable step that exists in the datasource as valid', async () => { + const datasource = createLokiDatasource(); + datasource.getVariables = jest.fn().mockReturnValue(['$interval']); + setup({ expr: 'rate({foo="bar"}[5m]', step: '$interval' }, undefined, { datasource }); + await userEvent.click(screen.getByRole('button', { name: /Options/ })); + expect(screen.queryByText(/Invalid step/)).not.toBeInTheDocument(); + }); + + it('considers variable step that does not exist in the datasource as invalid', async () => { + const datasource = createLokiDatasource(); + datasource.getVariables = jest.fn().mockReturnValue(['$interval']); + setup({ expr: 'rate({foo="bar"}[5m]', step: '$custom' }, undefined, { datasource }); + await userEvent.click(screen.getByRole('button', { name: /Options/ })); + expect(screen.getByText(/Invalid step/)).toBeInTheDocument(); + }); + + it('considers valid duration step as valid', async () => { + setup({ expr: 'rate({foo="bar"}[5m]', step: '1m' }); + await userEvent.click(screen.getByRole('button', { name: /Options/ })); + expect(screen.queryByText(/Invalid step/)).not.toBeInTheDocument(); + }); + + it('considers invalid step as invalid', async () => { + setup({ expr: 'rate({foo="bar"}[5m]', step: 'invalid' }); + await userEvent.click(screen.getByRole('button', { name: /Options/ })); + expect(screen.getByText(/Invalid step/)).toBeInTheDocument(); + }); + + it('considers non-duration number as invalid', async () => { + setup({ expr: 'rate({foo="bar"}[5m]', step: '123' }); + await userEvent.click(screen.getByRole('button', { name: /Options/ })); + expect(screen.getByText(/Invalid step/)).toBeInTheDocument(); + }); + }); }); function setup(queryOverrides: Partial = {}, onChange = jest.fn(), propOverrides: Partial = {}) { diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx index f692bf9496a..d04e9fc42f6 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx @@ -138,8 +138,18 @@ export const LokiQueryBuilderOptions = React.memo( if (!query.step) { return true; } - return typeof query.step === 'string' && isValidGrafanaDuration(query.step) && !isNaN(parseInt(query.step, 10)); - }, [query.step]); + + if (typeof query.step === 'string') { + // If we use a variable as step, we consider it valid + if (datasource.getVariables().includes(query.step)) { + return true; + } + // Check if the step is a valid Grafana duration + return isValidGrafanaDuration(query.step) && !isNaN(parseInt(query.step, 10)); + } + + return false; + }, [query.step, datasource]); return ( diff --git a/public/app/plugins/datasource/prometheus/configuration/DataSourceHttpSettingsOverhaulPackage.tsx b/public/app/plugins/datasource/prometheus/configuration/DataSourceHttpSettingsOverhaulPackage.tsx index f427d8c2181..098580399e9 100644 --- a/public/app/plugins/datasource/prometheus/configuration/DataSourceHttpSettingsOverhaulPackage.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/DataSourceHttpSettingsOverhaulPackage.tsx @@ -141,6 +141,12 @@ export const DataSourcehttpSettingsOverhaul = (props: Props) => { Service for Prometheus data source to authenticate with SigV4. )} + {azureAuthSelected && ( + + Azure authentication in the core Prometheus data source is deprecated. Please use the Azure Monitor Managed + Service for Prometheus data source to authenticate using Azure authentication. + + )} + Please note that TraceQL metrics is an experimental feature and should not be used in production. Read more about + it in{' '} + + documentation + + + . + + ); + const inAlerting = props.app === CoreApp.UnifiedAlerting || props.app === CoreApp.CloudAlerting; + return ( <> + {inAlerting && alertingWarning} Build complex queries using TraceQL to select a list of traces.{' '} diff --git a/public/app/plugins/panel/geomap/editor/layerEditor.tsx b/public/app/plugins/panel/geomap/editor/layerEditor.tsx index 064e6189cf7..865b9f94654 100644 --- a/public/app/plugins/panel/geomap/editor/layerEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/layerEditor.tsx @@ -122,7 +122,7 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptionsživotního cyklu funkce.", "schema-v2-library-panels-export-title": "Schéma nástěnky V2 zatím nepodporuje export panelů knihoven pro použití v jiné instanci", - "title-dashboard-drastically-changed": "Nástěnka se podstatně změnila" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Nastaví aktuální obnovení jako nové výchozí", @@ -7767,6 +7768,8 @@ }, "enable-highlighting": "Povolit zvýraznění", "escape-newlines": "Opravit nesprávně uniklé sekvence nového řádku a záložek v řádcích protokolu", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Skrýt časová razítka", "hide-unique-labels": "Skrýt jedinečné štítky", "newest-first": "Seřazeno od nejnovějších protokolů – kliknutím zobrazíte nejstarší protokoly jako první", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 41293652554..369650ceb84 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Auswählen", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "Nachricht", - "body-dashboard-drastically-changed": "Da Sie neue Dashboard-Funktionen nutzen, die nur im neuen Dashboard-Schema-Format von Grafana unterstützt werden, wird das Dashboard im neuen Format gespeichert. Bitte prüfen Sie, ob Sie diese Aktion wirklich ausführen möchten, oder ob Sie das Dashboard lieber als neue Kopie speichern möchten.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Abbrechen" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "Die dynamische Dashboard-Funktionalität ist experimentell und weist keine vollständige Funktionsparität mit dem aktuellen Dashboard-Verhalten auf. Sie basiert auf einem neuen Schemaformat, das keine Bibliotheks-Panels unterstützt. Das bedeutet, dass wir bei einem Export des Dashboards für die Nutzung in einer anderen Instanz keine Bibliotheks-Panels einbeziehen. Wir planen, sie im weiteren Verlauf des <2>Lebenszyklus der Funktion zu unterstützen.", "schema-v2-library-panels-export-title": "Das Dashboard-Schema V2 unterstützt noch keinen Export von Bibliotheks-Panels für die Verwendung in einer anderen Instanz", - "title-dashboard-drastically-changed": "Dashboard erheblich verändert" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Macht die aktuelle Aktualisierung zum neuen Standard", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Markierung aktivieren", "escape-newlines": "Falsch dargestellte Zeilenumbrüche und Tab-Sequenzen in Log-Zeilen korrigieren", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Zeitstempel ausblenden", "hide-unique-labels": "Eindeutige Labels ausblenden", "newest-first": "Sortiert nach neuesten Logs zuerst – klicken Sie, um die ältesten zuerst anzuzeigen", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 11561a508f8..a6afb1a35c5 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Elegir", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "mensaje", - "body-dashboard-drastically-changed": "Como estás utilizando nuevas funciones de dashboards que solo son compatibles con el nuevo formato de esquema de dashboards de Grafana, el dashboard se guardará en el nuevo formato. Asegúrate de que quieres realizar esta acción o prefieres guardar el dashboard como una nueva copia.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Cancelar" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "La funcionalidad del dashboard dinámico es experimental y no tiene una paridad de características completa con el comportamiento actual de los dashboards. Se basa en un nuevo formato de esquema, que no admite paneles de librería. Esto significa que al exportar el dashboard para usarlo en otra instancia, no se incluirán paneles de librería. Tenemos la intención de admitirlos a medida que avanzamos en el <2>ciclo de vida de la función.", "schema-v2-library-panels-export-title": "El esquema de dashboard V2 aún no admite la exportación de paneles de librería para su uso en otra instancia", - "title-dashboard-drastically-changed": "El dashboard ha cambiado drásticamente" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Hará que la actualización actual sea la nueva predeterminada", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Activar resaltado", "escape-newlines": "Corregir las secuencias de tabulación y de nueva línea que escaparon incorrectamente en las líneas de log", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Ocultar marcas de tiempo", "hide-unique-labels": "Ocultar etiquetas únicas", "newest-first": "Ordenado por los logs más nuevos primero: haga clic para mostrar los más antiguos primero", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 1550720bc2e..24437051729 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Choisir", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "message", - "body-dashboard-drastically-changed": "Étant donné que vous utilisez de nouvelles fonctionnalités de tableau de bord uniquement prises en charge par le nouveau format de schéma de tableau de bord Grafana, le tableau de bord sera enregistré dans le nouveau format. Veuillez vous assurer que vous souhaitez effectuer cette action ou que vous préférez enregistrer le tableau de bord en tant que nouvelle copie.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Annuler" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "La fonctionnalité de tableau de bord dynamique est expérimentale et n’a pas une parité complète de fonctionnalités avec le comportement actuel des tableaux de bord. Elle est basée sur un nouveau format de schéma, qui ne prend pas en charge les panneaux de bibliothèque. Cela signifie que lors de l’exportation du tableau de bord pour l’utiliser dans une autre instance, nous n’inclurons pas les panneaux de la bibliothèque. Nous avons l’intention de les prendre en charge au fur et à mesure de notre progression dans le <2>cycle de vie de la fonctionnalité.", "schema-v2-library-panels-export-title": "Le schéma de tableau de bord V2 ne prend pas encore en charge l’exportation de panneaux de bibliothèque à utiliser dans une autre instance", - "title-dashboard-drastically-changed": "Le tableau de bord a radicalement changé" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "L’actualisation actuelle deviendra la nouvelle valeur par défaut", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Activer la surbrillance", "escape-newlines": "Correction des séquences de sauts de ligne et de tabulations incorrectement échappées dans les lignes du journal", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Masquer les horodatages", "hide-unique-labels": "Masquer les étiquettes uniques", "newest-first": "Trié par les journaux les plus récents en premier - Cliquez pour afficher les plus anciens en premier", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 0742502cef8..56c696de3c3 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Kiválasztás", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "üzenet", - "body-dashboard-drastically-changed": "Mivel olyan új irányítópult-funkciókat használ, amelyeket csak a Grafana új irányítópult-sémaformátuma támogat, az irányítópultot az új formátumban menti a rendszer. Győződjön meg róla, hogy végre kívánja hajtani ezt a műveletet, vagy inkább új példányként menti az irányítópultot.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Mégse" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "A dinamikus irányítópult funkcionalitása kísérleti jellegű, és nem rendelkezik teljes funkcionalitással a jelenlegi irányítópultok viselkedéséhez képest. Új sémaformátumon alapul, amely nem támogatja a könyvtárpaneleket. Ez azt jelenti, hogy amikor az irányítópultot egy másik példányban való használathoz exportálja, nem fogja tartalmazni a könyvtárpaneleket. Támogatni kívánjuk őket a funkció <2>életciklusának előrehaladtával.", "schema-v2-library-panels-export-title": "A Dashboard Schema V2 még nem támogatja a könyvtárpanelek exportálását egy másik példányban való használathoz", - "title-dashboard-drastically-changed": "Irányítópult nagymértékben megváltozott" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Az aktuális frissítés lesz az új alapértelmezés", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Kiemelés engedélyezése", "escape-newlines": "Helytelenül értelmezett újsor- és tabulátorkarakteres szekvenciák javítása a naplósorokban", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Időbélyegek elrejtése", "hide-unique-labels": "Egyedi címkék elrejtése", "newest-first": "Rendezés a legújabb naplók szerint – kattintson, hogy a legrégebbi naplók jelenjenek meg elsőként", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 0958500de74..1d9d04451be 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -4408,6 +4408,7 @@ } }, "repeat-row-select2": { + "placeholder": "Pilih", "variable-options": { "label": { "disable-repeating": "", @@ -5223,7 +5224,7 @@ }, "save-dashboard-form": { "aria-label-message": "pesan", - "body-dashboard-drastically-changed": "Anda menggunakan fitur dasbor baru yang hanya didukung pada format skema dasbor Grafana baru, jadi dasbor akan disimpan dalam format baru. Pastikan Anda ingin melakukan tindakan ini atau Anda lebih suka menyimpan dasbor sebagai salinan baru.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Batalkan" }, @@ -5239,7 +5240,7 @@ }, "schema-v2-library-panels-export": "Fungsionalitas dasbor dinamis bersifat eksperimen, dan tidak memiliki paritas fitur penuh dengan perilaku dasbor saat ini. Ini didasarkan pada format skema baru, yang tidak mendukung panel pustaka. Ini berarti ketika mengekspor dasbor untuk menggunakannya di instans lain, kami tidak akan menyertakan panel pustaka. Kami bermaksud untuk mendukung seiring fitur <2>siklus pengembangan fitur.", "schema-v2-library-panels-export-title": "Skema Dasbor V2 belum mendukung ekspor panel pustaka untuk digunakan di instans lain", - "title-dashboard-drastically-changed": "Dasbor berubah drastis" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Akan menjadikan muat ulang saat ini sebagai default baru", @@ -7689,6 +7690,8 @@ }, "enable-highlighting": "Aktifkan penyorotan", "escape-newlines": "Perbaiki escape sequence baris baru dan tab yang salah di baris log", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Sembunyikan stempel waktu", "hide-unique-labels": "Sembunyikan label unik", "newest-first": "Diurutkan berdasarkan log terbaru lebih dulu - Klik untuk menampilkan yang paling lama lebih dulu", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index e42366bad35..b23d35408ab 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Scegli", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "messaggio", - "body-dashboard-drastically-changed": "Dato che stai utilizzando nuove funzionalità della dashboard supportate solo nel nuovo formato grafico della dashboard Grafana, la dashboard verrà salvata nel nuovo formato. Assicurati di voler eseguire questa azione o di voler salvare la dashboard come nuova copia.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Annulla" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "La funzionalità della dashboard dinamica è in fase sperimentale e non ha una parità di funzionalità completa con il comportamento attuale delle dashboard. Si basa su un nuovo formato di schema, che non supporta i pannelli della libreria. Ciò significa che quando si esporta la dashboard per utilizzarla in un'altra istanza, non includeremo i pannelli della libreria. Intendiamo supportarli man mano che progrediamo nel <2>ciclo di vita della funzionalità.", "schema-v2-library-panels-export-title": "Schema Dashboard V2 non supporta ancora l'esportazione di pannelli della libreria da utilizzare in un'altra istanza", - "title-dashboard-drastically-changed": "La dashboard è cambiata radicalmente" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Imposterà l'aggiornamento corrente come nuovo valore predefinito", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Abilita evidenziazione", "escape-newlines": "Correggi le sequenze di nuove righe e tabulazioni non corrette nelle righe del registro", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Nascondi marca temporale", "hide-unique-labels": "Nascondi etichette univoche", "newest-first": "Ordine: prima i registri più recenti – Fai clic per mostrare prima i meno recenti", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index f65a2584594..983d64de58b 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -4408,6 +4408,7 @@ } }, "repeat-row-select2": { + "placeholder": "選択する", "variable-options": { "label": { "disable-repeating": "", @@ -5223,7 +5224,7 @@ }, "save-dashboard-form": { "aria-label-message": "メッセージ", - "body-dashboard-drastically-changed": "新しいGrafanaダッシュボードスキーマ形式でのみサポートされている新たなダッシュボード機能を使用しているため、ダッシュボードは新形式で保存されます。この操作を実行するか、ダッシュボードを新しいコピーとして保存するかを選択してください。", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "キャンセル" }, @@ -5239,7 +5240,7 @@ }, "schema-v2-library-panels-export": "動的ダッシュボード機能は実験的なもので、現在のダッシュボード動作と完全な機能互換性はありません。この機能は、ライブラリパネルをサポートしていない新しいスキーマ形式に基づいています。そのため、別のインスタンスで使用する目的でダッシュボードをエクスポートした場合、ライブラリパネルは含まれません。機能の<2>ライフサイクルが進むにつれ、これらをサポートする予定です。", "schema-v2-library-panels-export-title": "現在ダッシュボードスキーマV2では、別のインスタンスでの使用目的でのライブラリパネルのエクスポートをサポートしていません", - "title-dashboard-drastically-changed": "ダッシュボードが大幅に変更されました" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "現在の更新を新しいデフォルトに設定", @@ -7689,6 +7690,8 @@ }, "enable-highlighting": "ハイライトを有効にする", "escape-newlines": "ログ行で誤ってエスケープされた改行とタブシーケンスを修正", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "タイムスタンプを非表示", "hide-unique-labels": "一意のラベルを非表示", "newest-first": "最新のログ順に並び替え - クリックして最も古いログを最初に表示", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 43bf9493a40..e1c26837746 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -4408,6 +4408,7 @@ } }, "repeat-row-select2": { + "placeholder": "선택", "variable-options": { "label": { "disable-repeating": "", @@ -5223,7 +5224,7 @@ }, "save-dashboard-form": { "aria-label-message": "메시지", - "body-dashboard-drastically-changed": "새로운 Grafana 대시보드 스키마 형식에서만 지원되는 새 대시보드 기능을 사용하고 있기 때문에 대시보드가 새 형식으로 저장됩니다. 이 동작을 수행할지 아니면 대시보드를 새 복사본으로 저장할지 확인하세요.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "취소" }, @@ -5239,7 +5240,7 @@ }, "schema-v2-library-panels-export": "동적 대시보드 기능은 실험 단계이며 현재 대시보드 동작과 모든 기능이 동일하지는 않습니다. 이 기능은 라이브러리 패널을 지원하지 않는 새로운 스키마 형식을 기반으로 합니다. 즉, 다른 인스턴스에서 사용하기 위해 대시보드를 내보낼 때 라이브러리 패널이 포함되지 않는다는 것을 의미합니다. Grafana는 기능 <2>수명 주기가 진행됨에 따라 이를 지원할 계획입니다.", "schema-v2-library-panels-export-title": "대시보드 스키마 V2는 아직 다른 인스턴스에서 사용할 라이브러리 패널 내보내기를 지원하지 않습니다", - "title-dashboard-drastically-changed": "대시보드가 대폭 변경됨" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "현재 새로 고침을 새로운 기본값으로 설정합니다", @@ -7689,6 +7690,8 @@ }, "enable-highlighting": "강조 표시 활성화", "escape-newlines": "로그 줄에서 잘못된 줄 바꿈 및 탭 시퀀스 수정", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "타임스탬프 숨기기", "hide-unique-labels": "고유 라벨 숨기기", "newest-first": "최신 로그순으로 정렬 - 클릭하여 오래된 로그순으로 표시", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index f46f810c390..07fdfeb9acd 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Kies", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "bericht", - "body-dashboard-drastically-changed": "Omdat je nieuwe dashboardfuncties gebruikt die alleen worden ondersteund in de nieuwe indeling van het Grafana-dashboardschema, wordt het dashboard opgeslagen in de nieuwe indeling. Beslis of je deze actie wilt uitvoeren of dat je het dashboard liever als een nieuwe kopie opslaat.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Annuleren" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "De dynamische dashboardfunctionaliteit is experimenteel en heeft geen volledige functiepariteit met het huidige gedrag van dashboards. Het is gebaseerd op een nieuw schemaformaat, dat geen bibliotheekpanelen ondersteunt. Dit betekent dat we bij het exporteren van het dashboard om het in een andere instantie te gebruiken, geen bibliotheekpanelen opnemen. We zijn van plan om ze te ondersteunen naarmate we verder komen in de <2>levenscyclus van de functie.", "schema-v2-library-panels-export-title": "Dashboardschema V2 biedt nog geen ondersteuning voor het exporteren van bibliotheekpanelen die in een andere instantie kunnen worden gebruikt", - "title-dashboard-drastically-changed": "Dashboard is drastisch gewijzigd" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Maakt de huidige vernieuwing de nieuwe standaard", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Markeren inschakelen", "escape-newlines": "Corrigeer onjuist escaped nieuwe lijn en tabbladsequenties in logregels", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Tijdstempels verbergen", "hide-unique-labels": "Unieke labels verbergen", "newest-first": "Gesorteerd op nieuwste logboeken eerst - klik om oudste eerst weer te geven", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 01ed2a83213..57b8b88d424 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -4465,6 +4465,7 @@ } }, "repeat-row-select2": { + "placeholder": "Wybierz", "variable-options": { "label": { "disable-repeating": "", @@ -5283,7 +5284,7 @@ }, "save-dashboard-form": { "aria-label-message": "Wiadomość", - "body-dashboard-drastically-changed": "Ponieważ używasz nowych funkcji obsługiwanych tylko w nowym formacie schematu pulpitu Grafany, pulpit zostanie zapisany w nowym formacie. Zdecyduj, czy chcesz wykonać tę czynność, czy wolisz zapisać pulpit jako nową kopię.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Anuluj" }, @@ -5299,7 +5300,7 @@ }, "schema-v2-library-panels-export": "Funkcja pulpitu dynamicznego jest eksperymentalna i nie odpowiada w pełni obecnemu sposobowi działania pulpitów. Opiera się na nowym formacie schematu, który nie obsługuje paneli biblioteki. Oznacza to, że przy eksporcie pulpitu, aby użyć go w innej instancji, nie uwzględnimy paneli biblioteki. Zamierzamy dodać obsługę tych paneli w miarę <2>rozwoju tej funkcji.", "schema-v2-library-panels-export-title": "Schemat pulpitu w wersji 2 nie obsługuje jeszcze eksportowania paneli biblioteki do użytku w innej instancji", - "title-dashboard-drastically-changed": "Panel znacznie się zmienił" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Bieżące odświeżenie stanie się nową wartością domyślną", @@ -7767,6 +7768,8 @@ }, "enable-highlighting": "Włącz wyróżnianie", "escape-newlines": "Napraw nieprawidłowe sekwencje znaków nowego wiersza i tabulacji we wpisach logów", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Ukryj znaczniki czasu", "hide-unique-labels": "Ukryj unikalne etykiety", "newest-first": "Sortowanie od najnowszych wpisów dziennika – kliknij, aby wyświetlić najpierw najstarsze", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index b915ddd779e..2e66bdf9632 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Escolher", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "mensagem", - "body-dashboard-drastically-changed": "Como você está usando novos recursos de painéis compatíveis apenas com o novo formato de esquema de painel da Grafana, o painel será salvo no novo formato. Certifique-se de que você deseja executar esta ação ou prefere salvar o painel como uma nova cópia.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Cancelar" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "A funcionalidade de painel dinâmico está em testes e não possui todos os recursos presentes no comportamento atual dos painéis. Ele se baseia em um novo formato de esquema, que não aceita painéis da biblioteca. Isso significa que, ao exportar o painel para usá-lo em outra instância, não incluiremos painéis da biblioteca. Pretendemos aceitá-los à medida que avançamos no <2>ciclo de vida do recurso.", "schema-v2-library-panels-export-title": "O esquema de painel V2 ainda não aceita a exportação de painéis da biblioteca para uso em outra instância", - "title-dashboard-drastically-changed": "O painel foi alterado drasticamente" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Fará com que a atualização atual seja o novo padrão", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Ativar destaque", "escape-newlines": "Corrigir sequências de tabulação e quebra de linha adicionadas incorretamente por escape nas linhas de log", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Ocultar data e hora", "hide-unique-labels": "Ocultar rótulos exclusivos", "newest-first": "Organizado por logs mais recentes primeiro: clique para exibir os mais antigos primeiro", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 423138a47e4..f701ea99c9c 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Escolher", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "mensagem", - "body-dashboard-drastically-changed": "Como está a utilizar novas funcionalidades de painéis de controlo suportadas apenas no novo formato de esquema de painel de controlo da Grafana, este será guardado no novo formato. Certifique-se de que pretende realizar esta ação ou se prefere guardar o painel de controlo como uma nova cópia.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Cancelar" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "A funcionalidade do painel de controlo dinâmico é experimental e não tem paridade de funcionalidade total com o comportamento atual dos painéis de controlo. Baseia-se num novo formato de esquema, que não suporta painéis de biblioteca. Isto significa que, ao exportar o painel de controlo para o utilizar noutra instância, não incluiremos painéis de biblioteca. Tencionamos suportá-los à medida que avançamos no <2>ciclo de vida da funcionalidade.", "schema-v2-library-panels-export-title": "O Esquema de Painel de Controlo V2 ainda não suporta a exportação de painéis de biblioteca para serem utilizados noutra instância", - "title-dashboard-drastically-changed": "Painel de controlo alterado drasticamente" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Tornará a atualização atual a nova predefinição", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Ativar destaque", "escape-newlines": "Corrigir sequências de novas linhas e tabulações ignoradas incorretamente nas linhas de registo", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Ocultar registos de hora/data", "hide-unique-labels": "Ocultar etiquetas únicas", "newest-first": "Ordenado por registos mais recentes primeiro - Clique para mostrar os mais antigos primeiro", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 60f01244e92..fff7cb7c230 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -4465,6 +4465,7 @@ } }, "repeat-row-select2": { + "placeholder": "Выбрать", "variable-options": { "label": { "disable-repeating": "", @@ -5283,7 +5284,7 @@ }, "save-dashboard-form": { "aria-label-message": "сообщение", - "body-dashboard-drastically-changed": "Поскольку вы используете новые функции дашбордов, поддерживаемые только в новом формате схемы дашбордов Grafana, дашборд будет сохранен в новом формате. Выполните это действие, если оно действительно необходимо, или сохраните дашборд как новую копию.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Отмена" }, @@ -5299,7 +5300,7 @@ }, "schema-v2-library-panels-export": "Функциональность динамического дашборда является экспериментальной и не имеет полного паритета функциональных возможностей с текущим поведением дашбордов. Она основана на новом формате схемы, который не поддерживает панели библиотеки. Таким образом, экспорт дашборда для использования в другом экземпляре не будет включать панели библиотеки. Мы намерены поддерживать их по мере прохождения <2>жизненного цикла функции.", "schema-v2-library-panels-export-title": "Дашборды схемы V2 пока не поддерживают экспорт панелей библиотеки для использования в другом экземпляре", - "title-dashboard-drastically-changed": "Дашборд существенно изменен" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Текущее обновление станет новым значением по умолчанию", @@ -7767,6 +7768,8 @@ }, "enable-highlighting": "Включить выделение", "escape-newlines": "Исправить неправильно экранированные последовательности новой строки и табуляции в строках журнала", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Скрыть метки времени", "hide-unique-labels": "Скрыть уникальные метки", "newest-first": "Сначала отображаются самые новые журналы. Нажмите, чтобы показать сначала самые старые", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 5f2a1d0eb85..a4af1a97a34 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Välj", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "meddelande", - "body-dashboard-drastically-changed": "Eftersom du använder nya instrumentpanelsfunktioner som endast stöds på det nya schemaformatet för Grafana-instrumentpaneler kommer instrumentpanelen att sparas i det nya formatet. Utför denna åtgärd om du verkligen vill göra det eller vill spara instrumentpanelen som en ny kopia.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "Avbryt" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "Funktionen för dynamiska instrumentpaneler är experimentell och har ännu inte fullständig funktionsparitet med nuvarande instrumentpanelbeteende. Det är baserat på ett nytt schemaformat som saknar stöd för bibliotekspaneler. Det innebär att om du vill använda instrumentpanelen i en annan instans och exporterar den kommer inte bibliotekspaneler att inkluderas under exporten. Vi har för avsikt att inkludera stöd för dem när vi går vidare med funktionens <2>livscykel.", "schema-v2-library-panels-export-title": "Instrumentpanelschema V2 saknar för närvarande stöd för export av bibliotekspaneler för användning i en annan instans", - "title-dashboard-drastically-changed": "Instrumentpanelen har ändrats drastiskt" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Kommer att göra den aktuella uppdateringen till ny standard", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Aktivera markering", "escape-newlines": "Åtgärda felaktiga undantagstecken för radbrytningar och tabbar i loggrader", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Dölj tidsstämplar", "hide-unique-labels": "Dölj unika etiketter", "newest-first": "Sorterat efter nyaste loggar först – klicka om du vill visa äldsta först", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 74de420a8fd..6bae3fef3ff 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -4427,6 +4427,7 @@ } }, "repeat-row-select2": { + "placeholder": "Seçin", "variable-options": { "label": { "disable-repeating": "", @@ -5243,7 +5244,7 @@ }, "save-dashboard-form": { "aria-label-message": "mesaj", - "body-dashboard-drastically-changed": "Yalnızca yeni Grafana pano şema biçiminde desteklenen yeni pano özelliklerini kullandığınız için pano yeni biçimde kaydedilecektir. Lütfen bu işlemi yapmak istediğinizden emin olun veya panoyu yeni bir kopya olarak kaydetmeyi tercih edin.", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "İptal" }, @@ -5259,7 +5260,7 @@ }, "schema-v2-library-panels-export": "Dinamik pano işlevi henüz test aşamasındadır ve mevcut panoların tüm işlevlerini tam olarak karşılamamaktadır. Yeni şema biçimi henüz kütüphane panellerini desteklemiyor. Bu nedenle, dışa aktarılan panolarda kütüphane panelleri yer almayacaktır. Özellik <2>geliştirme süreci ilerledikçe bu desteğin eklenmesi planlanmaktadır.", "schema-v2-library-panels-export-title": "Pano Şeması V2, kütüphane panellerinin başka bir örnekte kullanılmak üzere dışa aktarılmasını henüz desteklememektedir", - "title-dashboard-drastically-changed": "Pano büyük ölçüde değişti" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "Mevcut yenilemeyi yeni varsayılan yapacak", @@ -7715,6 +7716,8 @@ }, "enable-highlighting": "Vurgulamayı etkinleştir", "escape-newlines": "Günlük kaydı satırlarındaki kaçış karakterli satır sonu ve sekme dizilerini düzelt", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "Zaman damgalarını gizle", "hide-unique-labels": "Benzersiz etiketleri gizle", "newest-first": "Günlükler yeniden eskiye sıralandı: En eskileri göstermek için tıklayın", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index b1902f5aaab..da0e9c729f1 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -4408,6 +4408,7 @@ } }, "repeat-row-select2": { + "placeholder": "选择", "variable-options": { "label": { "disable-repeating": "", @@ -5223,7 +5224,7 @@ }, "save-dashboard-form": { "aria-label-message": "消息", - "body-dashboard-drastically-changed": "由于您正在使用的新数据面板功能仅支持新的 Grafana 数据面板架构格式,因此数据面板将以新格式保存。请确保您要执行此操作,或者您希望将数据面板保存为新副本。", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "取消" }, @@ -5239,7 +5240,7 @@ }, "schema-v2-library-panels-export": "动态数据面板功能是实验性的,并且与当前数据面板行为没有完全的功能对称性。它基于新的模式格式,且不支持库面板。这意味着,在导出数据面板以便在其他实例中使用时,其中不会包含库面板。我们打算在<2>生命周期功能中逐步支持它们。", "schema-v2-library-panels-export-title": "数据面板模式 V2 暂不支持导出库面板以便在其他实例中使用", - "title-dashboard-drastically-changed": "数据面板发生了重大变化" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "将使当前刷新成为新的默认值", @@ -7689,6 +7690,8 @@ }, "enable-highlighting": "启用突出显示", "escape-newlines": "修复日志行中错误转义的换行和制表符序列", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "隐藏时间戳", "hide-unique-labels": "隐藏唯一标签", "newest-first": "先显示最新日志 - 点击以先显示最旧日志", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 29108535604..851bc9e0b19 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -4408,6 +4408,7 @@ } }, "repeat-row-select2": { + "placeholder": "選擇", "variable-options": { "label": { "disable-repeating": "", @@ -5223,7 +5224,7 @@ }, "save-dashboard-form": { "aria-label-message": "訊息", - "body-dashboard-drastically-changed": "由於您正在使用僅在新的 Grafana 儀表板架構格式中支援的新儀表板功能,因此儀表板將以新格式儲存。請確認您要執行此操作,還是要將儀表板儲存為新副本。", + "body-dashboard-drastically-changed": "", "cancel-button": { "cancel": "取消" }, @@ -5239,7 +5240,7 @@ }, "schema-v2-library-panels-export": "動態儀表板功能僅供試驗,並未與目前的儀表板行為具有完全的功能平等性。它以新的模式格式為基礎,不支援資料庫面板。這表示在匯出儀表板以便在另一個執行個體中使用時,我們不會包含資料庫面板。隨著我們在功能<2>生命週期中取得進展,我們計劃為其提供支援。", "schema-v2-library-panels-export-title": "儀表板架構 V2 尚不支援為了在另一個執行個體中使用而匯出資料庫面板", - "title-dashboard-drastically-changed": "儀表板已大幅變更" + "title-dashboard-drastically-changed": "" }, "save-dashboard-form-common-options": { "save-refresh-description-current-refresh-default": "將使目前的重新整理成為新的預設值", @@ -7689,6 +7690,8 @@ }, "enable-highlighting": "啟用醒目提示", "escape-newlines": "修復紀錄行中新行和分頁序列的錯誤轉義", + "font-size-default": "", + "font-size-small": "", "hide-timestamps": "隱藏時間戳記", "hide-unique-labels": "隱藏唯一標籤", "newest-first": "按最新紀錄排序 - 按一下以顯示最舊紀錄", diff --git a/public/openapi3.json b/public/openapi3.json index 1c59dd4fe75..412cdd89dc7 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -3048,6 +3048,12 @@ "properties": { "alertmanager_config": { "$ref": "#/components/schemas/Config" + }, + "template_files": { + "additionalProperties": { + "type": "string" + }, + "type": "object" } }, "type": "object" @@ -5905,6 +5911,20 @@ }, "type": "object" }, + "GettableAlertmanagerUserConfig": { + "properties": { + "alertmanager_config": { + "type": "string" + }, + "template_files": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "GettableAlertmanagers": { "properties": { "data": { @@ -6758,7 +6778,11 @@ "description": "InhibitRule defines an inhibition rule that mutes alerts that match the\ntarget labels if an alert matching the source labels exists.\nBoth alerts have to have a set of labels being equal.", "properties": { "equal": { - "$ref": "#/components/schemas/LabelNames" + "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.", + "items": { + "type": "string" + }, + "type": "array" }, "source_match": { "additionalProperties": { @@ -6934,17 +6958,6 @@ "title": "Label is a key/value pair of strings.", "type": "object" }, - "LabelName": { - "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.", - "type": "string" - }, - "LabelNames": { - "items": { - "$ref": "#/components/schemas/LabelName" - }, - "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.", - "type": "array" - }, "LabelSet": { "additionalProperties": { "$ref": "#/components/schemas/LabelValue" @@ -10077,6 +10090,7 @@ "type": "object" }, "Route": { + "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "properties": { "active_time_intervals": { "items": { @@ -10118,6 +10132,12 @@ }, "type": "array" }, + "object_matchers": { + "$ref": "#/components/schemas/ObjectMatchers" + }, + "provenance": { + "$ref": "#/components/schemas/Provenance" + }, "receiver": { "type": "string" }, @@ -10131,7 +10151,6 @@ "type": "array" } }, - "title": "A Route is a node that contains definitions of how to handle alerts.", "type": "object" }, "RouteExport": { @@ -12459,6 +12478,9 @@ "Email": { "type": "string" }, + "ExternalUID": { + "type": "string" + }, "ID": { "format": "int64", "type": "integer" diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star index d5abeb28022..d0ed5fbd63e 100644 --- a/scripts/drone/variables.star +++ b/scripts/drone/variables.star @@ -3,7 +3,7 @@ global variables """ grabpl_version = "v3.1.2" -golang_version = "1.24.3" +golang_version = "1.24.4" # nodejs_version should match what's in ".nvmrc", but without the v prefix. nodejs_version = "22.11.0" diff --git a/scripts/go-workspace/go.mod b/scripts/go-workspace/go.mod index bc39d670ccd..5e23c2f70b2 100644 --- a/scripts/go-workspace/go.mod +++ b/scripts/go-workspace/go.mod @@ -1,5 +1,5 @@ module github.com/grafana/grafana/scripts/go-workspace -go 1.24.3 +go 1.24.4 require golang.org/x/mod v0.24.0 diff --git a/scripts/modowners/go.mod b/scripts/modowners/go.mod index 1b2315dde57..10b70c57884 100644 --- a/scripts/modowners/go.mod +++ b/scripts/modowners/go.mod @@ -1,5 +1,5 @@ module github.com/grafana/grafana/scripts/modowners -go 1.24.3 +go 1.24.4 require golang.org/x/mod v0.24.0 diff --git a/scripts/validate-npm-packages.sh b/scripts/validate-npm-packages.sh index 2f0f1764597..47b82a49717 100755 --- a/scripts/validate-npm-packages.sh +++ b/scripts/validate-npm-packages.sh @@ -9,72 +9,10 @@ for file in "$ARTIFACTS_DIR"/*.tgz; do echo "🔍 Checking NPM package: $file" # Ignore named-exports for now as builds aren't compatible yet. - yarn dlx @arethetypeswrong/cli "$file" --ignore-rules "named-exports" - - # get filename then strip everything after package name. - dir_name=$(basename "$file" .tgz | sed -E 's/@([a-zA-Z0-9-]+)-[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9-]+)?/\1/') - mkdir -p "./npm-artifacts/$dir_name" - tar -xzf "$file" -C "./npm-artifacts/$dir_name" --strip-components=1 - - # Make sure the tar wasn't empty - if [ ! -d "./npm-artifacts/$dir_name" ]; then - echo -e "❌ Failed: Empty package $dir_name.\n" - exit 1 - fi - - # Navigate inside the new extracted directory - pushd "./npm-artifacts/$dir_name" || exit - - # Check for required files - check_files=("package.json" "README.md" "CHANGELOG.md") - for check_file in "${check_files[@]}"; do - if [ ! -f "$check_file" ]; then - echo -e "❌ Failed: Missing required file $check_file in package $dir_name.\n" - exit 1 - fi - done - - # Check license files - if [ -f "LICENSE_APACHE2" ] || [ -f "LICENSE_AGPL" ]; then - echo -e "Found required license file in package $dir_name.\n" - else - echo -e "❌ Failed: Missing required license file in package $dir_name.\n" - exit 1 - fi - - # Assert commonjs builds - if [ ! -d dist ] || [ ! -f dist/cjs/index.cjs ] || [ ! -f dist/cjs/index.d.cts ]; then - echo -e "❌ Failed: Missing 'dist' directory or required commonjs files in package $dir_name.\n" - exit 1 - fi - - if [ "$(jq -r '.main' package.json)" != "./dist/cjs/index.cjs" ] || \ - [ "$(jq -r '.types' package.json)" != "./dist/cjs/index.d.cts" ]; then - echo -e "❌ Failed: Incorrect cjs package.json properties in package $dir_name.\n" - exit 1 - fi - - # Assert esm builds - esm_packages=("grafana-data" "grafana-ui" "grafana-runtime" "grafana-e2e-selectors" "grafana-schema") - for esm_package in "${esm_packages[@]}"; do - if [[ "$dir_name" == "$esm_package" ]]; then - if [ ! -d dist/esm ] || [ ! -f dist/esm/index.mjs ]; then - echo -e "❌ Failed: Missing 'dist/esm' directory or required esm files in package $dir_name.\n" - exit 1 - fi - - if [ "$(jq -r '.module' package.json)" != "./dist/esm/index.mjs" ]; then - echo -e "❌ Failed: Incorrect esm package.json properties in package $dir_name.\n" - exit 1 - fi - fi - done - - echo -e "✅ Passed: package checks for $file.\n" - popd || exit + yarn attw "$file" --ignore-rules "named-exports" + yarn publint "$file" done echo "🚀 All NPM package checks passed! 🚀" -rm -rf "${ARTIFACTS_DIR:?}/"*/ exit 0 diff --git a/yarn.lock b/yarn.lock index 962df61f595..8b95b41edca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,6 +29,13 @@ __metadata: languageName: node linkType: hard +"@andrewbranch/untar.js@npm:^1.0.3": + version: 1.0.3 + resolution: "@andrewbranch/untar.js@npm:1.0.3" + checksum: 10/a32de53839fc61af90a394cf93d4368aacd167c9c80f0b3ba0c268460942a6ce2bfe257b6d3f03986b9dcb7368f10b9dc7f66c2f94254d2662da8278454e7d12 + languageName: node + linkType: hard + "@apidevtools/json-schema-ref-parser@npm:9.0.6": version: 9.0.6 resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.6" @@ -71,6 +78,39 @@ __metadata: languageName: node linkType: hard +"@arethetypeswrong/cli@npm:^0.18.2": + version: 0.18.2 + resolution: "@arethetypeswrong/cli@npm:0.18.2" + dependencies: + "@arethetypeswrong/core": "npm:0.18.2" + chalk: "npm:^4.1.2" + cli-table3: "npm:^0.6.3" + commander: "npm:^10.0.1" + marked: "npm:^9.1.2" + marked-terminal: "npm:^7.1.0" + semver: "npm:^7.5.4" + bin: + attw: dist/index.js + checksum: 10/8b4506edeb37d58f15e347302df68981c5aefecce973e746026d46370bb560c1ebc05ef8a38eba3102881df4cd3c901961186e3df41077efca4e58adffd455a1 + languageName: node + linkType: hard + +"@arethetypeswrong/core@npm:0.18.2": + version: 0.18.2 + resolution: "@arethetypeswrong/core@npm:0.18.2" + dependencies: + "@andrewbranch/untar.js": "npm:^1.0.3" + "@loaderkit/resolve": "npm:^1.0.2" + cjs-module-lexer: "npm:^1.2.3" + fflate: "npm:^0.8.2" + lru-cache: "npm:^11.0.1" + semver: "npm:^7.5.4" + typescript: "npm:5.6.1-rc" + validate-npm-package-name: "npm:^5.0.0" + checksum: 10/9c3edeb8e09e572682e37f55bd523d0dad45388232d31fa1d8875f7f5c414a184070c2bb6d0c8f254dfce4ed9248da373d549ecdeaa571a0cfff04c387c94cf1 + languageName: node + linkType: hard + "@babel/code-frame@npm:7.25.7": version: 7.25.7 resolution: "@babel/code-frame@npm:7.25.7" @@ -1587,6 +1627,13 @@ __metadata: languageName: node linkType: hard +"@braidai/lang@npm:^1.0.0": + version: 1.1.1 + resolution: "@braidai/lang@npm:1.1.1" + checksum: 10/3d6b1827aabe8b4b39d938e1fecb747d3d45ea621958f8366d48b97e18b6b1edf68328ae57b8c0fb3d4b9757757a86761f911d9e15cde69bdbd28482952e43d6 + languageName: node + linkType: hard + "@braintree/sanitize-url@npm:7.0.1": version: 7.0.1 resolution: "@braintree/sanitize-url@npm:7.0.1" @@ -4803,6 +4850,15 @@ __metadata: languageName: node linkType: hard +"@loaderkit/resolve@npm:^1.0.2": + version: 1.0.4 + resolution: "@loaderkit/resolve@npm:1.0.4" + dependencies: + "@braidai/lang": "npm:^1.0.0" + checksum: 10/e999f0fc289c2e3f9f80ec92db69c123a5a74b5db7c4bc10292658fc9ef2e1afe6430346ca6cd52d941d7fc407bf28188c95bbbe0aa212c02c8716b5c4b03316 + languageName: node + linkType: hard + "@locker/near-membrane-base@npm:0.13.6": version: 0.13.6 resolution: "@locker/near-membrane-base@npm:0.13.6" @@ -6128,6 +6184,13 @@ __metadata: languageName: node linkType: hard +"@publint/pack@npm:^0.1.2": + version: 0.1.2 + resolution: "@publint/pack@npm:0.1.2" + checksum: 10/83e1de31ae29a0e651f7f91ebe6ad1fdf8cbb61d1eb056476586a234d05fa6fde9f34d3a0e36fbf18a2e9affa1082f758833242fd285637d303130f1a286b928 + languageName: node + linkType: hard + "@radix-ui/react-compose-refs@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-compose-refs@npm:1.0.1" @@ -7031,6 +7094,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/is@npm:^4.6.0": + version: 4.6.0 + resolution: "@sindresorhus/is@npm:4.6.0" + checksum: 10/e7f36ed72abfcd5e0355f7423a72918b9748bb1ef370a59f3e5ad8d40b728b85d63b272f65f63eec1faf417cda89dcb0aeebe94015647b6054659c1442fe5ce0 + languageName: node + linkType: hard + "@sindresorhus/merge-streams@npm:^2.1.0": version: 2.3.0 resolution: "@sindresorhus/merge-streams@npm:2.3.0" @@ -11138,6 +11208,15 @@ __metadata: languageName: node linkType: hard +"ansi-escapes@npm:^7.0.0": + version: 7.0.0 + resolution: "ansi-escapes@npm:7.0.0" + dependencies: + environment: "npm:^1.0.0" + checksum: 10/2d0e2345087bd7ae6bf122b9cc05ee35560d40dcc061146edcdc02bc2d7c7c50143cd12a22e69a0b5c0f62b948b7bc9a4539ee888b80f5bd33cdfd82d01a70ab + languageName: node + linkType: hard + "ansi-html-community@npm:0.0.8, ansi-html-community@npm:^0.0.8": version: 0.0.8 resolution: "ansi-html-community@npm:0.0.8" @@ -11177,6 +11256,13 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^6.1.0": + version: 6.1.0 + resolution: "ansi-regex@npm:6.1.0" + checksum: 10/495834a53b0856c02acd40446f7130cb0f8284f4a39afdab20d5dc42b2e198b1196119fe887beed8f9055c4ff2055e3b2f6d4641d0be018cdfb64fedf6fc1aac + languageName: node + linkType: hard + "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -11230,6 +11316,13 @@ __metadata: languageName: node linkType: hard +"any-promise@npm:^1.0.0": + version: 1.3.0 + resolution: "any-promise@npm:1.3.0" + checksum: 10/6737469ba353b5becf29e4dc3680736b9caa06d300bda6548812a8fee63ae7d336d756f88572fa6b5219aed36698d808fa55f62af3e7e6845c7a1dc77d240edb + languageName: node + linkType: hard + "anymatch@npm:^3.0.3, anymatch@npm:^3.1.1, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" @@ -12522,7 +12615,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.2.0, chalk@npm:^5.3.0": +"chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1": version: 5.4.1 resolution: "chalk@npm:5.4.1" checksum: 10/29df3ffcdf25656fed6e95962e2ef86d14dfe03cd50e7074b06bad9ffbbf6089adbb40f75c00744d843685c8d008adaf3aed31476780312553caf07fa86e5bc7 @@ -12786,6 +12879,22 @@ __metadata: languageName: node linkType: hard +"cli-highlight@npm:^2.1.11": + version: 2.1.11 + resolution: "cli-highlight@npm:2.1.11" + dependencies: + chalk: "npm:^4.0.0" + highlight.js: "npm:^10.7.1" + mz: "npm:^2.4.0" + parse5: "npm:^5.1.1" + parse5-htmlparser2-tree-adapter: "npm:^6.0.0" + yargs: "npm:^16.0.0" + bin: + highlight: bin/highlight + checksum: 10/05d2b5beb8a4d3259f693517d013bf53d04ad20f470b77c3d02e051963092fae388388e3127f67d3679884a0c32cb855bf590292017c5e68c0f8d86f4b8e146e + languageName: node + linkType: hard + "cli-spinners@npm:2.6.1": version: 2.6.1 resolution: "cli-spinners@npm:2.6.1" @@ -12800,7 +12909,7 @@ __metadata: languageName: node linkType: hard -"cli-table3@npm:~0.6.5": +"cli-table3@npm:^0.6.3, cli-table3@npm:^0.6.5, cli-table3@npm:~0.6.5": version: 0.6.5 resolution: "cli-table3@npm:0.6.5" dependencies: @@ -13082,7 +13191,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^10.0.0": +"commander@npm:^10.0.0, commander@npm:^10.0.1": version: 10.0.1 resolution: "commander@npm:10.0.1" checksum: 10/8799faa84a30da985802e661cc9856adfaee324d4b138413013ef7f087e8d7924b144c30a1f1405475f0909f467665cd9e1ce13270a2f41b141dab0b7a58f3fb @@ -15241,6 +15350,13 @@ __metadata: languageName: node linkType: hard +"emojilib@npm:^2.4.0": + version: 2.4.0 + resolution: "emojilib@npm:2.4.0" + checksum: 10/bef767eca49acaa881388d91bee6936ea57ae367d603d5227ff0a9da3e2d1e774a61c447e5f2f4901797d023c4b5239bc208285b6172a880d3655024a0f44980 + languageName: node + linkType: hard + "emojis-list@npm:^3.0.0": version: 3.0.0 resolution: "emojis-list@npm:3.0.0" @@ -15415,6 +15531,13 @@ __metadata: languageName: node linkType: hard +"environment@npm:^1.0.0": + version: 1.1.0 + resolution: "environment@npm:1.1.0" + checksum: 10/dd3c1b9825e7f71f1e72b03c2344799ac73f2e9ef81b78ea8b373e55db021786c6b9f3858ea43a436a2c4611052670ec0afe85bc029c384cc71165feee2f4ba6 + languageName: node + linkType: hard + "eol@npm:^0.9.1": version: 0.9.1 resolution: "eol@npm:0.9.1" @@ -16700,6 +16823,13 @@ __metadata: languageName: node linkType: hard +"fflate@npm:^0.8.2": + version: 0.8.2 + resolution: "fflate@npm:0.8.2" + checksum: 10/2bd26ba6d235d428de793c6a0cd1aaa96a06269ebd4e21b46c8fd1bd136abc631acf27e188d47c3936db090bf3e1ede11d15ce9eae9bffdc4bfe1b9dc66ca9cb + languageName: node + linkType: hard + "figures@npm:3.2.0, figures@npm:^3.0.0, figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -17856,6 +17986,7 @@ __metadata: version: 0.0.0-use.local resolution: "grafana@workspace:." dependencies: + "@arethetypeswrong/cli": "npm:^0.18.2" "@babel/core": "npm:7.26.10" "@babel/preset-env": "npm:7.26.9" "@babel/runtime": "npm:7.27.0" @@ -18120,6 +18251,7 @@ __metadata: postcss-scss: "npm:4.0.9" prettier: "npm:3.4.2" prismjs: "npm:1.30.0" + publint: "npm:^0.3.12" rc-slider: "npm:11.1.8" rc-tree: "npm:5.13.1" re-resizable: "npm:6.10.3" @@ -18160,7 +18292,6 @@ __metadata: regenerator-runtime: "npm:0.14.1" reselect: "npm:5.1.1" rimraf: "npm:6.0.1" - rudder-sdk-js: "npm:2.48.44" rxjs: "npm:7.8.2" sass: "npm:1.83.4" sass-loader: "npm:16.0.4" @@ -18428,7 +18559,7 @@ __metadata: languageName: node linkType: hard -"highlight.js@npm:^10.4.1, highlight.js@npm:~10.7.0": +"highlight.js@npm:^10.4.1, highlight.js@npm:^10.7.1, highlight.js@npm:~10.7.0": version: 10.7.3 resolution: "highlight.js@npm:10.7.3" checksum: 10/db8d10a541936b058e221dbde77869664b2b45bca75d660aa98065be2cd29f3924755fbc7348213f17fd931aefb6e6597448ba6fe82afba6d8313747a91983ee @@ -21895,6 +22026,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.0.1": + version: 11.1.0 + resolution: "lru-cache@npm:11.1.0" + checksum: 10/5011011675ca98428902de774d0963b68c3a193cd959347cb63b781dad4228924124afab82159fd7b8b4db18285d9aff462b877b8f6efd2b41604f806c1d9db4 + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -22115,6 +22253,23 @@ __metadata: languageName: node linkType: hard +"marked-terminal@npm:^7.1.0": + version: 7.3.0 + resolution: "marked-terminal@npm:7.3.0" + dependencies: + ansi-escapes: "npm:^7.0.0" + ansi-regex: "npm:^6.1.0" + chalk: "npm:^5.4.1" + cli-highlight: "npm:^2.1.11" + cli-table3: "npm:^0.6.5" + node-emoji: "npm:^2.2.0" + supports-hyperlinks: "npm:^3.1.0" + peerDependencies: + marked: ">=1 <16" + checksum: 10/1dfdfe752a4ebe6aec8de4a51180612a5f29982026b104a86215efb46b82b2a1942531a6bb840163c8d827e3eadc5cf93272e6eb29ec549f72b73b8b2eb97cfe + languageName: node + linkType: hard + "marked@npm:15.0.12": version: 15.0.12 resolution: "marked@npm:15.0.12" @@ -22124,6 +22279,15 @@ __metadata: languageName: node linkType: hard +"marked@npm:^9.1.2": + version: 9.1.6 + resolution: "marked@npm:9.1.6" + bin: + marked: bin/marked.js + checksum: 10/29d073500c70b6b53cd35a8d19f5e43df6e2819ddeca8848a31901b87b82ca0ea46a8a831920c656c69c33ad5dce4b75654c4c4ced34a67f4e4e4a31c7620cfe + languageName: node + linkType: hard + "matcher-collection@npm:^2.0.0": version: 2.0.1 resolution: "matcher-collection@npm:2.0.1" @@ -22809,6 +22973,13 @@ __metadata: languageName: node linkType: hard +"mri@npm:^1.1.0": + version: 1.2.0 + resolution: "mri@npm:1.2.0" + checksum: 10/6775a1d2228bb9d191ead4efc220bd6be64f943ad3afd4dcb3b3ac8fc7b87034443f666e38805df38e8d047b29f910c3cc7810da0109af83e42c82c73bd3f6bc + languageName: node + linkType: hard + "mrmime@npm:^2.0.0": version: 2.0.0 resolution: "mrmime@npm:2.0.0" @@ -22916,6 +23087,17 @@ __metadata: languageName: node linkType: hard +"mz@npm:^2.4.0": + version: 2.7.0 + resolution: "mz@npm:2.7.0" + dependencies: + any-promise: "npm:^1.0.0" + object-assign: "npm:^4.0.1" + thenify-all: "npm:^1.0.0" + checksum: 10/8427de0ece99a07e9faed3c0c6778820d7543e3776f9a84d22cf0ec0a8eb65f6e9aee9c9d353ff9a105ff62d33a9463c6ca638974cc652ee8140cd1e35951c87 + languageName: node + linkType: hard + "nano-css@npm:^5.6.1, nano-css@npm:^5.6.2": version: 5.6.2 resolution: "nano-css@npm:5.6.2" @@ -23030,6 +23212,18 @@ __metadata: languageName: node linkType: hard +"node-emoji@npm:^2.2.0": + version: 2.2.0 + resolution: "node-emoji@npm:2.2.0" + dependencies: + "@sindresorhus/is": "npm:^4.6.0" + char-regex: "npm:^1.0.2" + emojilib: "npm:^2.4.0" + skin-tone: "npm:^2.0.0" + checksum: 10/2548668f5cc9f781c94dc39971a630b2887111e0970c29fc523e924819d1b39b53a2694a4d1046861adf538c4462d06ee0269c48717ccad30336a918d9a911d5 + languageName: node + linkType: hard + "node-ensure@npm:^0.0.0": version: 0.0.0 resolution: "node-ensure@npm:0.0.0" @@ -23579,7 +23773,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": +"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f @@ -24156,6 +24350,13 @@ __metadata: languageName: node linkType: hard +"package-manager-detector@npm:^1.1.0": + version: 1.3.0 + resolution: "package-manager-detector@npm:1.3.0" + checksum: 10/b21155d53a8ab96d5be3bfae43cc1d397bf363782b922d1f6967d220d2a9f08234ebb76035318bf92822ce761d10451959f01019faebc08fdb4d4a8bc3103da6 + languageName: node + linkType: hard + "pacote@npm:^18.0.0, pacote@npm:^18.0.6": version: 18.0.6 resolution: "pacote@npm:18.0.6" @@ -24356,6 +24557,15 @@ __metadata: languageName: node linkType: hard +"parse5-htmlparser2-tree-adapter@npm:^6.0.0": + version: 6.0.1 + resolution: "parse5-htmlparser2-tree-adapter@npm:6.0.1" + dependencies: + parse5: "npm:^6.0.1" + checksum: 10/3400a2cd1ad450b2fe148544154f86ea53d3ed6b6eab56c78bb43b9629d3dfe9f580dffd75bbf32be134ffef645b68081fc764bf75c210f236ab9c5c8c38c252 + languageName: node + linkType: hard + "parse5-htmlparser2-tree-adapter@npm:^7.0.0": version: 7.0.0 resolution: "parse5-htmlparser2-tree-adapter@npm:7.0.0" @@ -24375,6 +24585,20 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^5.1.1": + version: 5.1.1 + resolution: "parse5@npm:5.1.1" + checksum: 10/5b509744cfe81488a33be05578df490c460690e64519fa67f0a0acb9c1bca05914e8acad17a977e2cf5964a000e43959b40024f0c243dd6595dd0cca8a32f71b + languageName: node + linkType: hard + +"parse5@npm:^6.0.1": + version: 6.0.1 + resolution: "parse5@npm:6.0.1" + checksum: 10/dfb110581f62bd1425725a7c784ae022a24669bd0efc24b58c71fc731c4d868193e2ebd85b74cde2dbb965e4dcf07059b1e651adbec1b3b5267531bd132fdb75 + languageName: node + linkType: hard + "parse5@npm:^7.0.0, parse5@npm:^7.1.1, parse5@npm:^7.1.2": version: 7.1.2 resolution: "parse5@npm:7.1.2" @@ -25536,6 +25760,20 @@ __metadata: languageName: node linkType: hard +"publint@npm:^0.3.12": + version: 0.3.12 + resolution: "publint@npm:0.3.12" + dependencies: + "@publint/pack": "npm:^0.1.2" + package-manager-detector: "npm:^1.1.0" + picocolors: "npm:^1.1.1" + sade: "npm:^1.8.1" + bin: + publint: src/cli.js + checksum: 10/77153a20821b58fbe57e3d90e2ddd7c014c4a7dd4b506f8919ef0ecbb1e14745514c89be9d72623dcad014f0c11dbc0b204c05f713297c4435b695ba9f874c21 + languageName: node + linkType: hard + "pump@npm:^3.0.0": version: 3.0.0 resolution: "pump@npm:3.0.0" @@ -27732,13 +27970,6 @@ __metadata: languageName: node linkType: hard -"rudder-sdk-js@npm:2.48.44": - version: 2.48.44 - resolution: "rudder-sdk-js@npm:2.48.44" - checksum: 10/538bc405c0cb7faf33b910a39d51bd6df53fbdb176d4e83c33821e7a454178b65ced216ad0f055b6dffd201ccc45cead814138e20683e15424a02fb54b894e15 - languageName: node - linkType: hard - "run-applescript@npm:^7.0.0": version: 7.0.0 resolution: "run-applescript@npm:7.0.0" @@ -27794,6 +28025,15 @@ __metadata: languageName: node linkType: hard +"sade@npm:^1.8.1": + version: 1.8.1 + resolution: "sade@npm:1.8.1" + dependencies: + mri: "npm:^1.1.0" + checksum: 10/1c67ba03c94083e0ae307ff5564ecb86c2104c0f558042fdaa40ea0054f91a63a9783f14069870f2f784336adabb70f90f22a84dc457b5a25e859aaadefe0910 + languageName: node + linkType: hard + "safe-array-concat@npm:^1.1.3": version: 1.1.3 resolution: "safe-array-concat@npm:1.1.3" @@ -28455,6 +28695,15 @@ __metadata: languageName: node linkType: hard +"skin-tone@npm:^2.0.0": + version: 2.0.0 + resolution: "skin-tone@npm:2.0.0" + dependencies: + unicode-emoji-modifier-base: "npm:^1.0.0" + checksum: 10/19de157586b8019cacc55eb25d9d640f00fc02415761f3e41a4527142970fd4e7f6af0333bc90e879858766c20a976107bb386ffd4c812289c01d51f2c8d182c + languageName: node + linkType: hard + "slash@npm:3.0.0, slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -29946,6 +30195,24 @@ __metadata: languageName: node linkType: hard +"thenify-all@npm:^1.0.0": + version: 1.6.0 + resolution: "thenify-all@npm:1.6.0" + dependencies: + thenify: "npm:>= 3.1.0 < 4" + checksum: 10/dba7cc8a23a154cdcb6acb7f51d61511c37a6b077ec5ab5da6e8b874272015937788402fd271fdfc5f187f8cb0948e38d0a42dcc89d554d731652ab458f5343e + languageName: node + linkType: hard + +"thenify@npm:>= 3.1.0 < 4": + version: 3.3.1 + resolution: "thenify@npm:3.3.1" + dependencies: + any-promise: "npm:^1.0.0" + checksum: 10/486e1283a867440a904e36741ff1a177faa827cf94d69506f7e3ae4187b9afdf9ec368b3d8da225c192bfe2eb943f3f0080594156bf39f21b57cd1411e2e7f6d + languageName: node + linkType: hard + "throttle-debounce@npm:^3.0.1": version: 3.0.1 resolution: "throttle-debounce@npm:3.0.1" @@ -30702,6 +30969,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:5.6.1-rc": + version: 5.6.1-rc + resolution: "typescript@npm:5.6.1-rc" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/5716659d5baf142b5c84b96209b30730a5e9dcc0202f879349f9974823f7452ec4ef3904397b6d89d861c688acdbb1dad0a449d753163519fae2ee06ea4a68be + languageName: node + linkType: hard + "typescript@npm:5.7.3": version: 5.7.3 resolution: "typescript@npm:5.7.3" @@ -30732,6 +31009,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A5.6.1-rc#optional!builtin": + version: 5.6.1-rc + resolution: "typescript@patch:typescript@npm%3A5.6.1-rc#optional!builtin::version=5.6.1-rc&hash=8c6c40" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/462e0bb46c63abfc5bfc43f2bb00b9777a4228f3ed52d8930b46404dce71dbada63c27a99262ff4570b5ff7d01455701bfd36823bd3c766e443b6fa33cd31dea + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A5.7.3#optional!builtin": version: 5.7.3 resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5786d5" @@ -30825,6 +31112,13 @@ __metadata: languageName: node linkType: hard +"unicode-emoji-modifier-base@npm:^1.0.0": + version: 1.0.0 + resolution: "unicode-emoji-modifier-base@npm:1.0.0" + checksum: 10/6e1521d35fa69493207eb8b41f8edb95985d8b3faf07c01d820a1830b5e8403e20002563e2f84683e8e962a49beccae789f0879356bf92a4ec7a4dd8e2d16fdb + languageName: node + linkType: hard + "unicode-match-property-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-match-property-ecmascript@npm:2.0.0" @@ -32301,7 +32595,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^16.2.0": +"yargs@npm:^16.0.0, yargs@npm:^16.2.0": version: 16.2.0 resolution: "yargs@npm:16.2.0" dependencies: