Compare commits
1 Commits
ifrost/res
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a4e3b0f4 |
@@ -10,7 +10,7 @@ require (
|
||||
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/proto v1.13.2 // indirect
|
||||
github.com/expr-lang/expr v1.17.7 // indirect
|
||||
github.com/expr-lang/expr v1.17.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.132.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
|
||||
2
.github/workflows/cleanup-branches.yml
vendored
2
.github/workflows/cleanup-branches.yml
vendored
@@ -14,5 +14,5 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: grafana/shared-workflows/actions/cleanup-branches@cleanup-branches/v0.2.1
|
||||
with:
|
||||
dry-run: false
|
||||
dry-run: true
|
||||
max-date: "1 month ago"
|
||||
|
||||
41
.github/workflows/release-npm.yml
vendored
41
.github/workflows/release-npm.yml
vendored
@@ -90,7 +90,6 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.grafana_commit }}
|
||||
fetch-depth: 2 # Need HEAD~1 for e2e-selectors change detection
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
@@ -128,43 +127,3 @@ jobs:
|
||||
env:
|
||||
NPM_TAG: ${{ steps.npm-tag.outputs.NPM_TAG }}
|
||||
run: ./scripts/publish-npm-packages.sh --dist-tag "$NPM_TAG" --registry 'https://registry.npmjs.org/'
|
||||
|
||||
# Notify plugin-tools when e2e-selectors changes so it can update its bundled version
|
||||
- name: Check for e2e-selectors changes
|
||||
id: check-e2e-changes
|
||||
run: |
|
||||
CHANGES=$(git diff --name-only HEAD~1 HEAD -- packages/grafana-e2e-selectors | wc -l)
|
||||
echo "changes=$CHANGES" >> "$GITHUB_OUTPUT"
|
||||
if [ "$CHANGES" -gt 0 ]; then
|
||||
echo "Detected $CHANGES file(s) changed in packages/grafana-e2e-selectors"
|
||||
fi
|
||||
|
||||
- name: Get Vault secrets
|
||||
if: steps.check-e2e-changes.outputs.changes > 0
|
||||
id: vault-secrets
|
||||
uses: grafana/shared-workflows/actions/get-vault-secrets@main
|
||||
with:
|
||||
repo_secrets: |
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM=delivery-bot-app:PRIVATE_KEY
|
||||
|
||||
- name: Generate token for plugin-tools
|
||||
if: steps.check-e2e-changes.outputs.changes > 0
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
|
||||
with:
|
||||
app_id: ${{ vars.DELIVERY_BOT_APP_ID }}
|
||||
private_key: ${{ env.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
repositories: '["plugin-tools"]'
|
||||
permissions: '{"actions": "write"}'
|
||||
|
||||
- name: Dispatch to plugin-tools
|
||||
if: steps.check-e2e-changes.outputs.changes > 0
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
echo "Dispatching bump-e2e-selectors workflow to grafana/plugin-tools with version $VERSION"
|
||||
gh workflow run bump-e2e-selectors.yml \
|
||||
--repo grafana/plugin-tools \
|
||||
--ref main \
|
||||
--field version="$VERSION"
|
||||
|
||||
@@ -2287,8 +2287,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kms v0.34.3 h1:QzBOD0sk1bGQVMcZQAHGjtbP1iKZJUyhC6D0I+BTxIE=
|
||||
k8s.io/kms v0.34.3/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
|
||||
k8s.io/kube-aggregator v0.34.3 h1:rKsZWTD2As4dKuv+zzdJU0uo5H7bFlAEoSucai4mW6M=
|
||||
k8s.io/kube-aggregator v0.34.3/go.mod h1:d4D8PV2FK4Qlq6u442FSum1tHPhK9tKdKBfH/A3R0I0=
|
||||
k8s.io/kube-aggregator v0.34.2 h1:Nn0Vksj67WHBL2x7bJ6vuxL44RbMTK6uRtXX+3vMVJk=
|
||||
k8s.io/kube-aggregator v0.34.2/go.mod h1:/tp4cc/1p2AvICsS4mjjSJakdrbhcGbRmj0mdHTdR2Q=
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
|
||||
@@ -261,7 +261,18 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
|
||||
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
|
||||
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
|
||||
k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo=
|
||||
k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE=
|
||||
k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
|
||||
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
|
||||
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
|
||||
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
|
||||
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
|
||||
@@ -763,6 +763,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Select/resetSelectStyles.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Select/types.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 6
|
||||
|
||||
4
go.mod
4
go.mod
@@ -133,7 +133,7 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // @grafana/grafana-backend-group
|
||||
github.com/lib/pq v1.10.9 // @grafana/grafana-backend-group
|
||||
github.com/m3db/prometheus_remote_client_golang v0.4.4 // @grafana/grafana-backend-group
|
||||
github.com/madflojo/testcerts v1.4.0 // @grafana/alerting-backend
|
||||
github.com/madflojo/testcerts v1.5.0 // @grafana/alerting-backend
|
||||
github.com/mattn/go-isatty v0.0.20 // @grafana/grafana-backend-group
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // @grafana/grafana-backend-group
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-backend
|
||||
@@ -221,7 +221,7 @@ require (
|
||||
k8s.io/client-go v0.34.3 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/component-base v0.34.3 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/klog/v2 v2.130.1 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/kube-aggregator v0.34.3 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/kube-aggregator v0.34.2 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // @grafana/grafana-app-platform-squad
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // @grafana/partner-datasources
|
||||
modernc.org/sqlite v1.40.1 // @grafana/grafana-backend-group
|
||||
|
||||
8
go.sum
8
go.sum
@@ -1974,8 +1974,8 @@ github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuz
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
|
||||
github.com/m3db/prometheus_remote_client_golang v0.4.4 h1:DsAIjVKoCp7Ym35tAOFL1OuMLIdIikAEHeNPHY+yyM8=
|
||||
github.com/m3db/prometheus_remote_client_golang v0.4.4/go.mod h1:wHfVbA3eAK6dQvKjCkHhusWYegCk3bDGkA15zymSHdc=
|
||||
github.com/madflojo/testcerts v1.4.0 h1:I09gN0C1ly9IgeVNcAqKk8RAKIJTe3QnFrrPBDyvzN4=
|
||||
github.com/madflojo/testcerts v1.4.0/go.mod h1:MW8sh39gLnkKh4K0Nc55AyHEDl9l/FBLDUsQhpmkuo0=
|
||||
github.com/madflojo/testcerts v1.5.0 h1:GhQllyAiGzXVZU+i8O/cQkPTHzN59RxMGtm3uETgXnU=
|
||||
github.com/madflojo/testcerts v1.5.0/go.mod h1:MW8sh39gLnkKh4K0Nc55AyHEDl9l/FBLDUsQhpmkuo0=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
@@ -3694,8 +3694,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kms v0.34.3 h1:QzBOD0sk1bGQVMcZQAHGjtbP1iKZJUyhC6D0I+BTxIE=
|
||||
k8s.io/kms v0.34.3/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
|
||||
k8s.io/kube-aggregator v0.34.3 h1:rKsZWTD2As4dKuv+zzdJU0uo5H7bFlAEoSucai4mW6M=
|
||||
k8s.io/kube-aggregator v0.34.3/go.mod h1:d4D8PV2FK4Qlq6u442FSum1tHPhK9tKdKBfH/A3R0I0=
|
||||
k8s.io/kube-aggregator v0.34.2 h1:Nn0Vksj67WHBL2x7bJ6vuxL44RbMTK6uRtXX+3vMVJk=
|
||||
k8s.io/kube-aggregator v0.34.2/go.mod h1:/tp4cc/1p2AvICsS4mjjSJakdrbhcGbRmj0mdHTdR2Q=
|
||||
k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4=
|
||||
k8s.io/kube-openapi v0.0.0-20190722073852-5e22f3d471e6/go.mod h1:RZvgC8MSN6DjiMV6oIfEE9pDL9CYXokkfaCKZeHm3nc=
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
|
||||
|
||||
@@ -2198,6 +2198,10 @@ k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU=
|
||||
k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
|
||||
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc=
|
||||
k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg=
|
||||
k8s.io/code-generator v0.34.2 h1:9bG6jTxmsU3HXE5BNYJTC8AZ1D6hVVfkm8yYSkdkGY0=
|
||||
k8s.io/code-generator v0.34.2/go.mod h1:dnDDEd6S/z4uZ+PG1aE58ySCi/lR4+qT3a4DddE4/2I=
|
||||
k8s.io/code-generator v0.34.3 h1:6ipJKsJZZ9q21BO8I2jEj4OLN3y8/1n4aihKN0xKmQk=
|
||||
k8s.io/code-generator v0.34.3/go.mod h1:oW73UPYpGLsbRN8Ozkhd6ZzkF8hzFCiYmvEuWZDroI4=
|
||||
k8s.io/component-base v0.26.2/go.mod h1:DxbuIe9M3IZPRxPIzhch2m1eT7uFrSBJUBuVCQEBivs=
|
||||
|
||||
@@ -2,7 +2,7 @@ module github.com/grafana/grafana/hack
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require k8s.io/code-generator v0.34.3
|
||||
require k8s.io/code-generator v0.34.2
|
||||
|
||||
require (
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
|
||||
@@ -12,8 +12,8 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
k8s.io/code-generator v0.34.3 h1:6ipJKsJZZ9q21BO8I2jEj4OLN3y8/1n4aihKN0xKmQk=
|
||||
k8s.io/code-generator v0.34.3/go.mod h1:oW73UPYpGLsbRN8Ozkhd6ZzkF8hzFCiYmvEuWZDroI4=
|
||||
k8s.io/code-generator v0.34.2 h1:9bG6jTxmsU3HXE5BNYJTC8AZ1D6hVVfkm8yYSkdkGY0=
|
||||
k8s.io/code-generator v0.34.2/go.mod h1:dnDDEd6S/z4uZ+PG1aE58ySCi/lR4+qT3a4DddE4/2I=
|
||||
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q=
|
||||
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
|
||||
@@ -11,7 +11,7 @@ set -o pipefail
|
||||
|
||||
SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
|
||||
pushd "${SCRIPT_ROOT}/hack" && GO111MODULE=on go mod tidy && popd
|
||||
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $(go env GOPATH)/pkg/mod/k8s.io/code-generator@v0.34.3)}
|
||||
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $(go env GOPATH)/pkg/mod/k8s.io/code-generator@v0.34.2)}
|
||||
|
||||
OUTDIR="${HOME}/go/src"
|
||||
OPENAPI_VIOLATION_EXCEPTIONS_FILENAME="zz_generated.openapi_violation_exceptions.list"
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"@emotion/eslint-plugin": "11.12.0",
|
||||
"@grafana/eslint-config": "8.2.0",
|
||||
"@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules",
|
||||
"@grafana/plugin-e2e": "^3.1.0",
|
||||
"@grafana/plugin-e2e": "^3.0.3",
|
||||
"@grafana/test-utils": "workspace:*",
|
||||
"@manypkg/get-packages": "^3.0.0",
|
||||
"@npmcli/package-json": "^6.0.0",
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('Resolver', () => {
|
||||
expect(pages.Alerting.AddAlertRule.url).toBe('/alerting/new');
|
||||
});
|
||||
|
||||
it('should throw an error if an invalid semver range is used in versioned selector', () => {
|
||||
it('should throw error if an invalid semver range is used in versioned selector', () => {
|
||||
expect(() =>
|
||||
resolveSelectors({
|
||||
Alerting: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { type ComponentProps, useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { createDataFrame } from '@grafana/data';
|
||||
|
||||
@@ -16,14 +16,14 @@ jest.mock('react-use', () => {
|
||||
return {
|
||||
...reactUse,
|
||||
useMeasure: () => {
|
||||
const ref = useRef(null);
|
||||
const ref = React.useRef();
|
||||
return [ref, { width: 1600 }];
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('FlameGraph', () => {
|
||||
function setup(props?: Partial<ComponentProps<typeof FlameGraph>>) {
|
||||
function setup(props?: Partial<React.ComponentProps<typeof FlameGraph>>) {
|
||||
const flameGraphData = createDataFrame(data);
|
||||
const container = new FlameGraphDataContainer(flameGraphData, { collapsing: true });
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jest.mock('@grafana/assistant', () => ({
|
||||
jest.mock('react-use', () => ({
|
||||
...jest.requireActual('react-use'),
|
||||
useMeasure: () => {
|
||||
const ref = useRef(null);
|
||||
const ref = useRef();
|
||||
return [ref, { width: 1600 }];
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -262,18 +262,24 @@ function createComponent<Props extends JSX.IntrinsicAttributes>(
|
||||
pluginId?: string,
|
||||
id?: string
|
||||
): ComponentTypeWithExtensionMeta<Props> {
|
||||
const ComponentWithMeta: ComponentTypeWithExtensionMeta<Props> = Object.assign(
|
||||
Implementation || (() => <div>Test</div>),
|
||||
{
|
||||
meta: {
|
||||
id: id ?? '',
|
||||
pluginId: pluginId ?? '',
|
||||
title: '',
|
||||
description: '',
|
||||
type: PluginExtensionTypes.component,
|
||||
} satisfies PluginExtensionComponentMeta,
|
||||
function ComponentWithMeta(props: Props) {
|
||||
if (Implementation) {
|
||||
return <Implementation {...props} />;
|
||||
}
|
||||
);
|
||||
|
||||
return <div>Test</div>;
|
||||
}
|
||||
|
||||
ComponentWithMeta.displayName = '';
|
||||
ComponentWithMeta.propTypes = {};
|
||||
ComponentWithMeta.contextTypes = {};
|
||||
ComponentWithMeta.meta = {
|
||||
id: id ?? '',
|
||||
pluginId: pluginId ?? '',
|
||||
title: '',
|
||||
description: '',
|
||||
type: PluginExtensionTypes.component,
|
||||
} satisfies PluginExtensionComponentMeta;
|
||||
|
||||
return ComponentWithMeta;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ export interface PanelContext {
|
||||
onSeriesColorChange?: (label: string, color: string) => void;
|
||||
|
||||
onToggleSeriesVisibility?: (label: string, mode: SeriesVisibilityChangeMode) => void;
|
||||
onResetAllSeriesVisibility?: () => void;
|
||||
|
||||
canAddAnnotations?: () => boolean;
|
||||
canEditAnnotations?: (dashboardUID?: string) => boolean;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
export enum SeriesVisibilityChangeMode {
|
||||
ToggleSelection = 'select',
|
||||
AppendToSelection = 'append',
|
||||
Show = 'show',
|
||||
}
|
||||
|
||||
export type OnSelectRangeCallback = (selections: RangeSelection2D[]) => void;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { StylesConfig } from 'react-select';
|
||||
import { CSSObjectWithLabel } from 'react-select';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
export default function resetSelectStyles(theme: GrafanaTheme2): Partial<StylesConfig> {
|
||||
export default function resetSelectStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
clearIndicator: () => ({}),
|
||||
container: () => ({}),
|
||||
@@ -13,7 +13,7 @@ export default function resetSelectStyles(theme: GrafanaTheme2): Partial<StylesC
|
||||
groupHeading: () => ({}),
|
||||
indicatorsContainer: () => ({}),
|
||||
indicatorSeparator: () => ({}),
|
||||
input: function (originalStyles) {
|
||||
input: function (originalStyles: CSSObjectWithLabel) {
|
||||
return {
|
||||
...originalStyles,
|
||||
color: 'inherit',
|
||||
@@ -27,7 +27,7 @@ export default function resetSelectStyles(theme: GrafanaTheme2): Partial<StylesC
|
||||
loadingIndicator: () => ({}),
|
||||
loadingMessage: () => ({}),
|
||||
menu: () => ({}),
|
||||
menuList: ({ maxHeight }) => ({
|
||||
menuList: ({ maxHeight }: { maxHeight: number }) => ({
|
||||
maxHeight,
|
||||
}),
|
||||
multiValue: () => ({}),
|
||||
@@ -38,7 +38,7 @@ export default function resetSelectStyles(theme: GrafanaTheme2): Partial<StylesC
|
||||
multiValueRemove: () => ({}),
|
||||
noOptionsMessage: () => ({}),
|
||||
option: () => ({}),
|
||||
placeholder: (originalStyles) => ({
|
||||
placeholder: (originalStyles: CSSObjectWithLabel) => ({
|
||||
...originalStyles,
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
@@ -47,11 +47,11 @@ export default function resetSelectStyles(theme: GrafanaTheme2): Partial<StylesC
|
||||
};
|
||||
}
|
||||
|
||||
export function useCustomSelectStyles(theme: GrafanaTheme2, width: number | string | undefined): Partial<StylesConfig> {
|
||||
export function useCustomSelectStyles(theme: GrafanaTheme2, width: number | string | undefined) {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...resetSelectStyles(theme),
|
||||
menuPortal: (base) => {
|
||||
menuPortal: (base: CSSObjectWithLabel) => {
|
||||
// Would like to correct top position when menu is placed bottom, but have props are not sent to this style function.
|
||||
// Only state is. https://github.com/JedWatson/react-select/blob/master/packages/react-select/src/components/Menu.tsx#L605
|
||||
return {
|
||||
@@ -60,7 +60,7 @@ export function useCustomSelectStyles(theme: GrafanaTheme2, width: number | stri
|
||||
};
|
||||
},
|
||||
//These are required for the menu positioning to function
|
||||
menu: ({ top, bottom, position }) => {
|
||||
menu: ({ top, bottom, position }: CSSObjectWithLabel) => {
|
||||
return {
|
||||
top,
|
||||
bottom,
|
||||
@@ -73,7 +73,7 @@ export function useCustomSelectStyles(theme: GrafanaTheme2, width: number | stri
|
||||
width: width ? theme.spacing(width) : '100%',
|
||||
display: width === 'auto' ? 'inline-flex' : 'flex',
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
option: (provided: CSSObjectWithLabel, state: any) => ({
|
||||
...provided,
|
||||
opacity: state.isDisabled ? 0.5 : 1,
|
||||
}),
|
||||
|
||||
@@ -263,16 +263,7 @@ export const Footer: StoryFn<typeof Table> = (args) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Pagination: StoryFn<typeof Table> = (args) => {
|
||||
const theme = useTheme2();
|
||||
const data = buildData(theme, {});
|
||||
|
||||
return (
|
||||
<DashboardStoryCanvas>
|
||||
<Table {...args} data={data} />
|
||||
</DashboardStoryCanvas>
|
||||
);
|
||||
};
|
||||
export const Pagination: StoryFn<typeof Table> = (args) => <Basic {...args} />;
|
||||
Pagination.args = {
|
||||
enablePagination: true,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DataHoverClearEvent, DataHoverEvent } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { LegendDisplayMode } from '@grafana/schema';
|
||||
|
||||
import { Button } from '../Button/Button';
|
||||
import { SeriesVisibilityChangeMode, usePanelContext } from '../PanelChrome';
|
||||
|
||||
import { VizLegendList } from './VizLegendList';
|
||||
@@ -35,7 +32,7 @@ export function VizLegend<T>({
|
||||
readonly,
|
||||
isSortable,
|
||||
}: LegendProps<T>) {
|
||||
const { eventBus, onResetAllSeriesVisibility, onToggleSeriesVisibility, onToggleLegendSort } = usePanelContext();
|
||||
const { eventBus, onToggleSeriesVisibility, onToggleLegendSort } = usePanelContext();
|
||||
|
||||
const onMouseOver = useCallback(
|
||||
(
|
||||
@@ -108,26 +105,6 @@ export function VizLegend<T>({
|
||||
[className, placement, onMouseOver, onMouseOut, onLegendLabelClick, itemRenderer, readonly]
|
||||
);
|
||||
|
||||
if (onResetAllSeriesVisibility && items.every((item) => (item.fieldName ?? item.label) && item.disabled)) {
|
||||
return (
|
||||
<div className={css({ paddingTop: '0.5em' })}>
|
||||
<Button
|
||||
size="sm"
|
||||
tooltip={t(
|
||||
'grafana-ui.viz-legend.show-all-series-tooltip',
|
||||
'Currently loaded series are hidden by previous selection. Click to show all series.'
|
||||
)}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
onResetAllSeriesVisibility();
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="grafana-ui.viz-legend.show-all-series">Show all series</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (displayMode) {
|
||||
case LegendDisplayMode.Table:
|
||||
return (
|
||||
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/dave/jennifer v1.7.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/proto v1.14.2 // indirect
|
||||
github.com/expr-lang/expr v1.17.7 // indirect
|
||||
github.com/expr-lang/expr v1.17.6 // indirect
|
||||
github.com/getkin/kin-openapi v0.133.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
|
||||
@@ -11,8 +11,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I=
|
||||
github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
|
||||
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
|
||||
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
|
||||
@@ -224,7 +224,7 @@ var (
|
||||
MStatTotalRepositories prometheus.Gauge
|
||||
|
||||
// MUnifiedStorageMigrationStatus indicates the migration status for unified storage in this instance.
|
||||
// Possible values: 0 (default/undefined), 1 (migration disabled), 2 (migration would run), 3 (migration will run).
|
||||
// Possible values: 0 (default/undefined), 1 (migration disabled), 2 (migration would run).
|
||||
MUnifiedStorageMigrationStatus prometheus.Gauge
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
|
||||
github.com/dave/dst v0.27.3 // indirect
|
||||
github.com/emicklei/proto v1.14.2 // indirect
|
||||
github.com/expr-lang/expr v1.17.7 // indirect
|
||||
github.com/expr-lang/expr v1.17.6 // indirect
|
||||
github.com/getkin/kin-openapi v0.133.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
|
||||
@@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I=
|
||||
github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
|
||||
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
|
||||
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
|
||||
@@ -154,6 +154,12 @@ func (a *dashboardSqlAccess) executeQuery(ctx context.Context, helper *legacysql
|
||||
return nil
|
||||
})
|
||||
|
||||
// Use transaction from unified storage if available in the context.
|
||||
// This allows us to run migrations in a transaction which is specifically required for SQLite.
|
||||
if tx == nil {
|
||||
tx = resource.TransactionFromContext(ctx)
|
||||
}
|
||||
|
||||
if tx != nil {
|
||||
return tx.QueryContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
@@ -632,9 +632,6 @@ type UnifiedStorageConfig struct {
|
||||
DataSyncerInterval time.Duration
|
||||
// DataSyncerRecordsLimit defines how many records will be processed at max during a sync invocation.
|
||||
DataSyncerRecordsLimit int
|
||||
// EnableMigration indicates whether migration is enabled for the resource.
|
||||
// If not set, will use the default from MigratedUnifiedResources.
|
||||
EnableMigration bool
|
||||
}
|
||||
|
||||
type InstallPlugin struct {
|
||||
|
||||
@@ -8,17 +8,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util/osutil"
|
||||
)
|
||||
|
||||
const (
|
||||
PlaylistResource = "playlists.playlist.grafana.app"
|
||||
FolderResource = "folders.folder.grafana.app"
|
||||
DashboardResource = "dashboards.dashboard.grafana.app"
|
||||
)
|
||||
|
||||
// MigratedUnifiedResources maps resources to a boolean indicating if migration is enabled by default
|
||||
var MigratedUnifiedResources = map[string]bool{
|
||||
PlaylistResource: true, // enabled by default
|
||||
FolderResource: false,
|
||||
DashboardResource: false,
|
||||
var MigratedUnifiedResources = []string{
|
||||
"playlists.playlist.grafana.app",
|
||||
"folders.folder.grafana.app",
|
||||
"dashboards.dashboard.grafana.app",
|
||||
}
|
||||
|
||||
// read storage configs from ini file. They look like:
|
||||
@@ -53,19 +46,12 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
|
||||
// parse dataSyncerInterval from resource section
|
||||
dataSyncerInterval := section.Key("dataSyncerInterval").MustDuration(time.Hour)
|
||||
|
||||
// parse EnableMigration from resource section
|
||||
enableMigration := MigratedUnifiedResources[resourceName]
|
||||
if section.HasKey("enableMigration") {
|
||||
enableMigration = section.Key("enableMigration").MustBool(MigratedUnifiedResources[resourceName])
|
||||
}
|
||||
|
||||
storageConfig[resourceName] = UnifiedStorageConfig{
|
||||
DualWriterMode: rest.DualWriterMode(dualWriterMode),
|
||||
DualWriterPeriodicDataSyncJobEnabled: dualWriterPeriodicDataSyncJobEnabled,
|
||||
DualWriterMigrationDataSyncDisabled: dualWriterMigrationDataSyncDisabled,
|
||||
DataSyncerRecordsLimit: dataSyncerRecordsLimit,
|
||||
DataSyncerInterval: dataSyncerInterval,
|
||||
EnableMigration: enableMigration,
|
||||
}
|
||||
}
|
||||
cfg.UnifiedStorage = storageConfig
|
||||
@@ -75,8 +61,8 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
|
||||
cfg.DisableDataMigrations = section.Key("disable_data_migrations").MustBool(false)
|
||||
if !cfg.DisableDataMigrations && cfg.getUnifiedStorageType() == "unified" {
|
||||
// Helper log to find instances running migrations in the future
|
||||
cfg.Logger.Info("Unified migration configs enforced")
|
||||
cfg.enforceMigrationToUnifiedConfigs()
|
||||
cfg.Logger.Info("Unified migration configs not yet enforced")
|
||||
// cfg.enforceMigrationToUnifiedConfigs() // TODO: uncomment when ready for release
|
||||
} else {
|
||||
// Helper log to find instances disabling migration
|
||||
cfg.Logger.Info("Unified migration configs enforcement disabled", "storage_type", cfg.getUnifiedStorageType(), "disable_data_migrations", cfg.DisableDataMigrations)
|
||||
@@ -121,6 +107,7 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
|
||||
cfg.MinFileIndexBuildVersion = section.Key("min_file_index_build_version").MustString("")
|
||||
}
|
||||
|
||||
// nolint:unused
|
||||
// enforceMigrationToUnifiedConfigs enforces configurations required to run migrated resources in mode 5
|
||||
// All migrated resources in MigratedUnifiedResources are set to mode 5 and unified search is enabled
|
||||
func (cfg *Cfg) enforceMigrationToUnifiedConfigs() {
|
||||
@@ -131,22 +118,14 @@ func (cfg *Cfg) enforceMigrationToUnifiedConfigs() {
|
||||
section.Key("enable_search").SetValue("true")
|
||||
cfg.EnableSearch = true
|
||||
}
|
||||
for resource, enabledByDefault := range MigratedUnifiedResources {
|
||||
resourceCfg, ok := cfg.UnifiedStorage[resource]
|
||||
if ok {
|
||||
if !resourceCfg.EnableMigration {
|
||||
cfg.Logger.Info("Resource migration disabled", "resource", resource)
|
||||
continue
|
||||
}
|
||||
cfg.Logger.Info("Overriding unified storage config for migrated resource", "resource", resource, "old_config", resourceCfg)
|
||||
} else if !enabledByDefault {
|
||||
continue
|
||||
}
|
||||
for _, resource := range MigratedUnifiedResources {
|
||||
cfg.Logger.Info("Enforcing mode 5 for resource in unified storage", "resource", resource)
|
||||
if oldCfg, ok := cfg.UnifiedStorage[resource]; ok {
|
||||
cfg.Logger.Info("Overriding unified storage config for migrated resource", "resource", resource, "old_config", oldCfg)
|
||||
}
|
||||
cfg.UnifiedStorage[resource] = UnifiedStorageConfig{
|
||||
DualWriterMode: 5,
|
||||
DualWriterMigrationDataSyncDisabled: true,
|
||||
EnableMigration: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -14,56 +13,31 @@ func TestCfg_setUnifiedStorageConfig(t *testing.T) {
|
||||
err := cfg.Load(CommandLineArgs{HomePath: "../../", Config: "../../conf/defaults.ini"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
setSectionKey := func(sectionName, key, value string) {
|
||||
section := cfg.Raw.Section(sectionName) // Gets existing or creates new
|
||||
_, err := section.NewKey(key, value)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
s, err := cfg.Raw.NewSection("unified_storage.playlists.playlist.grafana.app")
|
||||
assert.NoError(t, err)
|
||||
|
||||
setMigratedResourceKey := func(key, value string) {
|
||||
for migratedResource := range MigratedUnifiedResources {
|
||||
setSectionKey("unified_storage."+migratedResource, key, value)
|
||||
}
|
||||
}
|
||||
_, err = s.NewKey("dualWriterMode", "2")
|
||||
assert.NoError(t, err)
|
||||
|
||||
validateMigratedResources := func(optIn bool) {
|
||||
for migratedResource, enabled := range MigratedUnifiedResources {
|
||||
resourceCfg, exists := cfg.UnifiedStorage[migratedResource]
|
||||
_, err = s.NewKey("dualWriterPeriodicDataSyncJobEnabled", "true")
|
||||
assert.NoError(t, err)
|
||||
|
||||
isEnabled := enabled
|
||||
if optIn {
|
||||
isEnabled = true
|
||||
}
|
||||
_, err = s.NewKey("dataSyncerRecordsLimit", "1001")
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !isEnabled {
|
||||
if exists {
|
||||
assert.Equal(t, rest.DualWriterMode(1), resourceCfg.DualWriterMode, migratedResource)
|
||||
}
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, exists, true, migratedResource)
|
||||
|
||||
assert.Equal(t, UnifiedStorageConfig{
|
||||
DualWriterMode: 5,
|
||||
DualWriterMigrationDataSyncDisabled: true,
|
||||
EnableMigration: isEnabled,
|
||||
}, resourceCfg, migratedResource)
|
||||
}
|
||||
}
|
||||
|
||||
setMigratedResourceKey("dualWriterMode", "1") // migrated resources enabled by default will change to 5 in setUnifiedStorageConfig
|
||||
|
||||
setSectionKey("unified_storage.resource.not_migrated.grafana.app", "dualWriterMode", "2")
|
||||
setSectionKey("unified_storage.resource.not_migrated.grafana.app", "dualWriterPeriodicDataSyncJobEnabled", "true")
|
||||
setSectionKey("unified_storage.resource.not_migrated.grafana.app", "dataSyncerRecordsLimit", "1001")
|
||||
setSectionKey("unified_storage.resource.not_migrated.grafana.app", "dataSyncerInterval", "10m")
|
||||
_, err = s.NewKey("dataSyncerInterval", "10m")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Add unified_storage section for index settings
|
||||
setSectionKey("unified_storage", "index_min_count", "5")
|
||||
unifiedStorageSection, err := cfg.Raw.NewSection("unified_storage")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = unifiedStorageSection.NewKey("index_min_count", "5")
|
||||
assert.NoError(t, err)
|
||||
|
||||
cfg.setUnifiedStorageConfig()
|
||||
|
||||
value, exists := cfg.UnifiedStorage["resource.not_migrated.grafana.app"]
|
||||
value, exists := cfg.UnifiedStorage["playlists.playlist.grafana.app"]
|
||||
|
||||
assert.Equal(t, exists, true)
|
||||
assert.Equal(t, value, UnifiedStorageConfig{
|
||||
@@ -73,14 +47,6 @@ func TestCfg_setUnifiedStorageConfig(t *testing.T) {
|
||||
DataSyncerInterval: time.Minute * 10,
|
||||
})
|
||||
|
||||
validateMigratedResources(false)
|
||||
|
||||
setMigratedResourceKey("enableMigration", "true") // will be changed to 5 in setUnifiedStorageConfig
|
||||
|
||||
cfg.setUnifiedStorageConfig()
|
||||
|
||||
validateMigratedResources(true)
|
||||
|
||||
// Test that index settings are correctly parsed
|
||||
assert.Equal(t, 5, cfg.IndexMinCount)
|
||||
})
|
||||
|
||||
@@ -162,49 +162,14 @@ func runMigrationTestSuite(t *testing.T, testCases []resourceMigratorTestCase) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Step 3: verify that opted-out resources are not migrated", func(t *testing.T) {
|
||||
// Build unified storage config for Mode5
|
||||
unifiedConfig := make(map[string]setting.UnifiedStorageConfig)
|
||||
for _, tc := range testCases {
|
||||
for _, gvr := range tc.resources() {
|
||||
resourceKey := fmt.Sprintf("%s.%s", gvr.Resource, gvr.Group)
|
||||
unifiedConfig[resourceKey] = setting.UnifiedStorageConfig{
|
||||
DualWriterMode: grafanarest.Mode5,
|
||||
EnableMigration: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
helper := apis.NewK8sTestHelperWithOpts(t, apis.K8sTestHelperOpts{
|
||||
GrafanaOpts: testinfra.GrafanaOpts{
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
DisableDBCleanup: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: unifiedConfig,
|
||||
},
|
||||
Org1Users: org1,
|
||||
OrgBUsers: orgB,
|
||||
})
|
||||
t.Cleanup(helper.Shutdown)
|
||||
|
||||
for _, state := range testStates {
|
||||
t.Run(state.tc.name(), func(t *testing.T) {
|
||||
// Verify resources don't exist in unified storage yet
|
||||
state.tc.verify(t, helper, false)
|
||||
})
|
||||
}
|
||||
verifyRegisteredMigrations(t, helper, false, true)
|
||||
})
|
||||
|
||||
t.Run("Step 4: verify data is migrated to unified storage", func(t *testing.T) {
|
||||
// Migrations enabled by default will run automatically at startup and mode 5 is enforced by the config
|
||||
t.Run("Step 3: verify data is migrated to unified storage", func(t *testing.T) {
|
||||
// Migrations will run automatically at startup and mode 5 is enforced by the config
|
||||
helper := apis.NewK8sTestHelperWithOpts(t, apis.K8sTestHelperOpts{
|
||||
GrafanaOpts: testinfra.GrafanaOpts{
|
||||
// EnableLog: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
DisableDataMigrations: false, // Run migrations at startup
|
||||
DisableDBCleanup: true,
|
||||
APIServerStorageType: "unified",
|
||||
},
|
||||
Org1Users: org1,
|
||||
@@ -218,89 +183,7 @@ func runMigrationTestSuite(t *testing.T, testCases []resourceMigratorTestCase) {
|
||||
state.tc.verify(t, helper, true)
|
||||
})
|
||||
}
|
||||
|
||||
t.Logf("Verifying migrations are correctly registered")
|
||||
verifyRegisteredMigrations(t, helper, true, false)
|
||||
})
|
||||
|
||||
t.Run("Step 5: verify data is migrated for all migrations", func(t *testing.T) {
|
||||
// Trigger migrations that are not enabled by default
|
||||
unifiedConfig := make(map[string]setting.UnifiedStorageConfig)
|
||||
for _, tc := range testCases {
|
||||
for _, gvr := range tc.resources() {
|
||||
resourceKey := fmt.Sprintf("%s.%s", gvr.Resource, gvr.Group)
|
||||
unifiedConfig[resourceKey] = setting.UnifiedStorageConfig{
|
||||
EnableMigration: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
helper := apis.NewK8sTestHelperWithOpts(t, apis.K8sTestHelperOpts{
|
||||
GrafanaOpts: testinfra.GrafanaOpts{
|
||||
// EnableLog: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
DisableDataMigrations: false,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: unifiedConfig,
|
||||
},
|
||||
Org1Users: org1,
|
||||
OrgBUsers: orgB,
|
||||
})
|
||||
t.Cleanup(helper.Shutdown)
|
||||
|
||||
for _, state := range testStates {
|
||||
t.Run(state.tc.name(), func(t *testing.T) {
|
||||
// Verify resources still exist in unified storage after restart
|
||||
state.tc.verify(t, helper, true)
|
||||
})
|
||||
}
|
||||
|
||||
t.Logf("Verifying migrations are correctly registered")
|
||||
verifyRegisteredMigrations(t, helper, false, false)
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
migrationScope = "unifiedstorage"
|
||||
migrationTable = migrationScope + "_migration_log"
|
||||
|
||||
playlistsID = "playlists migration"
|
||||
foldersAndDashboardsID = "folders and dashboards migration"
|
||||
)
|
||||
|
||||
var migrationIDsToDefault = map[string]bool{
|
||||
playlistsID: true,
|
||||
foldersAndDashboardsID: false,
|
||||
}
|
||||
|
||||
func verifyRegisteredMigrations(t *testing.T, helper *apis.K8sTestHelper, onlyDefault bool, optOut bool) {
|
||||
getMigrationsQuery := fmt.Sprintf("SELECT migration_id FROM %s", migrationTable)
|
||||
createTableMigrationID := fmt.Sprintf("create %s table", migrationTable)
|
||||
expectedMigrationIDs := []string{createTableMigrationID}
|
||||
for id, enabled := range migrationIDsToDefault {
|
||||
if onlyDefault && !enabled {
|
||||
continue
|
||||
}
|
||||
if optOut {
|
||||
continue
|
||||
}
|
||||
expectedMigrationIDs = append(expectedMigrationIDs, id)
|
||||
}
|
||||
rows, err := helper.GetEnv().SQLStore.GetEngine().DB().Query(getMigrationsQuery)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, rows.Close())
|
||||
}()
|
||||
|
||||
migrationIDs := make(map[string]struct{})
|
||||
for rows.Next() {
|
||||
var migrationID string
|
||||
require.NoError(t, rows.Scan(&migrationID))
|
||||
require.Contains(t, expectedMigrationIDs, migrationID)
|
||||
migrationIDs[migrationID] = struct{}{}
|
||||
}
|
||||
require.NoError(t, rows.Err())
|
||||
require.Len(t, migrationIDs, len(expectedMigrationIDs))
|
||||
}
|
||||
|
||||
// verifyResourceCount verifies that the expected number of resources exist in K8s storage
|
||||
|
||||
@@ -7,9 +7,7 @@ import (
|
||||
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
playlists "github.com/grafana/grafana/apps/playlist/pkg/apis/playlist/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
|
||||
sqlstoremigrator "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
@@ -19,13 +17,7 @@ type ResourceDefinition struct {
|
||||
MigratorFunc string // Name of the method: "MigrateFolders", "MigrateDashboards", etc.
|
||||
}
|
||||
|
||||
type migrationDefinition struct {
|
||||
name string
|
||||
resources []string
|
||||
registerFunc func(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient)
|
||||
}
|
||||
|
||||
var resourceRegistry = []ResourceDefinition{
|
||||
var registeredResources = []ResourceDefinition{
|
||||
{
|
||||
GroupResource: schema.GroupResource{Group: folders.GROUP, Resource: folders.RESOURCE},
|
||||
MigratorFunc: "MigrateFolders",
|
||||
@@ -44,50 +36,9 @@ var resourceRegistry = []ResourceDefinition{
|
||||
},
|
||||
}
|
||||
|
||||
var migrationRegistry = []migrationDefinition{
|
||||
{
|
||||
name: "playlists",
|
||||
resources: []string{setting.PlaylistResource},
|
||||
registerFunc: registerPlaylistMigration,
|
||||
},
|
||||
{
|
||||
name: "folders and dashboards",
|
||||
resources: []string{setting.FolderResource, setting.DashboardResource},
|
||||
registerFunc: registerDashboardAndFolderMigration,
|
||||
},
|
||||
}
|
||||
|
||||
func registerMigrations(cfg *setting.Cfg, mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) error {
|
||||
for _, migration := range migrationRegistry {
|
||||
var (
|
||||
hasValue bool
|
||||
allEnabled bool
|
||||
)
|
||||
|
||||
for _, res := range migration.resources {
|
||||
enabled := cfg.UnifiedStorage[res].EnableMigration
|
||||
if !hasValue {
|
||||
allEnabled = enabled
|
||||
hasValue = true
|
||||
continue
|
||||
}
|
||||
if enabled != allEnabled {
|
||||
return fmt.Errorf("cannot migrate resources separately: %v migration must be either all enabled or all disabled", migration.resources)
|
||||
}
|
||||
}
|
||||
|
||||
if !allEnabled {
|
||||
logger.Info("Migration is disabled in config, skipping", "migration", migration.name)
|
||||
continue
|
||||
}
|
||||
migration.registerFunc(mg, migrator, client)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getResourceDefinition(group, resource string) *ResourceDefinition {
|
||||
for i := range resourceRegistry {
|
||||
r := &resourceRegistry[i]
|
||||
for i := range registeredResources {
|
||||
r := ®isteredResources[i]
|
||||
if r.GroupResource.Group == group && r.GroupResource.Resource == resource {
|
||||
return r
|
||||
}
|
||||
@@ -129,13 +80,13 @@ func getMigratorFunc(accessor legacy.MigrationDashboardAccessor, group, resource
|
||||
|
||||
func validateRegisteredResources() error {
|
||||
registeredMap := make(map[string]bool)
|
||||
for _, gr := range resourceRegistry {
|
||||
for _, gr := range registeredResources {
|
||||
key := fmt.Sprintf("%s.%s", gr.GroupResource.Resource, gr.GroupResource.Group)
|
||||
registeredMap[key] = true
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for expected := range setting.MigratedUnifiedResources {
|
||||
for _, expected := range setting.MigratedUnifiedResources {
|
||||
if !registeredMap[expected] {
|
||||
missing = append(missing, expected)
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
sqlstoremigrator "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestRegisterMigrations exercises registerMigrations with various EnableMigration configs using a table-driven test.
|
||||
func TestRegisterMigrations(t *testing.T) {
|
||||
origRegistry := migrationRegistry
|
||||
t.Cleanup(func() { migrationRegistry = origRegistry })
|
||||
|
||||
// helper to build a fake registry with custom register funcs that bump counters
|
||||
makeFakeRegistry := func(migrationCalls map[string]int) []migrationDefinition {
|
||||
return []migrationDefinition{
|
||||
{
|
||||
name: "playlists",
|
||||
resources: []string{setting.PlaylistResource},
|
||||
registerFunc: func(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) {
|
||||
migrationCalls["playlists"]++
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "folders and dashboards",
|
||||
resources: []string{setting.FolderResource, setting.DashboardResource},
|
||||
registerFunc: func(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) {
|
||||
migrationCalls["folders and dashboards"]++
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Build a minimal cfg with UnifiedStorage entries used by registerMigrations
|
||||
makeCfg := func(vals map[string]bool) *setting.Cfg {
|
||||
cfg := &setting.Cfg{UnifiedStorage: make(map[string]setting.UnifiedStorageConfig)}
|
||||
for k, v := range vals {
|
||||
cfg.UnifiedStorage[k] = setting.UnifiedStorageConfig{EnableMigration: v}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Table of scenarios
|
||||
tests := []struct {
|
||||
name string
|
||||
enablePlaylist bool
|
||||
enableFolder bool
|
||||
enableDashboard bool
|
||||
wantPlaylistCalls int
|
||||
wantFDCalls int
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "playlists enabled", enablePlaylist: true, wantPlaylistCalls: 1},
|
||||
{name: "playlists disabled", enablePlaylist: false, wantPlaylistCalls: 0},
|
||||
{name: "folders+dashboards both enabled", enableFolder: true, enableDashboard: true, wantFDCalls: 1},
|
||||
{name: "folders enabled, dashboards disabled (mismatch)", enableFolder: true, enableDashboard: false, wantFDCalls: 0, wantErr: true},
|
||||
{name: "folders disabled, dashboards enabled (mismatch)", enableFolder: false, enableDashboard: true, wantFDCalls: 0, wantErr: true},
|
||||
{name: "folders+dashboards both disabled", enableFolder: false, enableDashboard: false, wantFDCalls: 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
migrationCalls := map[string]int{
|
||||
"playlists": 0,
|
||||
"folders and dashboards": 0,
|
||||
}
|
||||
|
||||
migrationRegistry = makeFakeRegistry(migrationCalls)
|
||||
|
||||
cfg := makeCfg(map[string]bool{
|
||||
setting.PlaylistResource: tt.enablePlaylist,
|
||||
setting.FolderResource: tt.enableFolder,
|
||||
setting.DashboardResource: tt.enableDashboard,
|
||||
})
|
||||
|
||||
// We pass nils for migrator dependencies because our fake registerFuncs don't use them
|
||||
err := registerMigrations(cfg, nil, nil, nil)
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err, "expected error for mismatched enablement")
|
||||
} else {
|
||||
require.NoError(t, err, "unexpected error")
|
||||
}
|
||||
|
||||
require.Equal(t, tt.wantPlaylistCalls, migrationCalls["playlists"], "playlists register call count")
|
||||
require.Equal(t, tt.wantFDCalls, migrationCalls["folders and dashboards"], "folders+dashboards register call count")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package migrations
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
@@ -48,25 +49,34 @@ func ProvideUnifiedStorageMigrationService(
|
||||
}
|
||||
|
||||
func (p *UnifiedStorageMigrationServiceImpl) Run(ctx context.Context) error {
|
||||
// TODO: temporary skip migrations in test environments to prevent integration test timeouts.
|
||||
if os.Getenv("GRAFANA_TEST_DB") != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// skip migrations if disabled in config
|
||||
if p.cfg.DisableDataMigrations {
|
||||
metrics.MUnifiedStorageMigrationStatus.Set(1)
|
||||
logger.Info("Data migrations are disabled, skipping")
|
||||
return nil
|
||||
} else {
|
||||
metrics.MUnifiedStorageMigrationStatus.Set(2)
|
||||
logger.Info("Data migrations not yet enforced, skipping")
|
||||
}
|
||||
logger.Info("Running migrations for unified storage")
|
||||
metrics.MUnifiedStorageMigrationStatus.Set(3)
|
||||
return RegisterMigrations(ctx, p.migrator, p.cfg, p.sqlStore, p.client)
|
||||
|
||||
// TODO: Re-enable once migrations are ready
|
||||
// TODO: add guarantee that this only runs once
|
||||
// return RegisterMigrations(p.migrator, p.cfg, p.sqlStore, p.client)
|
||||
return nil
|
||||
}
|
||||
|
||||
func RegisterMigrations(
|
||||
ctx context.Context,
|
||||
migrator UnifiedMigrator,
|
||||
cfg *setting.Cfg,
|
||||
sqlStore db.DB,
|
||||
client resource.ResourceClient,
|
||||
) error {
|
||||
ctx, span := tracer.Start(ctx, "storage.unified.RegisterMigrations")
|
||||
ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations")
|
||||
defer span.End()
|
||||
mg := sqlstoremigrator.NewScopedMigrator(sqlStore.GetEngine(), cfg, "unifiedstorage")
|
||||
mg.AddCreateMigration()
|
||||
@@ -79,19 +89,12 @@ func RegisterMigrations(
|
||||
return err
|
||||
}
|
||||
|
||||
if err := registerMigrations(cfg, mg, migrator, client); err != nil {
|
||||
return err
|
||||
}
|
||||
// Register resource migrations
|
||||
registerDashboardAndFolderMigration(mg, migrator, client)
|
||||
registerPlaylistMigration(mg, migrator, client)
|
||||
|
||||
// Run all registered migrations (blocking)
|
||||
sec := cfg.Raw.Section("database")
|
||||
db := mg.DBEngine.DB().DB
|
||||
maxOpenConns := db.Stats().MaxOpenConnections
|
||||
if maxOpenConns <= 2 {
|
||||
// migrations require at least 3 connections due to extra GRPC connections
|
||||
db.SetMaxOpenConns(3)
|
||||
defer db.SetMaxOpenConns(maxOpenConns)
|
||||
}
|
||||
if err := mg.RunMigrations(ctx,
|
||||
sec.Key("migration_locking").MustBool(true),
|
||||
sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil {
|
||||
|
||||
@@ -1166,12 +1166,11 @@ func TestIntegrationConvertPrometheusEndpoints_Editor(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableAuthZClientCache: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
EnableRecordingRules: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
EnableRecordingRules: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
|
||||
|
||||
@@ -1192,11 +1192,10 @@ func TestIntegrationExportFileProvisionContactPoints(t *testing.T) {
|
||||
|
||||
func TestIntegrationFullpath(t *testing.T) {
|
||||
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableAuthZClientCache: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||
|
||||
@@ -53,11 +53,10 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableAuthZClientCache: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||
@@ -361,11 +360,10 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableAuthZClientCache: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||
@@ -1431,11 +1429,10 @@ func TestIntegrationRuleGroupSequence(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableAuthZClientCache: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
|
||||
|
||||
@@ -32,9 +32,8 @@ func TestIntegrationAnnotations(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableAuthZClientCache: true,
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{featuremgmt.FlagAnnotationPermissionUpdate},
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{featuremgmt.FlagAnnotationPermissionUpdate},
|
||||
})
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
noneUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -39,15 +38,8 @@ func TestMain(m *testing.M) {
|
||||
func TestIntegrationDashboardServiceValidation(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
unifiedConfig := make(map[string]setting.UnifiedStorageConfig)
|
||||
for _, resource := range []string{"folders.folder.grafana.app", "dashboards.dashboard.grafana.app"} {
|
||||
unifiedConfig[resource] = setting.UnifiedStorageConfig{
|
||||
EnableMigration: true,
|
||||
}
|
||||
}
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
UnifiedStorageConfig: unifiedConfig,
|
||||
DisableAnonymous: true,
|
||||
})
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
|
||||
@@ -226,12 +218,11 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
dashboardWithDuplicatedLegacyAnnotation := "new-uid"
|
||||
t.Run("When saving a dashboard with an already used legacy ID", func(t *testing.T) {
|
||||
t.Run("When updating uid with id", func(t *testing.T) {
|
||||
resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"id": savedDashInFolder.ID, // nolint:staticcheck
|
||||
"uid": dashboardWithDuplicatedLegacyAnnotation,
|
||||
"uid": "new-uid",
|
||||
"title": "Updated title",
|
||||
},
|
||||
"folderUid": savedDashInFolder.FolderUID,
|
||||
@@ -243,48 +234,7 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("When updating a dashboard with legacy ID in multiple dashboards", func(t *testing.T) {
|
||||
resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"id": savedDashInFolder.ID, // nolint:staticcheck
|
||||
"uid": savedDashInGeneralFolder.UID,
|
||||
"title": "Updated title",
|
||||
},
|
||||
"folderUid": savedDashInFolder.FolderUID,
|
||||
"overwrite": true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
// Delete the dashboard with duplicated legacy ID annotation
|
||||
u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/uid/%s", grafanaListedAddr, dashboardWithDuplicatedLegacyAnnotation)
|
||||
req, err := http.NewRequest("DELETE", u, nil)
|
||||
require.NoError(t, err)
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("When updating a dashboard already using that uid", func(t *testing.T) {
|
||||
resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"id": savedDashInFolder.ID,
|
||||
"uid": savedDashInFolder.UID,
|
||||
"title": "Dashboard with existing UID",
|
||||
},
|
||||
"folderUid": savedDashInFolder.FolderUID,
|
||||
"overwrite": true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("When updating id with a dashboard already using that uid", func(t *testing.T) {
|
||||
t.Run("When updating uid with a dashboard already using that uid", func(t *testing.T) {
|
||||
resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"id": savedDashInFolder.ID, // nolint:staticcheck
|
||||
@@ -318,9 +268,8 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Obs: in legacy, the dashboard request would fail
|
||||
// After the dashboard is created, the user can see that there is an error with the library panel and can remove them manually
|
||||
t.Run("When creating a dashboard that references a non-existent library panel", func(t *testing.T) {
|
||||
originalCount := getDashboardCount(t, grafanaListedAddr, "admin", "admin")
|
||||
resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
|
||||
"dashboard": map[string]interface{}{
|
||||
"title": "Bad dashboard",
|
||||
@@ -336,11 +285,15 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) {
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
_, err = io.ReadAll(resp.Body)
|
||||
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(body), "library element could not be found")
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// A new dashboard is not created in this situation.
|
||||
require.Equal(t, originalCount, getDashboardCount(t, grafanaListedAddr, "admin", "admin"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -379,7 +332,7 @@ func TestIntegrationDashboardQuota(t *testing.T) {
|
||||
dashboardDTO := &plugindashboards.PluginDashboard{}
|
||||
err = json.Unmarshal(b, dashboardDTO)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, "just testing", dashboardDTO.Title)
|
||||
require.EqualValues(t, 1, dashboardDTO.DashboardId)
|
||||
})
|
||||
|
||||
t.Run("when quota limit exceeds importing a dashboard should fail", func(t *testing.T) {
|
||||
@@ -468,7 +421,7 @@ providers:
|
||||
dashboardUID = d.UID
|
||||
dashboardID = d.ID // nolint:staticcheck
|
||||
}
|
||||
assert.Len(t, *dashboardList, 1)
|
||||
assert.Equal(t, int64(1), dashboardID)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
@@ -828,7 +781,7 @@ func TestIntegrationImportDashboardWithLibraryPanels(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Library Panel 2",
|
||||
"title": "Library Panel 2",
|
||||
"type": "stat",
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
|
||||
"libraryPanel": {
|
||||
@@ -852,7 +805,7 @@ func TestIntegrationImportDashboardWithLibraryPanels(t *testing.T) {
|
||||
}
|
||||
},
|
||||
"test-lib-panel-2": {
|
||||
"uid": "test-lib-panel-2",
|
||||
"uid": "test-lib-panel-2",
|
||||
"name": "Test Library Panel 2",
|
||||
"kind": 1,
|
||||
"type": "stat",
|
||||
@@ -1058,12 +1011,26 @@ func postDashboard(t *testing.T, grafanaListedAddr, user, password string, paylo
|
||||
return http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
|
||||
}
|
||||
|
||||
func getDashboardCount(t *testing.T, grafanaListenAddr, user, password string) int {
|
||||
endpoint := fmt.Sprintf("http://%s:%s@%s/apis/dashboard.grafana.app/v0alpha1/namespaces/default/search", user, password, grafanaListenAddr)
|
||||
resp, err := http.Get(endpoint) //nolint:gosec
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(body, &payload))
|
||||
|
||||
return int(payload["totalHits"].(float64))
|
||||
}
|
||||
|
||||
func TestIntegrationDashboardServicePermissions(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
DisableAuthZClientCache: true,
|
||||
DisableAnonymous: true,
|
||||
})
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
|
||||
@@ -42,10 +42,9 @@ func TestIntegrationStars(t *testing.T) {
|
||||
}
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: false, // required for experimental APIs
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: flags,
|
||||
AppModeProduction: false, // required for experimental APIs
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: flags,
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {
|
||||
DualWriterMode: mode,
|
||||
|
||||
@@ -37,8 +37,7 @@ func TestMain(m *testing.M) {
|
||||
func runDashboardTest(t *testing.T, mode rest.DualWriterMode, gvr schema.GroupVersionResource) {
|
||||
t.Run("simple crud+list", func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {
|
||||
DualWriterMode: mode,
|
||||
@@ -195,9 +194,7 @@ func TestIntegrationLegacySupport(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
ctx := context.Background()
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
})
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{})
|
||||
|
||||
clientV0 := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Admin,
|
||||
@@ -503,10 +500,9 @@ func runDashboardSearchTest(t *testing.T, mode rest.DualWriterMode) {
|
||||
ctx := context.Background()
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: true,
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {DualWriterMode: mode},
|
||||
"folders.folder.grafana.app": {DualWriterMode: mode},
|
||||
|
||||
@@ -68,8 +68,7 @@ func TestIntegrationDashboardAPIValidation(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
|
||||
// Create a K8sTestHelper which will set up a real API server
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesDashboards, // Enable FE-only dashboard feature flag
|
||||
},
|
||||
@@ -106,8 +105,7 @@ func TestIntegrationDashboardAPIValidation(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("DualWriterMode %d - kubernetesDashboards disabled", dualWriterMode), func(t *testing.T) {
|
||||
// Create a K8sTestHelper which will set up a real API server
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
DisableAnonymous: true,
|
||||
DisableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesDashboards,
|
||||
},
|
||||
@@ -140,8 +138,7 @@ func TestIntegrationDashboardAPIAuthorization(t *testing.T) {
|
||||
for _, dualWriterMode := range dualWriterModes {
|
||||
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
DisableAnonymous: true,
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {
|
||||
DualWriterMode: dualWriterMode,
|
||||
@@ -188,8 +185,7 @@ func TestIntegrationDashboardAPI(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
|
||||
// Create a K8sTestHelper which will set up a real API server
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesDashboards,
|
||||
},
|
||||
|
||||
@@ -29,8 +29,7 @@ func TestIntegrationLibraryPanelConnections(t *testing.T) {
|
||||
for _, dualWriterMode := range dualWriterModes {
|
||||
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
"kubernetesLibraryPanels",
|
||||
},
|
||||
@@ -94,8 +93,7 @@ func TestIntegrationLibraryElementPermissions(t *testing.T) {
|
||||
for _, dualWriterMode := range dualWriterModes {
|
||||
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
"kubernetesLibraryPanels",
|
||||
"grafanaAPIServerWithExperimentalAPIs", // needed until we move it to v0beta1 at least (currently v0alpha1)
|
||||
@@ -298,8 +296,7 @@ func TestIntegrationLibraryPanelConnectionsWithFolderAccess(t *testing.T) {
|
||||
for _, dualWriterMode := range dualWriterModes {
|
||||
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
"kubernetesLibraryPanels",
|
||||
},
|
||||
|
||||
@@ -37,10 +37,9 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) {
|
||||
ctx := context.Background()
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {DualWriterMode: mode},
|
||||
"folders.folder.grafana.app": {DualWriterMode: mode},
|
||||
|
||||
@@ -48,10 +48,9 @@ func TestIntegrationFolderTree(t *testing.T) {
|
||||
for _, mode := range modes {
|
||||
t.Run(fmt.Sprintf("mode %d", mode), func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {
|
||||
DualWriterMode: mode,
|
||||
|
||||
@@ -139,10 +139,9 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
|
||||
t.Run(fmt.Sprintf("with dual write (unified storage, mode %v)", modeDw), func(t *testing.T) {
|
||||
doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -154,10 +153,9 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
|
||||
t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create nested folders)", modeDw), func(t *testing.T) {
|
||||
doNestedCreateTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -168,10 +166,9 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
|
||||
t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create existing folder)", modeDw), func(t *testing.T) {
|
||||
doCreateDuplicateFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -182,10 +179,9 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
|
||||
t.Run(fmt.Sprintf("when creating a folder, mode %v, it should trim leading and trailing spaces", modeDw), func(t *testing.T) {
|
||||
doCreateEnsureTitleIsTrimmedTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -196,10 +192,9 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
|
||||
t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create circular reference folder)", modeDw), func(t *testing.T) {
|
||||
doCreateCircularReferenceFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -223,10 +218,9 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
for _, mode := range modes {
|
||||
t.Run(fmt.Sprintf("mode %d", mode), func(t *testing.T) {
|
||||
doListFoldersTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: mode,
|
||||
@@ -741,10 +735,9 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("Mode_%d", mode), func(t *testing.T) {
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -869,10 +862,9 @@ func TestIntegrationFolderGetPermissions(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("Mode_%d", mode), func(t *testing.T) {
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -1087,10 +1079,9 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) {
|
||||
for mode := 0; mode <= 4; mode++ {
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -1263,10 +1254,9 @@ func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) {
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -1376,10 +1366,9 @@ func TestIntegrationFolderDeletionBlockedByLibraryElements(t *testing.T) {
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -1457,10 +1446,9 @@ func TestIntegrationRootFolderDeletionBlockedByLibraryElementsInSubfolder(t *tes
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -1554,10 +1542,9 @@ func TestIntegrationFolderDeletionBlockedByConnectedLibraryPanels(t *testing.T)
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -1633,10 +1620,9 @@ func TestIntegrationFolderDeletionWithDanglingLibraryPanels(t *testing.T) {
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -1915,10 +1901,9 @@ func TestIntegrationDeleteNestedFoldersPostorder(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("Mode %d: Delete nested folder hierarchy in postorder", mode), func(t *testing.T) {
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -2042,10 +2027,9 @@ func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("Mode %d: Delete provisioned folders and dashboards", mode), func(t *testing.T) {
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
ops := testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
APIServerStorageType: "unified",
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
@@ -2147,14 +2131,11 @@ func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) {
|
||||
// Test that folders created during provisioning using the dual writer have the
|
||||
// appropriate labels and annotations in unified storage.
|
||||
func TestIntegrationProvisionedFolderPropagatesLabelsAndAnnotations(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
mode3 := grafanarest.DualWriterMode(3)
|
||||
ops := testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
APIServerStorageType: "unified",
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folders.RESOURCEGROUP: {
|
||||
DualWriterMode: mode3,
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"tags": [
|
||||
"Playlist"
|
||||
],
|
||||
"description": "list or watch objects of kind Playlist",
|
||||
"description": "list objects of kind Playlist",
|
||||
"operationId": "listPlaylist",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1850,32 +1850,6 @@
|
||||
"description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": {
|
||||
"description": "Event represents a single event to a watched resource.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"object"
|
||||
],
|
||||
"properties": {
|
||||
"object": {
|
||||
"description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"io.k8s.apimachinery.pkg.runtime.RawExtension": {
|
||||
"description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,48 +54,47 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
// t.Logf("%s", disco)
|
||||
require.JSONEq(t, `[
|
||||
{
|
||||
"freshness": "Current",
|
||||
"resources": [
|
||||
{
|
||||
"resource": "playlists",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "Playlist",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "playlist",
|
||||
"subresources": [
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "Playlist",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "status",
|
||||
"verbs": [
|
||||
"get",
|
||||
"patch",
|
||||
"update"
|
||||
]
|
||||
}
|
||||
],
|
||||
"verbs": [
|
||||
"create",
|
||||
"delete",
|
||||
"deletecollection",
|
||||
"get",
|
||||
"list",
|
||||
"patch",
|
||||
"update",
|
||||
"watch"
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": "v0alpha1"
|
||||
}
|
||||
]`, disco)
|
||||
{
|
||||
"version": "v0alpha1",
|
||||
"freshness": "Current",
|
||||
"resources": [
|
||||
{
|
||||
"resource": "playlists",
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "Playlist",
|
||||
"version": ""
|
||||
},
|
||||
"scope": "Namespaced",
|
||||
"singularResource": "playlist",
|
||||
"subresources": [
|
||||
{
|
||||
"responseKind": {
|
||||
"group": "",
|
||||
"kind": "Playlist",
|
||||
"version": ""
|
||||
},
|
||||
"subresource": "status",
|
||||
"verbs": [
|
||||
"get",
|
||||
"patch",
|
||||
"update"
|
||||
]
|
||||
}
|
||||
],
|
||||
"verbs": [
|
||||
"create",
|
||||
"delete",
|
||||
"deletecollection",
|
||||
"get",
|
||||
"list",
|
||||
"patch",
|
||||
"update"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`, disco)
|
||||
})
|
||||
|
||||
t.Run("with k8s api flag", func(t *testing.T) {
|
||||
@@ -107,10 +106,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
|
||||
t.Run("with dual write (file, mode 0)", func(t *testing.T) {
|
||||
doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode0,
|
||||
@@ -121,10 +119,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
|
||||
t.Run("with dual write (file, mode 1)", func(t *testing.T) {
|
||||
doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode1,
|
||||
@@ -135,10 +132,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
|
||||
t.Run("with dual write (file, mode 2)", func(t *testing.T) {
|
||||
doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode2,
|
||||
@@ -149,10 +145,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
|
||||
t.Run("with dual write (file, mode 3)", func(t *testing.T) {
|
||||
doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode3,
|
||||
@@ -163,10 +158,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
|
||||
t.Run("with dual write (file, mode 5)", func(t *testing.T) {
|
||||
helper := doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "file", // write the files to disk
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode5,
|
||||
@@ -204,10 +198,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
|
||||
t.Run("with dual write (unified storage, mode 0)", func(t *testing.T) {
|
||||
doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: false, // required for unified storage
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified, // use the entity api tables
|
||||
AppModeProduction: false, // required for unified storage
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified, // use the entity api tables
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode0,
|
||||
@@ -218,20 +211,18 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
|
||||
t.Run("with dual write (unified storage, mode 1)", func(t *testing.T) {
|
||||
doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: false,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified,
|
||||
EnableFeatureToggles: []string{},
|
||||
AppModeProduction: false,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified,
|
||||
EnableFeatureToggles: []string{},
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("with dual write (unified storage, mode 2)", func(t *testing.T) {
|
||||
doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: false, // required for unified storage
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified, // use the entity api tables
|
||||
AppModeProduction: false, // required for unified storage
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified, // use the entity api tables
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode2,
|
||||
@@ -242,10 +233,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
|
||||
t.Run("with dual write (unified storage, mode 3)", func(t *testing.T) {
|
||||
doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: false, // required for unified storage
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified, // use the entity api tables
|
||||
AppModeProduction: false, // required for unified storage
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified, // use the entity api tables
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode3,
|
||||
@@ -256,10 +246,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
|
||||
t.Run("with dual write (unified storage, mode 5)", func(t *testing.T) {
|
||||
doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: false, // required for unified storage
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified, // use the entity api tables
|
||||
AppModeProduction: false, // required for unified storage
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified, // use the entity api tables
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode5,
|
||||
@@ -273,10 +262,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
t.Skip("local etcd testing")
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode0,
|
||||
@@ -300,10 +288,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
t.Skip("local etcd testing")
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode1,
|
||||
@@ -327,10 +314,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
t.Skip("local etcd testing")
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode2,
|
||||
@@ -354,10 +340,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
t.Skip("local etcd testing")
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode3,
|
||||
@@ -381,10 +366,9 @@ func TestIntegrationPlaylist(t *testing.T) {
|
||||
t.Skip("local etcd testing")
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: grafanarest.Mode5,
|
||||
|
||||
@@ -362,13 +362,6 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) {
|
||||
_, err = rbacSect.NewKey("permission_cache", "false")
|
||||
require.NoError(t, err)
|
||||
|
||||
if opts.DisableAuthZClientCache {
|
||||
authzSect, err := cfg.NewSection("authorization")
|
||||
require.NoError(t, err)
|
||||
_, err = authzSect.NewKey("cache_ttl", "0")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
analyticsSect, err := cfg.NewSection("analytics")
|
||||
require.NoError(t, err)
|
||||
_, err = analyticsSect.NewKey("intercom_secret", "intercom_secret_at_config")
|
||||
@@ -554,8 +547,6 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) {
|
||||
require.NoError(t, err)
|
||||
_, err = section.NewKey("dualWriterMode", fmt.Sprintf("%d", v.DualWriterMode))
|
||||
require.NoError(t, err)
|
||||
_, err = section.NewKey("enableMigration", fmt.Sprintf("%t", v.EnableMigration))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
if opts.UnifiedStorageEnableSearch {
|
||||
@@ -679,7 +670,6 @@ type GrafanaOpts struct {
|
||||
DisableDataMigrations bool
|
||||
SecretsManagerEnableDBMigrations bool
|
||||
OpenFeatureAPIEnabled bool
|
||||
DisableAuthZClientCache bool
|
||||
|
||||
// Allow creating grafana dir beforehand
|
||||
Dir string
|
||||
|
||||
@@ -119,11 +119,11 @@ export const LoginCtrl = memo(({ resetCode, children }: Props) => {
|
||||
}, []);
|
||||
|
||||
const login = useCallback(
|
||||
async (formModel: FormModel) => {
|
||||
(formModel: FormModel) => {
|
||||
setLoginErrorMessage(undefined);
|
||||
setIsLoggingIn(true);
|
||||
|
||||
return getBackendSrv()
|
||||
getBackendSrv()
|
||||
.post<LoginDTO>('/login', formModel, { showErrorAlert: false })
|
||||
.then((result) => {
|
||||
setResult(result);
|
||||
|
||||
@@ -165,10 +165,10 @@ describe('CloneRuleEditor', function () {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ui.inputs.namespace.get()).toHaveTextContent('namespace-one');
|
||||
expect(ui.inputs.name.get()).toHaveValue('First Ruler Rule (copy)');
|
||||
});
|
||||
expect(ui.inputs.name.get()).toHaveValue('First Ruler Rule (copy)');
|
||||
expect(ui.inputs.expr.get()).toHaveValue('vector(1) > 0');
|
||||
expect(ui.inputs.namespace.get()).toHaveTextContent('namespace-one');
|
||||
expect(ui.inputs.group.get()).toHaveTextContent('group1');
|
||||
expect(
|
||||
byRole('listitem', {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ElementSelectionContext, useSidebar, useStyles2, Sidebar } from '@grafa
|
||||
import NativeScrollbar, { DivScrollElement } from 'app/core/components/NativeScrollbar';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { KioskMode } from 'app/types/dashboard';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
@@ -33,7 +32,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
const styles = useStyles2(getStyles, headerHeight ?? 0);
|
||||
const { chrome } = useGrafana();
|
||||
const { kioskMode } = chrome.useState();
|
||||
const { isPlaying } = playlistSrv.useState();
|
||||
const isInKioskMode = kioskMode === KioskMode.Full;
|
||||
|
||||
if (!config.featureToggles.dashboardNewLayouts) {
|
||||
return (
|
||||
@@ -95,10 +94,8 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
};
|
||||
|
||||
function renderBody() {
|
||||
const renderWithoutSidebar = isPlaying || kioskMode === KioskMode.Full;
|
||||
|
||||
// In kiosk mode the full document body scrolls so we don't need to wrap in our own scrollbar
|
||||
if (renderWithoutSidebar) {
|
||||
if (isInKioskMode) {
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.bodyWrapper, styles.bodyWrapperKiosk)}
|
||||
|
||||
@@ -15,14 +15,13 @@ export function DashboardDataLayerControls({ dashboard }: { dashboard: Dashboard
|
||||
const state = sceneGraph.getData(dashboard).useState();
|
||||
// It is possible to render the controls for the annotation data layers in separate places using the `placement` property.
|
||||
// In case it's not specified, we are rendering the controls here (default).
|
||||
const isDefaultPlacementAndNotHidden = (layer: SceneDataLayerProvider) =>
|
||||
layer.state.placement === undefined && !layer.state.isHidden;
|
||||
const isDefaultPlacement = (layer: SceneDataLayerProvider) => layer.state.placement === undefined;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (isDashboardDataLayerSetState(state)) {
|
||||
return (
|
||||
<>
|
||||
{state.annotationLayers.filter(isDefaultPlacementAndNotHidden).map((layer) => (
|
||||
{state.annotationLayers.filter(isDefaultPlacement).map((layer) => (
|
||||
<div key={layer.state.key} className={styles.container}>
|
||||
<DataLayerControl layer={layer} />
|
||||
</div>
|
||||
@@ -36,7 +35,6 @@ export function DashboardDataLayerControls({ dashboard }: { dashboard: Dashboard
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
label: 'dashboard-data-layer-controls',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
verticalAlign: 'middle',
|
||||
|
||||
@@ -18,7 +18,7 @@ export function DashboardLinksControls({ links, dashboard }: Props) {
|
||||
const uid = dashboard.state.uid;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!links || !uid || links.length === 0) {
|
||||
if (!links || !uid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ export function DashboardLinksControls({ links, dashboard }: Props) {
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
linksContainer: css({
|
||||
label: 'dashboard-links-controls',
|
||||
display: 'inline-flex',
|
||||
gap: theme.spacing(1),
|
||||
marginRight: theme.spacing(1),
|
||||
|
||||
@@ -47,7 +47,5 @@ export function DashboardControlsButton({ dashboard }: { dashboard: DashboardSce
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
dropdownButton: css({
|
||||
display: 'inline-flex',
|
||||
marginBottom: theme.spacing(1),
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ import { loadSnapshotData } from '../utils/loadSnapshotData';
|
||||
|
||||
import { PanelHeaderMenuWrapper } from './PanelHeader/PanelHeaderMenuWrapper';
|
||||
import { PanelLoadTimeMonitor } from './PanelLoadTimeMonitor';
|
||||
import { isHideSeriesOverride, seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
||||
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
||||
import { liveTimer } from './liveTimer';
|
||||
import { PanelOptionsLogger } from './panelOptionsLogger';
|
||||
|
||||
@@ -106,7 +106,6 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
sync: this.getSync,
|
||||
onSeriesColorChange: this.onSeriesColorChange,
|
||||
onToggleSeriesVisibility: this.onSeriesVisibilityChange,
|
||||
onResetAllSeriesVisibility: this.onSeriesVisibilityReset,
|
||||
onAnnotationCreate: this.onAnnotationCreate,
|
||||
onAnnotationUpdate: this.onAnnotationUpdate,
|
||||
onAnnotationDelete: this.onAnnotationDelete,
|
||||
@@ -172,13 +171,6 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
onSeriesVisibilityReset = () => {
|
||||
this.onFieldConfigChange({
|
||||
...this.props.panel.fieldConfig,
|
||||
overrides: this.props.panel.fieldConfig.overrides.filter((rule) => !isHideSeriesOverride(rule)),
|
||||
});
|
||||
};
|
||||
|
||||
onToggleLegendSort = (sortKey: string) => {
|
||||
const legendOptions: VizLegendOptions = this.props.panel.options.legend;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { SeriesVisibilityChangeMode } from '@grafana/ui';
|
||||
|
||||
const displayOverrideRef = 'hideSeriesFrom';
|
||||
export const isHideSeriesOverride = isSystemOverrideWithRef(displayOverrideRef);
|
||||
const isHideSeriesOverride = isSystemOverrideWithRef(displayOverrideRef);
|
||||
|
||||
export function seriesVisibilityConfigFactory(
|
||||
label: string,
|
||||
|
||||
@@ -34,10 +34,7 @@ import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/times
|
||||
import { Options as TimeSeriesOptions } from 'app/plugins/panel/timeseries/panelcfg.gen';
|
||||
import { ExploreGraphStyle } from 'app/types/explore';
|
||||
|
||||
import {
|
||||
isHideSeriesOverride,
|
||||
seriesVisibilityConfigFactory,
|
||||
} from '../../dashboard/dashgrid/SeriesVisibilityConfigFactory';
|
||||
import { seriesVisibilityConfigFactory } from '../../dashboard/dashgrid/SeriesVisibilityConfigFactory';
|
||||
import { useExploreDataLinkPostProcessor } from '../hooks/useExploreDataLinkPostProcessor';
|
||||
|
||||
import { applyGraphStyle, applyThresholdsConfig } from './exploreGraphStyleUtils';
|
||||
@@ -174,12 +171,6 @@ export function ExploreGraph({
|
||||
onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) {
|
||||
setFieldConfig(seriesVisibilityConfigFactory(label, mode, fieldConfig, data));
|
||||
},
|
||||
onResetAllSeriesVisibility: () => {
|
||||
setFieldConfig({
|
||||
...fieldConfig,
|
||||
overrides: fieldConfig.overrides.filter((rule) => !isHideSeriesOverride(rule)),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function toggleLegend(name: string | undefined, mode: SeriesVisibilityChangeMode) {
|
||||
|
||||
@@ -14,7 +14,6 @@ interface PopoverMenuProps {
|
||||
y: number;
|
||||
onClickFilterString?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
onClickSearchString?: (text: string) => void;
|
||||
onDisable: () => void;
|
||||
row: LogRowModel;
|
||||
close: () => void;
|
||||
@@ -25,7 +24,6 @@ export const PopoverMenu = ({
|
||||
y,
|
||||
onClickFilterString,
|
||||
onClickFilterOutString,
|
||||
onClickSearchString,
|
||||
selection,
|
||||
row,
|
||||
close,
|
||||
@@ -52,7 +50,7 @@ export const PopoverMenu = ({
|
||||
props.onDisable();
|
||||
}, [props, row.datasourceType, selection.length]);
|
||||
|
||||
const supported = onClickFilterString || onClickFilterOutString || onClickSearchString;
|
||||
const supported = onClickFilterString || onClickFilterOutString;
|
||||
|
||||
if (!supported) {
|
||||
return null;
|
||||
@@ -91,17 +89,6 @@ export const PopoverMenu = ({
|
||||
/>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
{onClickSearchString && (
|
||||
<Menu.Item
|
||||
label={t('logs.popover-menu.search-text', 'Search in results')}
|
||||
onClick={() => {
|
||||
onClickSearchString(selection);
|
||||
close();
|
||||
track('search_text', selection.length, row.datasourceType);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item label={t('logs.popover-menu.disable-menu', 'Disable menu')} onClick={onDisable} />
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('Explore: handle running/not running query', () => {
|
||||
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
||||
|
||||
// Make sure we render the logs panel
|
||||
await screen.findByRole('heading', { name: /^Logs$/ });
|
||||
await screen.findByText(/^Logs$/);
|
||||
|
||||
// Make sure we render the log line
|
||||
await screen.findByText(/custom log line/i);
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('Handles open/close splits and related events in UI and URL', () => {
|
||||
|
||||
// Make sure we render the logs panel
|
||||
await waitFor(() => {
|
||||
const logsPanels = screen.getAllByRole('heading', { name: /^Logs$/ });
|
||||
const logsPanels = screen.getAllByText(/^Logs$/);
|
||||
expect(logsPanels.length).toBe(2);
|
||||
});
|
||||
|
||||
|
||||
@@ -185,12 +185,11 @@ export const LogRowMenuCell = memo(
|
||||
}
|
||||
);
|
||||
|
||||
type AddonOnClickListener = (event: MouseEvent<HTMLElement>, row: LogRowModel) => void | undefined;
|
||||
type ChildElementProps = Record<string, unknown> & { onClick: AddonOnClickListener };
|
||||
type AddonOnClickListener = (event: MouseEvent, row: LogRowModel) => void | undefined;
|
||||
function addClickListenersToNode(nodes: ReactNode[], row: LogRowModel) {
|
||||
return nodes.map((node, index) => {
|
||||
if (isValidElement<ChildElementProps>(node)) {
|
||||
const onClick = node.props.onClick;
|
||||
if (isValidElement(node)) {
|
||||
const onClick: AddonOnClickListener = node.props.onClick;
|
||||
if (!onClick) {
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { LogListModel, NEWLINES_REGEX } from '../panel/processing';
|
||||
export const OTEL_PROBE_FIELD = 'severity_number';
|
||||
const OTEL_LANGUAGE_UNKNOWN = 'unknown';
|
||||
|
||||
export function identifyOTelLanguages(logs: LogListModel[] | LogRowModel[]): string[] {
|
||||
function identifyOTelLanguages(logs: LogListModel[] | LogRowModel[]): string[] {
|
||||
const languagesSet = new Set<string>();
|
||||
logs.forEach((log) => {
|
||||
const lang = identifyOTelLanguage(log);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { camelCase, groupBy } from 'lodash';
|
||||
import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, startTransition, useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { DataFrameType, GrafanaTheme2, store, TimeRange } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
@@ -18,7 +18,6 @@ import { LogLineDetailsLinks } from './LogLineDetailsLinks';
|
||||
import { LogLineDetailsLog } from './LogLineDetailsLog';
|
||||
import { LogLineDetailsTrace } from './LogLineDetailsTrace';
|
||||
import { useLogListContext } from './LogListContext';
|
||||
import { reportInteractionOnce } from './analytics';
|
||||
import { getTempoTraceFromLinks } from './links';
|
||||
import { LogListModel } from './processing';
|
||||
|
||||
@@ -125,21 +124,6 @@ export const LogLineDetailsComponent = memo(
|
||||
[fieldsWithLinks.links, fieldsWithLinks.linksFromVariableMap]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (noInteractions) {
|
||||
return;
|
||||
}
|
||||
reportInteractionOnce('logs_log_line_details_fields_displayed', {
|
||||
links: allLinks.length,
|
||||
trace: trace !== undefined,
|
||||
fields: fieldsWithoutLinks.length,
|
||||
labels: labelsWithLinks.length,
|
||||
labelGroups: labelGroups.join(', '),
|
||||
});
|
||||
// Once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LogLineDetailsHeader focusLogLine={focusLogLine} log={log} search={search} onSearch={handleSearch} />
|
||||
|
||||
@@ -102,11 +102,7 @@ export const LogLineDetailsHeader = ({ focusLogLine, log, search, onSearch }: Pr
|
||||
}
|
||||
|
||||
setDetailsMode(newMode);
|
||||
|
||||
reportInteractionWrapper('logs_log_line_details_header_toggle_details_mode', {
|
||||
newMode,
|
||||
});
|
||||
}, [detailsMode, logOptionsStorageKey, reportInteractionWrapper, setDetailsMode]);
|
||||
}, [detailsMode, logOptionsStorageKey, setDetailsMode]);
|
||||
|
||||
const toggleLogLine = useCallback(() => {
|
||||
if (logLineDisplayed) {
|
||||
|
||||
@@ -280,7 +280,6 @@ const LogListComponent = ({
|
||||
wrapLogMessage,
|
||||
} = useLogListContext();
|
||||
const { detailsMode, showDetails, toggleDetails } = useLogDetailsContext();
|
||||
const { setSearch, showSearch } = useLogListSearchContext();
|
||||
const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]);
|
||||
const [listHeight, setListHeight] = useState(getListHeight(containerElement, app));
|
||||
const theme = useTheme2();
|
||||
@@ -442,14 +441,6 @@ const LogListComponent = ({
|
||||
[debouncedScrollToItem, filteredLogs]
|
||||
);
|
||||
|
||||
const onClickSearchString = useCallback(
|
||||
(search: string) => {
|
||||
showSearch();
|
||||
setSearch(search);
|
||||
},
|
||||
[setSearch, showSearch]
|
||||
);
|
||||
|
||||
const logLevels = useMemo(() => getLevelsFromLogs(processedLogs), [processedLogs]);
|
||||
|
||||
if (!containerElement || listHeight == null) {
|
||||
@@ -480,7 +471,6 @@ const LogListComponent = ({
|
||||
{...popoverState.popoverMenuCoordinates}
|
||||
onClickFilterString={onClickFilterString}
|
||||
onClickFilterOutString={onClickFilterOutString}
|
||||
onClickSearchString={onClickSearchString}
|
||||
onDisable={onDisablePopoverMenu}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -34,7 +34,7 @@ import { getDefaultDetailsMode, getDetailsWidth } from './LogDetailsContext';
|
||||
import { LogLineTimestampResolution } from './LogLine';
|
||||
import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu';
|
||||
import { LogListOptions, LogListFontSize } from './LogList';
|
||||
import { collectInsights } from './analytics';
|
||||
import { reportInteractionOnce } from './analytics';
|
||||
import { LogListModel } from './processing';
|
||||
|
||||
export interface LogListContextData extends Omit<Props, 'containerElement' | 'logs' | 'logsMeta' | 'showControls'> {
|
||||
@@ -241,7 +241,7 @@ export const LogListContextProvider = ({
|
||||
if (noInteractions) {
|
||||
return;
|
||||
}
|
||||
collectInsights(logs, app, {
|
||||
reportInteractionOnce(`logs_log_list_${app}_logs_displayed`, {
|
||||
dedupStrategy,
|
||||
fontSize,
|
||||
forceEscape: logListState.forceEscape,
|
||||
|
||||
@@ -18,11 +18,19 @@ interface Props {
|
||||
|
||||
export const LOG_LIST_SEARCH_HEIGHT = 48;
|
||||
export const LogListSearch = ({ listRef, logs }: Props) => {
|
||||
const { hideSearch, filterLogs, matchingUids, search, setMatchingUids, setSearch, searchVisible, toggleFilterLogs } =
|
||||
useLogListSearchContext();
|
||||
const {
|
||||
hideSearch,
|
||||
filterLogs,
|
||||
matchingUids,
|
||||
setMatchingUids,
|
||||
setSearch: setContextSearch,
|
||||
searchVisible,
|
||||
toggleFilterLogs,
|
||||
} = useLogListSearchContext();
|
||||
const { displayedFields, noInteractions } = useLogListContext();
|
||||
const [search, setSearch] = useState('');
|
||||
const [currentResult, setCurrentResult] = useState<number | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const inputRef = useRef('');
|
||||
const searchUsedRef = useRef(false);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@@ -35,15 +43,16 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
inputRef.current = e.target.value;
|
||||
startTransition(() => {
|
||||
setSearch(inputRef.current?.value ?? '');
|
||||
setSearch(inputRef.current);
|
||||
});
|
||||
if (!searchUsedRef.current && !noInteractions) {
|
||||
reportInteraction('logs_log_list_search_used');
|
||||
searchUsedRef.current = true;
|
||||
}
|
||||
},
|
||||
[noInteractions, setSearch]
|
||||
[noInteractions]
|
||||
);
|
||||
|
||||
const prevResult = useCallback(() => {
|
||||
@@ -69,27 +78,19 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
|
||||
setCurrentResult(null);
|
||||
return;
|
||||
}
|
||||
if (currentResult === null) {
|
||||
if (!currentResult) {
|
||||
setCurrentResult(0);
|
||||
// No need to filter if we're only showing matching logs, otherwise scroll to the first result.
|
||||
if (!filterLogs) {
|
||||
listRef?.scrollToItem(logs.indexOf(matches[0]), 'center');
|
||||
}
|
||||
listRef?.scrollToItem(logs.indexOf(matches[0]), 'center');
|
||||
}
|
||||
}, [currentResult, filterLogs, listRef, logs, matches]);
|
||||
}, [currentResult, listRef, logs, matches]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchVisible) {
|
||||
setSearch('');
|
||||
setContextSearch(undefined);
|
||||
setMatchingUids(null);
|
||||
}
|
||||
}, [searchVisible, setMatchingUids]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputRef.current || !search) {
|
||||
return;
|
||||
}
|
||||
inputRef.current.value = search;
|
||||
}, [search]);
|
||||
}, [searchVisible, setContextSearch, setMatchingUids]);
|
||||
|
||||
useEffect(() => {
|
||||
const newMatchingUids = matches.map((log) => log.uid);
|
||||
@@ -103,12 +104,13 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
|
||||
.forEach((log) => log.setCurrentSearch(undefined));
|
||||
}
|
||||
|
||||
setContextSearch(search ? search : undefined);
|
||||
if (!sameLogs) {
|
||||
setMatchingUids(newMatchingUids.length ? newMatchingUids : null);
|
||||
} else if (!matches.length) {
|
||||
setMatchingUids(null);
|
||||
}
|
||||
}, [logs, matches, matchingUids, search, setMatchingUids]);
|
||||
}, [logs, matches, matchingUids, search, setContextSearch, setMatchingUids]);
|
||||
|
||||
if (!searchVisible) {
|
||||
return null;
|
||||
@@ -124,7 +126,6 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
|
||||
onChange={handleChange}
|
||||
autoFocus
|
||||
placeholder={t('logs.log-list-search.input-placeholder', 'Search in logs')}
|
||||
ref={inputRef}
|
||||
suffix={suffix}
|
||||
/>
|
||||
</div>
|
||||
@@ -140,22 +141,22 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
|
||||
onClick={prevResult}
|
||||
disabled={!matches || !matches.length}
|
||||
name="angle-up"
|
||||
tooltip={t('logs.log-list-search.prev', 'Previous result')}
|
||||
aria-label={t('logs.log-list-search.prev', 'Previous result')}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={nextResult}
|
||||
disabled={!matches || !matches.length}
|
||||
name="angle-down"
|
||||
tooltip={t('logs.log-list-search.next', 'Next result')}
|
||||
aria-label={t('logs.log-list-search.next', 'Next result')}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={toggleFilterLogs}
|
||||
disabled={!matches || !matches.length}
|
||||
className={filterLogs ? styles.controlButtonActive : undefined}
|
||||
name="filter"
|
||||
tooltip={t('logs.log-list-search.filter', 'Filter matching logs')}
|
||||
aria-label={t('logs.log-list-search.filter', 'Filter matching logs')}
|
||||
/>
|
||||
<IconButton onClick={hideSearch} name="times" tooltip={t('logs.log-list-search.close', 'Close search')} />
|
||||
<IconButton onClick={hideSearch} name="times" aria-label={t('logs.log-list-search.close', 'Close search')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface LogListSearchContextData {
|
||||
search?: string;
|
||||
searchVisible?: boolean;
|
||||
setMatchingUids: (matches: string[] | null) => void;
|
||||
setSearch: (search: string) => void;
|
||||
setSearch: (search: string | undefined) => void;
|
||||
showSearch: () => void;
|
||||
toggleFilterLogs: () => void;
|
||||
}
|
||||
@@ -33,14 +33,13 @@ export const useLogListSearchContext = (): LogListSearchContextData => {
|
||||
};
|
||||
|
||||
export const LogListSearchContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [search, setSearch] = useState<string | undefined>(undefined);
|
||||
const [searchVisible, setSearchVisible] = useState(false);
|
||||
const [matchingUids, setMatchingUids] = useState<string[] | null>(null);
|
||||
const [filterLogs, setFilterLogs] = useState(false);
|
||||
|
||||
const hideSearch = useCallback(() => {
|
||||
setSearchVisible(false);
|
||||
setSearch('');
|
||||
}, []);
|
||||
|
||||
const showSearch = useCallback(() => {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { CoreApp, LogRowModel } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
|
||||
import { identifyOTelLanguages } from '../otel/formats';
|
||||
|
||||
export const reportInteractionOnce = (interactionName: string, properties?: Record<string, unknown>) => {
|
||||
const key = `logs.interactions.${interactionName}`;
|
||||
if (sessionStorage.getItem(key)) {
|
||||
@@ -11,56 +8,3 @@ export const reportInteractionOnce = (interactionName: string, properties?: Reco
|
||||
sessionStorage.setItem(key, '1');
|
||||
reportInteraction(interactionName, properties);
|
||||
};
|
||||
|
||||
export function collectInsights(logs: LogRowModel[], app: CoreApp, properties?: Record<string, unknown>) {
|
||||
if (!logs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { longest, shortest, average, median } = getLogsStats(logs);
|
||||
|
||||
reportInteractionOnce(`logs_log_list_${app}_logs_displayed`, {
|
||||
...properties,
|
||||
otelLanguage: identifyOTelLanguages(logs).join(', '),
|
||||
ansi: logs.some((logs) => logs.hasAnsi),
|
||||
unescaped: logs.some((logs) => logs.hasUnescapedContent),
|
||||
dsType: logs[0]?.datasourceType ?? '',
|
||||
count: logs.length,
|
||||
longestLog: longest,
|
||||
shortestLog: shortest,
|
||||
averageLog: average,
|
||||
medianLog: median,
|
||||
});
|
||||
}
|
||||
|
||||
function getLogsStats(logs: LogRowModel[]) {
|
||||
let longest = 0,
|
||||
shortest = logs[0].raw.length,
|
||||
median = 0;
|
||||
|
||||
const lengths: number[] = [];
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
let length = logs[i].raw.length;
|
||||
if (length > longest) {
|
||||
longest = length;
|
||||
} else if (length < shortest) {
|
||||
shortest = length;
|
||||
}
|
||||
sum += length;
|
||||
lengths.push(length);
|
||||
}
|
||||
|
||||
lengths.sort((a, b) => a - b);
|
||||
|
||||
const mid = Math.floor(lengths.length / 2);
|
||||
|
||||
if (lengths.length % 2 === 0) {
|
||||
median = (lengths[mid - 1] + lengths[mid]) / 2;
|
||||
} else {
|
||||
median = lengths[mid];
|
||||
}
|
||||
|
||||
return { longest, shortest, average: Math.round(sum / logs.length), median };
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ function countNewLines(log: string, limit = Infinity) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < log.length; ++i) {
|
||||
// No need to iterate further
|
||||
if (count > limit) {
|
||||
if (count > Infinity) {
|
||||
return count;
|
||||
}
|
||||
if (log[i] === '\n') {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const useKeyBindings = () => {
|
||||
const { showDetails, detailsMode, closeDetails } = useLogDetailsContext();
|
||||
|
||||
useEffect(() => {
|
||||
function handleOpenSearch(event: KeyboardEvent) {
|
||||
function handleToggleSearch(event: KeyboardEvent) {
|
||||
const isMac = navigator.userAgent.includes('Mac');
|
||||
const isFKey = event.key === 'f' || event.key === 'F';
|
||||
|
||||
@@ -23,8 +23,6 @@ export const useKeyBindings = () => {
|
||||
showSearch();
|
||||
return;
|
||||
}
|
||||
}
|
||||
function handleClose(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && searchVisible) {
|
||||
hideSearch();
|
||||
}
|
||||
@@ -32,11 +30,9 @@ export const useKeyBindings = () => {
|
||||
closeDetails();
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleOpenSearch);
|
||||
document.addEventListener('keyup', handleClose);
|
||||
document.addEventListener('keydown', handleToggleSearch);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleOpenSearch);
|
||||
document.removeEventListener('keyup', handleClose);
|
||||
document.removeEventListener('keydown', handleToggleSearch);
|
||||
};
|
||||
}, [closeDetails, detailsMode, hideSearch, searchVisible, showDetails.length, showSearch]);
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
this.subs.add(
|
||||
this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
|
||||
if (this.mapDiv && this.props.id === evt.payload) {
|
||||
this.initMapAsync(this.mapDiv);
|
||||
this.initMapRef(this.mapDiv);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -97,7 +97,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
});
|
||||
|
||||
if (hasDependencies) {
|
||||
this.initMapAsync(this.mapDiv);
|
||||
this.initMapRef(this.mapDiv);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -182,7 +182,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
|
||||
if (noRepeatChanged) {
|
||||
if (this.mapDiv) {
|
||||
this.initMapAsync(this.mapDiv);
|
||||
this.initMapRef(this.mapDiv);
|
||||
}
|
||||
// Skip other options processing
|
||||
return;
|
||||
@@ -227,7 +227,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
this.setState({ legends: this.getLegends() });
|
||||
}
|
||||
|
||||
initMapAsync = async (div: HTMLDivElement | null) => {
|
||||
initMapRef = async (div: HTMLDivElement) => {
|
||||
if (!div) {
|
||||
// Do not initialize new map or dispose old map
|
||||
return;
|
||||
@@ -437,10 +437,6 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
return legends;
|
||||
}
|
||||
|
||||
initMapRef = (div: HTMLDivElement | null) => {
|
||||
this.initMapAsync(div);
|
||||
};
|
||||
|
||||
render() {
|
||||
let { ttip, ttipOpen, topRight1, legends, topRight2 } = this.state;
|
||||
const { options } = this.props;
|
||||
|
||||
@@ -9314,9 +9314,7 @@
|
||||
"remove-button": "Remove {{children}}"
|
||||
},
|
||||
"viz-legend": {
|
||||
"right-axis-indicator": "(right y-axis)",
|
||||
"show-all-series": "Show all series",
|
||||
"show-all-series-tooltip": "Currently loaded series are hidden by previous selection. Click to show all series."
|
||||
"right-axis-indicator": "(right y-axis)"
|
||||
},
|
||||
"viz-tooltip": {
|
||||
"actions-confirmation-input-placeholder": "Are you sure you want to {{ actionTitle }}?",
|
||||
@@ -10183,8 +10181,7 @@
|
||||
"copy": "Copy selection",
|
||||
"disable-menu": "Disable menu",
|
||||
"line-contains": "Add as line contains filter",
|
||||
"line-contains-not": "Add as line does not contain filter",
|
||||
"search-text": "Search in results"
|
||||
"line-contains-not": "Add as line does not contain filter"
|
||||
},
|
||||
"show-log-attributes": "Display log attributes for OTel logs",
|
||||
"timestamp-format": "Timestamp resolution",
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -3446,17 +3446,17 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/plugin-e2e@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "@grafana/plugin-e2e@npm:3.1.0"
|
||||
"@grafana/plugin-e2e@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "@grafana/plugin-e2e@npm:3.0.3"
|
||||
dependencies:
|
||||
"@grafana/e2e-selectors": "npm:12.4.0-20165274911"
|
||||
"@grafana/e2e-selectors": "npm:^12.4.0-19890644192"
|
||||
semver: "npm:^7.5.4"
|
||||
uuid: "npm:^13.0.0"
|
||||
yaml: "npm:^2.3.4"
|
||||
peerDependencies:
|
||||
"@playwright/test": ^1.52.0
|
||||
checksum: 10/a4003a1c594e8ecd771a8ab7af77357cfe2d942dee821d922e317da2eb7962f86c19bddcc4bdf4d0c793b3ebfec409095b25022b0e3664d66b93163e61cc3fb5
|
||||
checksum: 10/d9247d52cc5b65c91983212790610b0c2470d705fa4e37178d04cbf681c4fc80bb08b2c39ea6f0061cb0d78242f108a5c8e6fa1ad2f2209d6f15d34000cb1d00
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -19480,7 +19480,7 @@ __metadata:
|
||||
"@grafana/llm": "npm:1.0.1"
|
||||
"@grafana/monaco-logql": "npm:^0.0.8"
|
||||
"@grafana/o11y-ds-frontend": "workspace:*"
|
||||
"@grafana/plugin-e2e": "npm:^3.1.0"
|
||||
"@grafana/plugin-e2e": "npm:^3.0.3"
|
||||
"@grafana/plugin-ui": "npm:^0.11.1"
|
||||
"@grafana/prometheus": "workspace:*"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user