diff --git a/.betterer.results b/.betterer.results index 7e0ee1091bd..63208ca90b0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -208,7 +208,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "7"] ], "packages/grafana-data/src/geo/layer.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "packages/grafana-data/src/panel/PanelPlugin.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -792,9 +794,6 @@ exports[`better eslint`] = { "packages/grafana-runtime/src/analytics/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-runtime/src/components/DataSourcePicker.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] - ], "packages/grafana-runtime/src/components/PanelRenderer.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -1871,6 +1870,9 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/alerting/unified/components/receivers/TemplateForm.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -2729,10 +2731,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Do not use any type assertions.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], @@ -2744,8 +2746,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "16"], [0, 0, 0, "Unexpected any. Specify a different type.", "17"], [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"], - [0, 0, 0, "Unexpected any. Specify a different type.", "20"] + [0, 0, 0, "Unexpected any. Specify a different type.", "19"] ], "public/app/features/manage-dashboards/state/reducers.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -3560,13 +3561,7 @@ exports[`better eslint`] = { "public/app/plugins/datasource/alertmanager/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/.drone.yml b/.drone.yml index e59cba53b00..aba51e371e7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2417,6 +2417,8 @@ trigger: - '*.md' - docs/** - latest.json + repo: + - grafana/grafana type: docker volumes: - host: @@ -2740,7 +2742,7 @@ steps: from_secret: packages_gpg_private_key gpg_public_key: from_secret: packages_gpg_public_key - package_path: gs://grafana-prerelease/artifacts/downloads/*$${DRONE_TAG}/oss/**.deb + package_path: gs://grafana-prerelease/artifacts/downloads/*${DRONE_TAG}/oss/**.deb secret_access_key: from_secret: packages_secret_access_key service_account_json: @@ -2761,7 +2763,7 @@ steps: from_secret: packages_gpg_private_key gpg_public_key: from_secret: packages_gpg_public_key - package_path: gs://grafana-prerelease/artifacts/downloads/*$${DRONE_TAG}/oss/**.rpm + package_path: gs://grafana-prerelease/artifacts/downloads/*${DRONE_TAG}/oss/**.rpm secret_access_key: from_secret: packages_secret_access_key service_account_json: @@ -2774,7 +2776,7 @@ steps: - publish-linux-packages-rpm environment: GCP_KEY: - from_secret: gcp_grafanauploads + from_secret: gcp_grafanauploads_base64 GRAFANA_COM_API_KEY: from_secret: grafana_api_key image: grafana/grafana-ci-deploy:1.3.3 @@ -3129,7 +3131,7 @@ steps: - windows-init environment: GCP_KEY: - from_secret: gcp_key + from_secret: gcp_grafanauploads_base64 GITHUB_TOKEN: from_secret: github_token PRERELEASE_BUCKET: @@ -3878,7 +3880,7 @@ steps: - windows-init environment: GCP_KEY: - from_secret: gcp_key + from_secret: gcp_grafanauploads_base64 GITHUB_TOKEN: from_secret: github_token PRERELEASE_BUCKET: @@ -4562,6 +4564,6 @@ kind: secret name: delivery-bot-app-private-key --- kind: signature -hmac: 96c0aa2795b34cff5fd37f6f97dd6444c820f9dfd502adb98870193a313717cc +hmac: d8d0924bde06def64d1267fa3ac77d25c978e4cfde2bf31f1f2821a18c909876 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c13a332333e..b183e5a6a8b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -602,6 +602,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/codeowners-validator.yml @tolzhabayev /.github/workflows/codeql-analysis.yml @DanCech /.github/workflows/commands.yml @torkelo +/.github/workflows/community-release.yml @grafana/grafana-delivery /.github/workflows/detect-breaking-changes-* @grafana/plugins-platform-frontend /.github/workflows/doc-validator.yml @grafana/docs-tooling /.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina diff --git a/.github/workflows/community-release.yml b/.github/workflows/community-release.yml new file mode 100644 index 00000000000..502f96192c7 --- /dev/null +++ b/.github/workflows/community-release.yml @@ -0,0 +1,25 @@ +name: Create community release post +on: + workflow_dispatch: + inputs: + version: + required: true + description: 'Needs to match, exactly, the name of a milestone. The version to be released please respect: major.minor.patch, major.minor.patch-preview or major.minor.patch-preview format. example: 7.4.3, 7.4.3-preview or 7.4.3-preview1' +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: "Generate token" + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }} + private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }} + - name: Run community-release (manually invoked) + uses: grafana/grafana-github-actions-go/community-release@main + with: + token: ${{ steps.generate_token.outputs.token }} + version: ${{ inputs.version }} + metrics_api_key: ${{ secrets.GRAFANA_MISC_STATS_API_KEY }} + community_api_key: ${{ secrets.GRAFANABOT_FORUM_KEY }} + community_api_username: grafanabot diff --git a/.github/workflows/detect-breaking-changes-report.yml b/.github/workflows/detect-breaking-changes-report.yml index 397729aad36..f69a58417f7 100644 --- a/.github/workflows/detect-breaking-changes-report.yml +++ b/.github/workflows/detect-breaking-changes-report.yml @@ -5,6 +5,9 @@ on: workflows: ["Levitate / Detect breaking changes"] types: [completed] +permissions: + pull-requests: write + jobs: notify: name: Report @@ -35,7 +38,7 @@ jobs: run_id: runId, }); const artifact = artifacts.data.artifacts.find(a => a.name === artifactName); - + if (!artifact) { throw new Error(`Could not find artifact ${ artifactName } in workflow (${ runId })`); } @@ -49,11 +52,11 @@ jobs: fs.mkdirSync(artifactFolder, { recursive: true }); fs.writeFileSync(`${ artifactFolder }/${ artifactName }.zip`, Buffer.from(download.data)); - + # Unzip artifact - name: Unzip artifact run: unzip "${ARTIFACT_FOLDER}/${ARTIFACT_NAME}.zip" -d "${ARTIFACT_FOLDER}" - + # Parse the artifact and register fields as step output variables # (All fields in the JSON will be available as ${{ steps.levitate-run.outputs. }} - name: Parsing levitate result @@ -137,7 +140,7 @@ jobs: env: PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }} with: - github-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.issues.addLabels({ issue_number: process.env.PR_NUMBER, @@ -153,7 +156,7 @@ jobs: env: PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }} with: - github-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.issues.removeLabel({ issue_number: process.env.PR_NUMBER, @@ -171,7 +174,7 @@ jobs: env: PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }} with: - github-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.pulls.requestReviewers({ pull_number: process.env.PR_NUMBER, @@ -188,7 +191,7 @@ jobs: env: PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }} with: - github-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.pulls.removeRequestedReviewers({ pull_number: process.env.PR_NUMBER, diff --git a/.github/workflows/pr-commands.yml b/.github/workflows/pr-commands.yml index e034b8993d1..d868efb6926 100644 --- a/.github/workflows/pr-commands.yml +++ b/.github/workflows/pr-commands.yml @@ -19,9 +19,15 @@ jobs: ref: main - name: Install Actions run: npm install --production --prefix ./actions + - name: "Generate token" + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_ID }} + private_key: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_PEM }} - name: Run Commands uses: ./actions/commands with: metricsWriteAPIKey: ${{secrets.GRAFANA_MISC_STATS_API_KEY}} - token: ${{secrets.GITHUB_TOKEN}} + token: ${{ steps.generate_token.outputs.token }} configPath: pr-commands diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 598193f696a..de50fe2f26b 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -3,13 +3,16 @@ on: schedule: - cron: '30 1 * * *' +permissions: + pull-requests: write + jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v8 with: - repo-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + repo-token: ${{ secrets.GITHUB_TOKEN }} # Number of days of inactivity before a stale Issue or Pull Request is closed. # Set to -1 to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. days-before-close: 14 diff --git a/CHANGELOG.md b/CHANGELOG.md index 90da0d8c979..d77da361b82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ + + +# 10.0.3 (2023-07-18) + +### Features and enhancements + +- **Alerting:** Sort NumberCaptureValues in EvaluationString. [#71931](https://github.com/grafana/grafana/issues/71931), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Alerting:** No longer silence paused alerts during legacy migration. [#71761](https://github.com/grafana/grafana/issues/71761), [@JacobsonMT](https://github.com/JacobsonMT) +- **Auth:** Add support for custom signing keys in auth.azure_ad. [#71708](https://github.com/grafana/grafana/issues/71708), [@Jguer](https://github.com/Jguer) +- **Chore:** Upgrade Go to 1.20.6. [#71445](https://github.com/grafana/grafana/issues/71445), [@sakjur](https://github.com/sakjur) +- **Chore:** Upgrade Go to 1.20.6. (Enterprise) + +### Bug fixes + +- **Alerting:** Fix edit / view of webhook contact point when no authorization is set. [#71972](https://github.com/grafana/grafana/issues/71972), [@gillesdemey](https://github.com/gillesdemey) +- **AzureMonitor:** Set timespan in Logs Portal URL link. [#71910](https://github.com/grafana/grafana/issues/71910), [@aangelisc](https://github.com/aangelisc) +- **Plugins:** Only configure plugin proxy transport once. [#71742](https://github.com/grafana/grafana/issues/71742), [@wbrowne](https://github.com/wbrowne) +- **Elasticsearch:** Fix multiple max depth flatten of multi-level objects. [#71636](https://github.com/grafana/grafana/issues/71636), [@fridgepoet](https://github.com/fridgepoet) +- **Elasticsearch:** Fix histogram colors in backend mode. [#71447](https://github.com/grafana/grafana/issues/71447), [@gabor](https://github.com/gabor) +- **Alerting:** Fix state in expressions footer. [#71443](https://github.com/grafana/grafana/issues/71443), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **AppChromeService:** Fixes update to breadcrumb parent URL. [#71418](https://github.com/grafana/grafana/issues/71418), [@torkelo](https://github.com/torkelo) +- **Elasticsearch:** Fix using multiple indexes with comma separated string. [#71322](https://github.com/grafana/grafana/issues/71322), [@gabor](https://github.com/gabor) +- **Alerting:** Fix Alertmanager change detection for receivers with secure settings. [#71320](https://github.com/grafana/grafana/issues/71320), [@JacobsonMT](https://github.com/JacobsonMT) +- **Transformations:** Fix `extractFields` throwing Error if one value is undefined or null. [#71267](https://github.com/grafana/grafana/issues/71267), [@svennergr](https://github.com/svennergr) +- **XYChart:** Point size editor should reflect correct default (5). [#71229](https://github.com/grafana/grafana/issues/71229), [@Develer](https://github.com/Develer) +- **Annotations:** Fix database lock while updating annotations. [#71207](https://github.com/grafana/grafana/issues/71207), [@sakjur](https://github.com/sakjur) +- **TimePicker:** Fix issue with previous fiscal quarter not parsing correctly. [#71093](https://github.com/grafana/grafana/issues/71093), [@ashharrison90](https://github.com/ashharrison90) +- **AzureMonitor:** Correctly build multi-resource queries for Application Insights components. [#71039](https://github.com/grafana/grafana/issues/71039), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Fix metric names for multi-resources. [#70994](https://github.com/grafana/grafana/issues/70994), [@asimpson](https://github.com/asimpson) +- **Logs:** Do not insert log-line into log-fields in json download. [#70954](https://github.com/grafana/grafana/issues/70954), [@gabor](https://github.com/gabor) +- **Loki:** Fix wrong query expression with inline comments. [#70948](https://github.com/grafana/grafana/issues/70948), [@svennergr](https://github.com/svennergr) + + # 10.0.2 (2023-07-11) @@ -407,6 +440,46 @@ The `database` field has been deprecated in the Elasticsearch datasource provisi - **InteractiveTable:** Updated design and minor tweak to Correlactions page. [#66443](https://github.com/grafana/grafana/issues/66443), [@torkelo](https://github.com/torkelo) + + +# 9.5.7 (2023-07-20) + +### Features and enhancements + +- **Alerting:** Sort NumberCaptureValues in EvaluationString. [#71930](https://github.com/grafana/grafana/issues/71930), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Alerting:** No longer silence paused alerts during legacy migration. [#71765](https://github.com/grafana/grafana/issues/71765), [@JacobsonMT](https://github.com/JacobsonMT) +- **Chore:** Upgrade Go to 1.20.6. [#71446](https://github.com/grafana/grafana/issues/71446), [@sakjur](https://github.com/sakjur) +- **Alerting:** Remove and revert flag alertingBigTransactions. [#70910](https://github.com/grafana/grafana/issues/70910), [@santihernandezc](https://github.com/santihernandezc) +- **Alerting:** Migrate unknown NoData\Error settings to the default. [#70905](https://github.com/grafana/grafana/issues/70905), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Tempo:** Escape regex-sensitive characters in span name before building promql query. [#68318](https://github.com/grafana/grafana/issues/68318), [@joey-grafana](https://github.com/joey-grafana) +- **Alerting:** Update grafana/alerting to ce9fba9. [#67685](https://github.com/grafana/grafana/issues/67685), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Chore:** Upgrade Go to 1.20.6. (Enterprise) + +### Bug fixes + +- **Plugins:** Only configure plugin proxy transport once. [#71741](https://github.com/grafana/grafana/issues/71741), [@wbrowne](https://github.com/wbrowne) +- **Alerting:** Fix unique violation when updating rule group with title chains/cycles. [#71330](https://github.com/grafana/grafana/issues/71330), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Fix:** Change getExistingDashboardByTitleAndFolder to get dashboard by title, not slug. [#70961](https://github.com/grafana/grafana/issues/70961), [@yangkb09](https://github.com/yangkb09) +- **Alerting:** Convert 'Both' type Prometheus queries to 'Range' in migration. [#70907](https://github.com/grafana/grafana/issues/70907), [@JacobsonMT](https://github.com/JacobsonMT) +- **Alerting:** Support newer http_config struct. [#69718](https://github.com/grafana/grafana/issues/69718), [@gillesdemey](https://github.com/gillesdemey) +- **InfluxDB:** Interpolate retention policies. [#69299](https://github.com/grafana/grafana/issues/69299), [@itsmylife](https://github.com/itsmylife) +- **StatusHistory:** Fix rendering of value-mapped null. [#69107](https://github.com/grafana/grafana/issues/69107), [@leeoniya](https://github.com/leeoniya) +- **Alerting:** Fix provenance guard checks for Alertmanager configuration to not cause panic when compared nested objects. [#69092](https://github.com/grafana/grafana/issues/69092), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **AnonymousAuth:** Fix concurrent read-write crash. [#68802](https://github.com/grafana/grafana/issues/68802), [@Jguer](https://github.com/Jguer) +- **AzureMonitor:** Ensure legacy properties containing template variables are correctly migrated. [#68790](https://github.com/grafana/grafana/issues/68790), [@aangelisc](https://github.com/aangelisc) +- **Explore:** Remove data source onboarding page. [#68643](https://github.com/grafana/grafana/issues/68643), [@harisrozajac](https://github.com/harisrozajac) +- **Dashboard:** Re-align Save form. [#68625](https://github.com/grafana/grafana/issues/68625), [@polibb](https://github.com/polibb) +- **Azure Monitor:** Fix bug that did not show alert rule preview. [#68582](https://github.com/grafana/grafana/issues/68582), [@alyssabull](https://github.com/alyssabull) +- **Histogram:** Respect min/max panel settings for x-axis. [#68244](https://github.com/grafana/grafana/issues/68244), [@leeoniya](https://github.com/leeoniya) +- **Heatmap:** Fix color rendering for value ranges < 1. [#68163](https://github.com/grafana/grafana/issues/68163), [@leeoniya](https://github.com/leeoniya) +- **Heatmap:** Handle unsorted timestamps in calculate mode. [#68150](https://github.com/grafana/grafana/issues/68150), [@leeoniya](https://github.com/leeoniya) +- **Google Cloud Monitor:** Fix mem usage for dropdown. [#67949](https://github.com/grafana/grafana/issues/67949), [@asimpson](https://github.com/asimpson) +- **AzureMonitor:** Fix logs query multi-resource and timespan values. [#67932](https://github.com/grafana/grafana/issues/67932), [@aangelisc](https://github.com/aangelisc) +- **Utils:** Reimplement util.GetRandomString to avoid modulo bias. [#66970](https://github.com/grafana/grafana/issues/66970), [@DanCech](https://github.com/DanCech) +- **License:** Enable FeatureUserLimit for all products. (Enterprise) +- **Auth:** Remove ldap init sync. (Enterprise) + + # 9.5.6 (2023-07-11) diff --git a/conf/defaults.ini b/conf/defaults.ini index 76542724d10..b3cc15b3586 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -612,7 +612,7 @@ allow_sign_up = true auto_login = false client_id = some_id client_secret = -scopes = api +scopes = openid email profile auth_url = https://gitlab.com/oauth/authorize token_url = https://gitlab.com/oauth/token api_url = https://gitlab.com/api/v4 @@ -1278,7 +1278,7 @@ news_feed_enabled = true #################################### Query ############################# [query] # Set the number of data source queries that can be executed concurrently in mixed queries. Default is the number of CPUs. -concurrent_query_limit = +concurrent_query_limit = #################################### Query History ############################# [query_history] diff --git a/conf/sample.ini b/conf/sample.ini index 960f917dc71..265cb8a9b9a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -595,7 +595,7 @@ ;auto_login = false ;client_id = some_id ;client_secret = some_secret -;scopes = api +;scopes = openid email profile ;auth_url = https://gitlab.com/oauth/authorize ;token_url = https://gitlab.com/oauth/token ;api_url = https://gitlab.com/api/v4 diff --git a/devenv/docker/blocks/auth/oauth/docker-build-keycloak-m1-image.sh b/devenv/docker/blocks/auth/oauth/docker-build-keycloak-m1-image.sh deleted file mode 100644 index 46c1ef09fa3..00000000000 --- a/devenv/docker/blocks/auth/oauth/docker-build-keycloak-m1-image.sh +++ /dev/null @@ -1,9 +0,0 @@ -#/bin/sh - -VERSION=12.0.1 # set version here - -cd /tmp -git clone git@github.com:keycloak/keycloak-containers.git -cd keycloak-containers/server -git checkout $VERSION -docker build -t "quay.io/keycloak/keycloak:${VERSION}" . diff --git a/devenv/docker/blocks/auth/oauth/docker-compose.yaml b/devenv/docker/blocks/auth/oauth/docker-compose.yaml index 762e3c5a7a9..f322a879757 100644 --- a/devenv/docker/blocks/auth/oauth/docker-compose.yaml +++ b/devenv/docker/blocks/auth/oauth/docker-compose.yaml @@ -10,7 +10,7 @@ restart: unless-stopped oauthkeycloak: - image: quay.io/keycloak/keycloak:21.1 + image: quay.io/keycloak/keycloak:22.0 container_name: oauthkeycloak command: --spi-login-protocol-openid-connect-legacy-logout-redirect-uri=true start-dev environment: diff --git a/devenv/docker/blocks/auth/oauth/readme.md b/devenv/docker/blocks/auth/oauth/readme.md index a935f2c8682..e9bdab5e1ba 100644 --- a/devenv/docker/blocks/auth/oauth/readme.md +++ b/devenv/docker/blocks/auth/oauth/readme.md @@ -120,20 +120,3 @@ docker-compose exec -T oauthkeycloakdb bash -c "pg_dump -U keycloak keycloak" > - grafana oauth editor login: oauth-editor:grafana - grafana oauth admin login: oauth-admin:grafana - grafana oauth server admin login: oauth-grafanaadmin:grafana - -# Troubleshooting - -## Mac M1 Users - -The new arm64 architecture does not build for the latest docker image of keycloak. Refer to https://github.com/docker/for-mac/issues/5310 for the issue to see if it resolved. -Until then you need to build the docker image locally and then run `devenv`. - -1. Remove any lingering keycloak image -```sh -$ docker rmi $(docker images | grep 'keycloak') -``` -1. Build keycloak image locally -```sh -$ ./docker-build-keycloak-m1-image.sh -``` -1. Start from beginning of this readme diff --git a/devenv/docker/blocks/postgres/.env b/devenv/docker/blocks/postgres/.env index 8bd1265589e..e916eb4ee21 100644 --- a/devenv/docker/blocks/postgres/.env +++ b/devenv/docker/blocks/postgres/.env @@ -1 +1 @@ -postgres_version=11.20 +postgres_version=11.20-alpine3.18 diff --git a/devenv/docker/blocks/postgres_tests/.env b/devenv/docker/blocks/postgres_tests/.env index 8bd1265589e..e916eb4ee21 100644 --- a/devenv/docker/blocks/postgres_tests/.env +++ b/devenv/docker/blocks/postgres_tests/.env @@ -1 +1 @@ -postgres_version=11.20 +postgres_version=11.20-alpine3.18 diff --git a/devenv/docker/blocks/postgres_tests/Dockerfile b/devenv/docker/blocks/postgres_tests/Dockerfile index aad5e348cf5..090fb4a52b3 100644 --- a/devenv/docker/blocks/postgres_tests/Dockerfile +++ b/devenv/docker/blocks/postgres_tests/Dockerfile @@ -1,4 +1,4 @@ -ARG postgres_version=11.20 +ARG postgres_version=11.20-alpine3.18 FROM postgres:${postgres_version} ADD setup.sql /docker-entrypoint-initdb.d RUN chown -R postgres:postgres /docker-entrypoint-initdb.d/ diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index 36377ae8894..da1bda8d87d 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -22,21 +22,15 @@ Configure the features and integrations that you need to create and manage your **Configure alert rules** -An alert rule is a set of evaluation criteria that determines whether an alert will fire. The alert rule consists of one or more queries and expressions, a condition, the frequency of evaluation, and optionally, the duration over which the condition is met. +[Configure Grafana-managed alert rules][create-grafana-managed-rule]. -While queries and expressions select the data set to evaluate, a condition sets the threshold that an alert must meet or exceed to create an alert. An interval specifies how frequently an alert rule is evaluated. Duration, when configured, indicates how long a condition must be met. Alert rules can also define alerting behavior in the absence of data. +[Configure data source-managed alert rules][create-mimir-loki-managed-rule] -You can: +**Configure recording rules** -- [Create Grafana Mimir or Loki managed alert rules][create-mimir-loki-managed-rule]. -- [Create Grafana Mimir or Loki managed recording rules][create-mimir-loki-managed-recording-rule]. -- [Edit Grafana Mimir or Loki rule groups and namespaces][edit-mimir-loki-namespace-group]. -- [Create Grafana managed alert rules][create-grafana-managed-rule]. +_Recording rules are only available for compatible Prometheus or Loki data sources._ -**Note:** -Grafana managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. - -Alert rules for an external Grafana Mimir or Loki instance can be edited or deleted by users with Editor or Admin roles. +For more information, see [Configure recording rules][create-mimir-loki-managed-recording-rule]. **Configure contact points** diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 1259d24e016..8ed5b22cd95 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -2,76 +2,147 @@ aliases: - ../unified-alerting/alerting-rules/create-grafana-managed-rule/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/ -description: Create Grafana managed alert rule +description: Configure Grafana-managed alert rules keywords: - grafana - alerting - guide - rules - create + - grafana-managed + - data source-managed labels: products: - cloud - enterprise - oss -title: Create Grafana-managed alert rules -weight: 400 +title: Configure Grafana-managed alert rules +weight: 100 --- -# Create Grafana-managed alert rules +# Configure Grafana-managed alert rules Grafana-managed rules are the most flexible alert rule type. They allow you to create alerts that can act on data from any of our supported data sources. In addition to supporting multiple data sources, you can also add expressions to transform your data and set alert conditions. Using images in alert notifications is also supported. This is the only type of rule that allows alerting from multiple data sources in a single rule definition. Multiple alert instances can be created as a result of one alert rule (also known as a multi-dimensional alerting). -For information on Grafana Alerting, see [Introduction to Grafana Alerting][fundamentals], which explains the key concepts and features of Grafana Alerting. +**Note:** -Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}} +Grafana managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. -To create a Grafana-managed alert rule, complete the following steps. +Watch this video to learn more about creating alert rules: {{< vimeo 720001934 >}} -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -2. Click **Alert rules**. -3. Click **+ Create alert rule**. The new alert rule page opens where the **Grafana managed alerts** option is selected by default. -4. In Step 1, add the rule name. - - In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. -5. In Step 2, add queries and expressions to evaluate, and then select the alert condition. +In the following sections, we’ll guide you through the process of creating your Grafana-managed alert rules. - - For queries, select a data source from the dropdown. - - Specify a [time range][time-units-and-relative-ranges]. +To create a Grafana-managed alert rule, use the in-product alert creation flow and follow these steps to help you. - **Note:** - Grafana Alerting only supports fixed relative time ranges, for example, `now-24hr: now`. +## Set alert rule name - It does not support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`. +1. Click **Alerts & IRM** -> **Alert rules** -> **+ New alert rule**. +1. Enter a name to identify your alert rule. - - Add one or more [queries][add-a-query] or [expressions][expression-queries]. - - For each expression, select either **Classic condition** to create a single alert rule, or choose from the **Math**, **Reduce**, and **Resample** options to generate separate alert for each series. For details on these options, see [Single and multi dimensional rule](#single-and-multi-dimensional-rule). - - Click **Run queries** to verify that the query is successful. - - Next, select the query or expression for your alert condition. + This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. -6. In Step 3, specify the alert evaluation interval. +## Define query and condition - - From the **Condition** dropdown, select the query or expression to trigger the alert rule. - - For **Evaluate every**, specify the frequency of evaluation. Must be a multiple of 10 seconds. For examples, `1m`, `30s`. - - For **Evaluate for**, specify the duration for which the condition must be true before an alert fires. - > **Note:** Once a condition is breached, the alert goes into the Pending state. If the condition remains breached for the duration specified, the alert transitions to the `Firing` state, otherwise it reverts back to the `Normal` state. - - In **Configure no data and error handling**, configure alerting behavior in the absence of data. Use the guidelines in [No data and error handling](#configure-no-data-and-error-handling). - - Click **Preview** to check the result of running the query at this moment. Preview excludes no data and error handling. +Define a query to get the data you want to measure and a condition that needs to be met before an alert rule fires. - **Note:** +1. Select a data source. +1. From the **Options** dropdown, specify a [time range][time-units-and-relative-ranges]. - You can pause alert rule evaluation to prevent noisy alerting while tuning your alerts. Pausing stops alert rule evaluation and does not create any alert instances. This is different to mute timings, which stop notifications from being delivered, but still allow for alert rule evaluation and the creation of alert instances. + **Note:** -7. In Step 4, add the storage location, rule group, as well as additional metadata associated with the rule. - - From the **Folder** dropdown, select the folder where you want to store the rule. - - For **Group**, specify a pre-defined group. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. - - Add a description and summary to customize alert messages. Use the guidelines in [Annotations and labels for alerting][annotation-label]. - - Add Runbook URL, panel, dashboard, and alert IDs. -8. In Step 5, add custom labels. - - Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value . -9. Click **Save** to save the rule or **Save and exit** to save the rule and go back to the Alerting page. -10. Next, create a for the rule. + Grafana Alerting only supports fixed relative time ranges, for example, `now-24hr: now`. + + It does not support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`. + +1. Add a query. + + To add multiple [queries][add-a-query], click **Add query**. + + All alert rules are managed by Grafana by default. If you want to switch to a data source-managed alert rule, click **Switch to data source-managed alert rule**. + +1. Add one or more [expressions][expression-queries]. + a. For each expression, select either **Classic condition** to create a single alert rule, or choose from the **Math**, **Reduce**, and **Resample** options to generate separate alert for each series. + + For details on these options, see [Single and multi dimensional rule] + b. Click **Preview** to verify that the expression is successful. + +1. Click **Set as alert condition** on the query or expression you want to set as your alert condition. + +## Set alert evaluation behavior + +Use alert rule evaluation to determine how frequently an alert rule should be evaluated and how quickly it should change its state. + +To do this, you need to make sure that your alert rule is in the right evaluation group and set a pending period time that works best for your use case. + +1. Select a folder or click **+ New folder**. +1. Select an evaluation group or click **+ New evaluation group**. + + If you are creating a new evaluation group, specify the interval for the group. + + All rules within the same group are evaluated sequentially over the same time interval. + +1. Enter a pending period. + + The pending period is the period in which an alert rule can be in breach of the condition until it fires. + + Once a condition is met, the alert goes into the **Pending** state. If the condition remains active for the duration specified, the alert transitions to the **Firing** state, else it reverts to the **Normal** state. + +1. Turn on pause alert notifications, if required. + + **Note**: + + Pause alert rule evaluation to prevent noisy alerting while tuning your alerts. Pausing stops alert rule evaluation and does not create any alert instances. This is different to mute timings, which stop notifications from being delivered, but still allow for alert rule evaluation and the creation of alert instances. + + You can pause alert rule evaluation to prevent noisy alerting while tuning your alerts. Pausing stops alert rule evaluation and does not create any alert instances. This is different to mute timings, which stop notifications from being delivered, but still allow for alert rule evaluation and the creation of alert instances. + +1. In **Configure no data and error handling**, configure alerting behavior in the absence of data. + + Use the guidelines in [No data and error handling](#configure-no-data-and-error-handling). + +## Add annotations + +Add [annotations][annotation-label]. to provide more context on the alert in your alert notifications. + +Annotations add metadata to provide more information on the alert in your alert notifications. For example, add a **Summary** annotation to tell you which value caused the alert to fire or which server it happened on. + +1. [Optional] Add a summary. + + Short summary of what happened and why. + +2. [Optional] Add a description. + + Description of what the alert rule does. + +3. [Optional] Add a Runbook URL. + + Webpage where you keep your runbook for the alert + +4. [Optional] Add a custom annotation +5. [Optional] Add a dashboard and panel link. + + Links alerts to panels in a dashboard. + +## Configure notifications + +Add labels to your alert rules to set which notification policy should handle your firing alert instances. + +All alert rules and instances, irrespective of their labels, match the default notification policy. If there are no nested policies, or no nested policies match the labels in the alert rule or alert instance, then the default notification policy is the matching policy. + +1. Add labels if you want to change the way your notifications are routed. + + Add custom labels by selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value. + +1. Preview your alert instance routing set up. + + Based on the labels added, alert instances are routed to the following notification policies displayed. + + Expand each notification policy below to view more details. + +1. Click **See details** to view alert routing details and an email preview. + +1. Click **Save rule**. ### Single and multi-dimensional rule diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index 45ba9ea2fcb..929f6276c25 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -17,7 +17,7 @@ labels: - enterprise - oss title: Configure recording rules -weight: 400 +weight: 300 --- # Configure recording rules @@ -46,21 +46,21 @@ This setting has precedence over each individual rule frequency. If a rule frequ To create recording rules, follow these steps. -1. Click **Alerts & IRM** and then **Alerting**. -1. Click **Alert rules**. +1. Click **Alerts & IRM** -> **Alerting** -> + **Alert rules**. 1. Click the **More** dropdown and then **New recording rule**. -1. Add the rule name. +1. Set rule name. The recording rule name must be a Prometheus metric name and contain no whitespace. -1. Select a data source. +1. Define query. - Select your Loki or Prometheus data source. - Enter a query. -1. Add a namespace and a group. +1. Add namespace and group. - From the **Namespace** dropdown, select an existing rule namespace or add a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Grafana Mimir or Loki rule groups and namespaces][edit-mimir-loki-namespace-group]. - From the **Group** dropdown, select an existing group within the selected namespace or add a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. -1. Add custom labels. +1. Add labels. - Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value . 1. Click **Save rule** to save the rule or **Save rule and exit** to save the rule and go back to the Alerting page. diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md index 5035b8271c0..cc58c44ae51 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md @@ -4,7 +4,7 @@ aliases: - ../unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule/ - ../unified-alerting/alerting-rules/create-mimir-loki-managed-rule/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-mimir-loki-managed-rule/ -description: Create Grafana Mimir or Loki managed alerting rule +description: Configure data source-managed alert rules keywords: - grafana - alerting @@ -16,19 +16,23 @@ labels: - cloud - enterprise - oss -title: Create Grafana Mimir or Loki managed alert rules -weight: 400 +title: Configure data source-managed alert rules +weight: 200 --- -# Create Grafana Mimir or Loki managed alert rules +# Configure data source-managed alert rules -Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting][alerting] which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts][fundamentals] of Grafana Alerting. +Create alert rules for an external Grafana Mimir or Loki instance that has ruler API enabled; these are called data source-managed alert rules. + +**Note**: + +Alert rules for an external Grafana Mimir or Loki instance can be edited or deleted by users with Editor or Admin roles. ## Before you begin -- Verify that you have write permission to the Prometheus or Loki data source. Otherwise, you will not be able to create or update Grafana Mimir managed alerting rules. +- Verify that you have write permission to the Prometheus or Loki data source. Otherwise, you will not be able to create or update Grafana Mimir managed alert rules. -- For Grafana Mimir and Loki data sources, enable the ruler API by configuring their respective services. +- For Grafana Mimir and Loki data sources, enable the Ruler API by configuring their respective services. - **Loki** - The `local` rule storage type, default for the Loki data source, supports only viewing of rules. To edit rules, configure one of the other rule storage types. @@ -36,34 +40,84 @@ Grafana allows you to create alerting rules for an external Grafana Mimir or Lok Watch this video to learn more about how to create a Mimir managed alert rule: {{< vimeo 720001865 >}} -_Refer to [Add a Grafana Mimir or Loki managed alerting rule]({{< relref "#add-a-grafana-mimir-or-loki-managed-alerting-rule" >}}) (following) for current instructions._ - {{% admonition type="note" %}} -If you do not want to manage alerting rules for a particular Loki or Prometheus data source, go to its settings and clear the **Manage alerts via Alerting UI** checkbox. +If you do not want to manage alert rules for a particular Loki or Prometheus data source, go to its settings and clear the **Manage alerts via Alerting UI** checkbox. {{% /admonition %}} -## Add a Grafana Mimir or Loki managed alerting rule +In the following sections, we’ll guide you through the process of creating your data source-managed alert rules. -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -1. Click **Alert rules**. -1. Click **+ Create alert rule**. The new alerting rule page opens where the **Grafana managed alerts** option is selected by default. -1. In Step 1, add the rule name. - - In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. -1. In Step 2, select **Mimir or Loki alert** option. - - Next, select your Loki or Prometheus data source and add the query to evaluate. - - Enter a PromQL or LogQL expression to query. The rule fires if the evaluation result has at least one series with a value that is greater than 0. An alert is created for each series. -1. In Step 3, specify the alert evaluation interval. - - In the **For** text box, specify the duration for which the condition must be true before an alert fires. If you specify `5m`, the condition must be true for 5 minutes before the alert fires. - > **Note:** Once a condition is met, the alert goes into the `Pending` state. If the condition remains active for the duration specified, the alert transitions to the `Firing` state, else it reverts to the `Normal` state. -1. In Step 4, add the namespace, rule group, as well as additional metadata associated with the rule. - - From the **Namespace** dropdown, select an existing rule namespace. Otherwise, click **Add new** and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Grafana Mimir or Loki rule groups and namespaces][edit-mimir-loki-namespace-group]. - - From the **Group** dropdown, select an existing group within the selected namespace. Otherwise, click **Add new** and enter a name to create a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. - - Add a description and summary to customize alert messages. Use the guidelines in [Annotations and labels for alerting][annotation-label]. - - Add Runbook URL, panel, dashboard, and alert IDs. -1. In Step 5, add custom labels. - - Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value . -1. Click **Save** to save the rule or **Save and exit** to save the rule and go back to the Alerting page. -1. Next, create a notification for the rule. +To create a data source-managed alert rule, use the in-product alert creation flow and follow these steps to help you. + +## Set alert rule name + +1. Click **Alerts & IRM** -> **Alert rules** -> **+ New alert rule**. +1. Enter a name to identify your alert rule. + + This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. + +## Define query and condition + +Define a query to get the data you want to measure and a condition that needs to be met before an alert rule fires. + +**Note**: + +All alert rules are managed by Grafana by default. To switch to a data source-managed alert rule, click **Switch to data source-managed alert rule**. + +1. Select a data source. +1. Enter a PromQL or LogQL query. +1. Click **Preview alerts**. + +## Set alert evaluation behavior + +Use alert rule evaluation to determine how frequently an alert rule should be evaluated and how quickly it should change its state. + +1. Select a namespace or click **+ New namespace**. +1. Select an evaluation group or click **+ New evaluation group**. + + If you are creating a new evaluation group, specify the interval for the group. + + All rules within the same group are evaluated sequentially over the same time interval. + +1. Enter a pending period. + + The pending period is the period in which an alert rule can be in breach of the condition until it fires. + + Once a condition is met, the alert goes into the **Pending** state. If the condition remains active for the duration specified, the alert transitions to the **Firing** state, else it reverts to the **Normal** state. + +## Add annotations + +Add [annotations][annotation-label]. to provide more context on the alert in your alert notifications. + +Annotations add metadata to provide more information on the alert in your alert notifications. For example, add a **Summary** annotation to tell you which value caused the alert to fire or which server it happened on. + +1. [Optional] Add a summary. + + Short summary of what happened and why. + +2. [Optional] Add a description. + + Description of what the alert rule does. + +3. [Optional] Add a Runbook URL. + + Webpage where you keep your runbook for the alert + +4. [Optional] Add a custom annotation +5. [Optional] Add a dashboard and panel link. + + Links alerts to panels in a dashboard. + +## Configure notifications + +Add labels to your alert rules to set which notification policy should handle your firing alert instances. + +All alert rules and instances, irrespective of their labels, match the default notification policy. If there are no nested policies, or no nested policies match the labels in the alert rule or alert instance, then the default notification policy is the matching policy. + +1. Add labels if you want to change the way your notifications are routed. + + Add custom labels by selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value. + +1. Click **Save rule**. {{% docs/reference %}} [alerting]: "/docs/grafana/ -> /docs/grafana//alerting" diff --git a/docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md b/docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md deleted file mode 100644 index 7a9c35d2575..00000000000 --- a/docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -aliases: - - ../unified-alerting/alerting-rules/edit-cortex-loki-namespace-group/ - - ../unified-alerting/alerting-rules/edit-mimir-loki-namespace-group/ -canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/edit-mimir-loki-namespace-group/ -description: Edit Grafana Mimir or Loki rule groups and namespaces -keywords: - - grafana - - alerting - - guide - - group - - namespace - - grafana mimir - - loki -labels: - products: - - cloud - - enterprise - - oss -title: Grafana Mimir or Loki rule groups and namespaces -weight: 405 ---- - -# Grafana Mimir or Loki rule groups and namespaces - -A namespace contains one or more groups. The rules within a group are run sequentially at a regular interval. The default interval is one (1) minute. You can rename Grafana Mimir or Loki rule namespaces and groups, and edit group evaluation intervals. - -{{< figure src="/static/img/docs/alerting/unified/rule-list-edit-mimir-loki-icon-8-2.png" caption="Rule group list" alt="Group list" >}} - - - -## Rename a namespace - -To rename a namespace: - -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -1. Click **Alert rules** to view the list of existing alerts. -1. Find a Grafana Mimir or Loki managed rule with the group that belongs to the namespace you want to edit. -1. Click the **Edit** (pen) icon. -1. Enter a new name in the **Namespace** field, then click **Save changes**. - -A new namespace is created and all groups are copied into this namespace from the old one. The old namespace is deleted. - -## Rename rule group or change the rule group evaluation interval - -The rules within a group are run sequentially at a regular interval, the default interval is one (1) minute. You can modify this interval using the following instructions. - -1. In the left-side menu, click the **Alerts & IRM** and then **Alerting**. -1. Click **Alert rules** to view the list of existing alerts. -1. Find a Grafana Mimir or Loki managed rule with the group you want to edit. -1. Click **Edit** (pen) icon. -1. Modify the **Rule group** and **Rule group evaluation interval** information as necessary. -1. Click **Save changes**. - -When you rename the group, a new group with all the rules from the old group is created. The old group is deleted. - -![Group edit modal](/static/img/docs/alerting/unified/rule-list-mimir-loki-edit-ns-group-8-2.png 'Rule group edit modal screenshot') - - diff --git a/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md b/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md index e078524b4cc..5e5244c593e 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md +++ b/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md @@ -1,6 +1,9 @@ --- +aliases: + - ../unified-alerting/alerting-rules/edit-cortex-loki-namespace-group/ + - ../unified-alerting/alerting-rules/edit-mimir-loki-namespace-group/ canonical: https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/organising-alerts/ -description: Learn how to organize alert rules +description: Namespaces, folders, and groups keywords: - grafana - alerting @@ -10,22 +13,22 @@ labels: - cloud - enterprise - oss -title: Organising alert rules +title: Namespaces, folders, and groups weight: 105 --- -## Namespaces and groups +## Namespaces, folders, and groups -Alerts can be organized using Folders for Grafana-managed rules and namespaces for Mimir or Loki rules and group names. +Alerts can be organized using folders for Grafana-managed rules and namespaces for Mimir or Loki rules and group names. -### Namespaces +### Namespaces and folders When creating Grafana-managed rules, the folder can be used to perform access control and grant or deny access to all rules within a specific folder. +A namespace contains one or more groups. The rules within a group are run sequentially at a regular interval. The default interval is one (1) minute. You can rename Grafana Mimir or Loki rule namespaces and groups, and edit group evaluation intervals. + ### Groups -All rules within a group are evaluated at the same **interval**. - -Alert rules and recording rules within a group will always be evaluated **sequentially**, meaning no rules will be evaluated at the same time and in order of appearance. +The rules within a group are run sequentially at a regular interval, meaning no rules will be evaluated at the same time and in order of appearance.. The default interval is one (1) minute. You can rename Grafana Mimir or Loki rule namespaces and groups, and edit group evaluation intervals. > **Note** If you want rules to be evaluated concurrently and with different intervals, consider storing them in different groups. diff --git a/docs/sources/datasources/jaeger/_index.md b/docs/sources/datasources/jaeger/_index.md index 96d493bd9d6..e5ecfc09ada 100644 --- a/docs/sources/datasources/jaeger/_index.md +++ b/docs/sources/datasources/jaeger/_index.md @@ -104,16 +104,16 @@ To use a variable you need to wrap it in `${}`. For example: `${__span.name}`. The following table describes the ways in which you can configure your trace to logs settings: -| Setting name | Description | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | -| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | -| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | -| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | -| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | -| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | -| **Use custom query** | Toggles use of custom query with interpolation. | -| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | +| Setting name | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | +| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | +| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | +| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`, `deployment.environment`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | +| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | +| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | +| **Use custom query** | Toggles use of custom query with interpolation. | +| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | ### Trace to metrics diff --git a/docs/sources/datasources/tempo/_index.md b/docs/sources/datasources/tempo/_index.md index 4f3ac4931d9..c10c18f00b2 100644 --- a/docs/sources/datasources/tempo/_index.md +++ b/docs/sources/datasources/tempo/_index.md @@ -104,16 +104,16 @@ To use a variable you need to wrap it in `${}`. For example `${__span.name}`. The following table describes the ways in which you can configure your trace to logs settings: -| Setting name | Description | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | -| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | -| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | -| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | -| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | -| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | -| **Use custom query** | Toggles use of custom query with interpolation. | -| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | +| Setting name | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | +| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | +| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | +| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`, `deployment.environment`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | +| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | +| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | +| **Use custom query** | Toggles use of custom query with interpolation. | +| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | ### Trace to metrics diff --git a/docs/sources/datasources/zipkin/_index.md b/docs/sources/datasources/zipkin/_index.md index 993aa73ece2..cf7633a7954 100644 --- a/docs/sources/datasources/zipkin/_index.md +++ b/docs/sources/datasources/zipkin/_index.md @@ -102,16 +102,16 @@ To use a variable you need to wrap it in `${}`. For example `${__span.name}`. The following table describes the ways in which you can configure your trace to logs settings: -| Setting name | Description | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | -| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | -| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | -| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | -| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | -| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | -| **Use custom query** | Toggles use of custom query with interpolation. | -| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | +| Setting name | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | +| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | +| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | +| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`, `deployment.environment`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | +| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | +| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | +| **Use custom query** | Toggles use of custom query with interpolation. | +| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | ### Trace to metrics diff --git a/docs/sources/introduction/grafana-enterprise.md b/docs/sources/introduction/grafana-enterprise.md index 4c60908b02d..e7ea3c27e3b 100644 --- a/docs/sources/introduction/grafana-enterprise.md +++ b/docs/sources/introduction/grafana-enterprise.md @@ -34,7 +34,7 @@ Supported auth providers: - [Auth Proxy]({{< relref "../setup-grafana/configure-security/configure-authentication/auth-proxy#team-sync-enterprise-only" >}}) - [Azure AD OAuth]({{< relref "../setup-grafana/configure-security/configure-authentication/azuread#team-sync-enterprise-only" >}}) - [GitHub OAuth]({{< relref "../setup-grafana/configure-security/configure-authentication/github#configure-team-synchronization" >}}) -- [GitLab OAuth]({{< relref "../setup-grafana/configure-security/configure-authentication/gitlab#team-sync-enterprise-only" >}}) +- [GitLab OAuth]({{< relref "../setup-grafana/configure-security/configure-authentication/gitlab#configure-team-synchronization" >}}) - [LDAP]({{< relref "../setup-grafana/configure-security/configure-authentication/enhanced-ldap#ldap-group-synchronization-for-teams" >}}) - [Okta]({{< relref "../setup-grafana/configure-security/configure-authentication/okta#configure-team-synchronization-enterprise-only" >}}) - [SAML]({{< relref "../setup-grafana/configure-security/configure-authentication/saml#configure-team-sync" >}}) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index a321a8e11f5..c07d9f8c0b7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -24,7 +24,6 @@ Some features are enabled by default. You can disable these feature by setting t | `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | | `featureHighlights` | Highlight Grafana Enterprise features | | | `dataConnectionsConsole` | Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins. | Yes | -| `topnav` | Enables new top navigation and page layouts | Yes | | `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | | `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | Yes | | `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | Yes | @@ -44,6 +43,7 @@ Some features are enabled by default. You can disable these feature by setting t | `alertingNotificationsPoliciesMatchingInstances` | Enables the preview of matching instances for notification policies | Yes | | `useCachingService` | When turned on, the new query and resource caching implementation using a wire service inject will be used in place of the previous middleware implementation | | | `advancedDataSourcePicker` | Enable a new data source picker with contextual information, recently used order and advanced mode | Yes | +| `transformationsRedesign` | Enables the transformations redesign | Yes | | `azureMonitorDataplane` | Adds dataplane compliant frame metadata in the Azure Monitor datasource | Yes | ## Preview feature toggles @@ -63,6 +63,7 @@ Some features are enabled by default. You can disable these feature by setting t | `accessControlOnCall` | Access control primitives for OnCall | | `nestedFolders` | Enable folder nesting | | `alertingNoNormalState` | Stop maintaining state of alerts that are not firing | +| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | | `renderAuthJWT` | Uses JWT-based auth for rendering instead of relying on remote cache | | `refactorVariablesTimeRange` | Refactor time range variables flow to reduce number of API calls made when query variables are chained | | `enableElasticsearchBackendQuerying` | Enable the processing of queries and responses in the Elasticsearch data source through backend | @@ -97,7 +98,6 @@ Experimental features might be changed or removed without prior notice. | `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | | `individualCookiePreferences` | Support overriding cookie preferences per user | | `timeSeriesTable` | Enable time series table transformer & sparkline cell type | -| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | | `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation | | `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | | `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | @@ -122,13 +122,13 @@ Experimental features might be changed or removed without prior notice. | `prometheusIncrementalQueryInstrumentation` | Adds RudderStack events to incremental queries | | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | -| `transformationsRedesign` | Enables the transformations redesign | | `toggleLabelsInLogsUI` | Enable toggleable filters in log details view | | `mlExpressions` | Enable support for Machine Learning in server-side expressions | -| `disableTraceQLStreaming` | Disables the option to stream the response of TraceQL queries of the Tempo data source | +| `traceQLStreaming` | Enables response streaming of TraceQL queries of the Tempo data source | | `grafanaAPIServer` | Enable Kubernetes API Server for Grafana resources | | `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end | | `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the `useCachingService` feature toggle is enabled and the datasource has caching and async query support enabled | +| `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component | ## Development feature toggles diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md index 940d545a311..2ad0dcd173e 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md @@ -146,7 +146,7 @@ For example, `https://github.com/orgs/grafana/teams/developers` or `@grafana/dev To learn more about Team Sync, refer to [Configure team sync]({{< relref "../../configure-team-sync" >}}). -## Examples of GitHub configuration in Grafana +## Example of GitHub configuration in Grafana This section includes an example of GitHub configuration in the Grafana configuration file. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index 2eddacc5c22..9759567471f 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -19,85 +19,83 @@ weight: 1000 # Configure GitLab OAuth2 authentication -To enable GitLab OAuth2 you must register the application in GitLab. GitLab will generate a client ID and secret key for you to use. +{{< docs/shared "auth/intro.md" >}} -## Create GitLab OAuth keys +This topic describes how to configure GitLab OAuth2 authentication. -You need to [create a GitLab OAuth application](https://docs.gitlab.com/ce/integration/oauth_provider.html). -Choose a descriptive _Name_, and use the following _Redirect URI_: +## Before you begin -``` -https://grafana.example.com/login/gitlab -``` +To follow this guide: -where `https://grafana.example.com` is the URL you use to connect to Grafana. -Adjust it as needed if you don't use HTTPS or if you use a different port; for -instance, if you access Grafana at `http://203.0.113.31:3000`, you should use +- Ensure that you have access to the [Grafana configuration file]({{< relref "../../../configure-grafana#configuration-file-location" >}}). +- Ensure you know how to create a GitLab OAuth application. Consult GitLab's documentation on [creating a GitLab OAuth application](https://docs.gitlab.com/ee/integration/oauth_provider.html) for more information. -``` -http://203.0.113.31:3000/login/gitlab -``` +## Steps -Finally, select `openid`, `email` and `profile` as the scopes and submit the form. +To configure GitLab authentication with Grafana, follow these steps: -You'll get an _Application Id_ and a _Secret_ in return; we'll call them -`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this -section. +1. Create an OAuth application in GitLab. -## Enable GitLab in Grafana + 1. Set the redirect URI to `http://:/login/gitlab`. -In this example, we'll assume you use the public `gitlab.com` instance, but you -can use your own instance of GitLab instead by replacing `auth_url`, `token_url` with the URL of your instance. + Ensure that the Redirect URI is the complete HTTP address that you use to access Grafana via your browser, but with the appended path of `/login/gitlab`. -You can find these URLs in the `well known` configuration file of your GitLab instance, for example `https://gitlab.com/.well-known/openid-configuration`. + For the Redirect URI to be correct, it might be necessary to set the `root_url` option in the `[server]`section of the Grafana configuration file. For example, if you are serving Grafana behind a proxy. -Add the following to your Grafana configuration file to enable GitLab -authentication: + 1. Set the OAuth2 scopes to `openid`, `email` and `profile`. -```bash -[auth.gitlab] -enabled = true -allow_sign_up = true -auto_login = false -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = openid email profile -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -allowed_groups = -role_attribute_path = -role_attribute_strict = false -allow_assign_grafana_admin = false -tls_skip_verify_insecure = false -tls_client_cert = -tls_client_key = -tls_client_ca = -use_pkce = true -``` +1. Refer to the following table to update field values located in the `[auth.gitlab]` section of the Grafana configuration file: -You may have to set the `root_url` option of `[server]` for the callback URL to be -correct. For example in case you are serving Grafana behind a proxy. + | Field | Description | + | ---------------------------- | -------------------------------------------------------------------------------------------- | + | `client_id`, `client_secret` | These values must match the client ID and client secret from your GitLab OAuth2 application. | + | `enabled` | Enables GitLab authentication. Set this value to `true`. | -Restart the Grafana backend for your changes to take effect. + Review the list of other GitLab [configuration options]({{< relref "#configuration-options" >}}) and complete them, as necessary. -With `allow_sign_up` set to `false`, only existing users will be able to login -using their GitLab account, but with `allow_sign_up` set to `true`, _any_ user -who can authenticate on GitLab will be able to login on your Grafana instance; -if you use the public `gitlab.com`, it means anyone in the world would be able -to login on your Grafana instance. +1. Optional: [Configure a refresh token]({{< relref "#configure-a-refresh-token" >}}): -You can limit access to only members of a given group or list of -groups by setting the `allowed_groups` option. + a. Enable `accessTokenExpirationCheck` feature toggle. -You can also specify the SSL/TLS configuration used by the client. + b. Set `use_refresh_token` to `true` in `[auth.gitlab]` section in Grafana configuration file. -- Set `tls_client_cert` to the path of the certificate. -- Set `tls_client_key` to the path containing the key. -- Set `tls_client_ca` to the path containing a trusted certificate authority list. +1. [Configure role mapping]({{< relref "#configure-role-mapping" >}}). +1. Optional: [Configure team synchronization]({{< relref "#configure-team-synchronization" >}}). +1. Restart Grafana. -`tls_skip_verify_insecure` controls whether a client verifies the server's certificate chain and host name. If it is true, then SSL/TLS accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. + You should now see a GitLab login button on the login page and be able to log in or sign up with your GitLab accounts. -### Configure refresh token +## Configuration options + +The table below describes all GitLab OAuth configuration options. Like any other Grafana configuration, you can apply these options as environment variables. + +| Setting | Required | Description | Default | +| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `enabled` | Yes | Whether GitLab OAuth authentication is allowed. | `false` | +| `client_id` | Yes | Client ID provided by your GitLab OAuth app. | | +| `client_secret` | Yes | Client secret provided by your GitLab OAuth app. | | +| `auth_url` | Yes | Authorization endpoint of your GitLab OAuth provider. If you use your own instance of GitLab instead of gitlab.com, adjust `auth_url` by replacing the `gitlab.com` hostname with your own. | `https://gitlab.com/oauth/authorize` | +| `token_url` | Yes | Endpoint used to obtain GitLab OAuth access token. If you use your own instance of GitLab instead of gitlab.com, adjust `token_url` by replacing the `gitlab.com` hostname with your own. | `https://gitlab.com/oauth/token` | +| `api_url` | No | Grafana uses `/user` endpoint to obtain GitLab user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://gitlab.com/api/v4` | +| `name` | No | Name used to refer to the GitLab authentication in the Grafana user interface. | `GitLab` | +| `icon` | No | Icon used for GitLab authentication in the Grafana user interface. | `gitlab` | +| `scopes` | No | List of comma- or space-separated GitLab OAuth scopes. | `openid email profile` | +| `allow_sign_up` | No | Whether to allow new Grafana user creation through GitLab login. If set to `false`, then only existing Grafana users can log in with GitLab OAuth. | `true` | +| `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | +| `role_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the GitLab OAuth token. If no role is found, Grafana creates a JSON data with `groups` key that maps to groups obtained from GitLab's `/oauth/userinfo` endpoint, and evaluates the expression using this data. Finally, if a valid role is still not found, the expression is evaluated against the user information retrieved from `api_url/users` endpoint and groups retrieved from `api_url/groups` endpoint. The result of the evaluation should be a valid Grafana role (`Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | | +| `role_attribute_strict` | No | Set to `true` to deny user login if the Grafana role cannot be extracted using `role_attribute_path`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` | +| `allow_assign_grafana_admin` | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` | +| `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. | `false` | +| `allowed_domains` | No | List of comma- or space-separated domains. User must belong to at least one domain to log in. | | +| `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | | +| `tls_skip_verify_insecure` | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | +| `tls_client_cert` | No | The path to the certificate. | | +| `tls_client_key` | No | The path to the key. | | +| `tls_client_ca` | No | The path to the trusted certificate authority list. | | +| `use_pkce` | No | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | +| `use_refresh_token` | No | Set to `true` to use refresh token and check access token expiration. The `accessTokenExpirationCheck` feature toggle should also be enabled to use refresh token. | `true` | + +### Configure a refresh token > Available in Grafana v9.3 and later versions. @@ -113,169 +111,95 @@ Refresh token fetching and access token expiration check is enabled by default f > **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check. -### allowed_groups +### Configure allowed groups To limit access to authenticated users that are members of one or more [GitLab groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups` -to a comma- or space-separated list of groups. For instance, if you want to -only give access to members of the `example` group, set +to a comma- or space-separated list of groups. + +GitLab's groups are referenced by the group name. For example, `developers`. To reference a subgroup `frontend`, use `developers/frontend`. +Note that in GitLab, the group or subgroup name does not always match its display name, especially if the display name contains spaces or special characters. +Make sure you always use the group or subgroup name as it appears in the URL of the group or subgroup. + +## Configure role mapping + +Unless `skip_org_role_sync` option is enabled, the user's role will be set to the role retrieved from GitLab upon user login. + +The user's role is retrieved using a [JMESPath](http://jmespath.org/examples.html) expression from the `role_attribute_path` configuration option. +To map the server administrator role, use the `allow_assign_grafana_admin` configuration option. +Refer to [configuration options]({{< relref "#configuration-options" >}}) for more information. + +If no valid role is found, the user is assigned the role specified by [the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). +You can disable this default role assignment by setting `role_attribute_strict = true`. +This setting denies user access if no role or an invalid role is returned. + +To ease configuration of a proper JMESPath expression, go to [JMESPath](http://jmespath.org/) to test and evaluate expressions with custom payloads. + +### Role mapping examples + +This section includes examples of JMESPath expressions used for role mapping. + +#### Map roles using user information from OAuth token + +In this example, the user with email `admin@company.com` has been granted the `Admin` role. +All other users are granted the `Viewer` role. ```ini -allowed_groups = example +role_attribute_path = email=='admin@company.com' && 'Admin' || 'Viewer' ``` -If you want to also give access to members of the subgroup `bar`, which is in -the group `foo`, set - -```ini -allowed_groups = example, foo/bar -``` - -To put values containing spaces in the list, use the following JSON syntax: - -```ini -allowed_groups = ["Admins", "Software Engineers"] -``` - -Note that in GitLab, the group or subgroup name doesn't always match its -display name, especially if the display name contains spaces or special -characters. Make sure you always use the group or subgroup name as it appears -in the URL of the group or subgroup. - -Here's a complete example with `allow_sign_up` enabled, with access limited to -the `example` and `foo/bar` groups. The example also promotes all GitLab Admins to Grafana organization admins: - -```ini -[auth.gitlab] -enabled = true -allow_sign_up = true -auto_login = false -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = openid email profile -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -allowed_groups = example, foo/bar -role_attribute_path = is_admin && 'Admin' || 'Viewer' -role_attribute_strict = true -allow_assign_grafana_admin = false -tls_skip_verify_insecure = false -tls_client_cert = -tls_client_key = -tls_client_ca = -use_pkce = true -``` - -### PKCE - -IETF's [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) -introduces "proof key for code exchange" (PKCE) which provides -additional protection against some forms of authorization code -interception attacks. PKCE will be required in [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-03). - -> You can disable PKCE in Grafana by setting `use_pkce` to `false` in the`[auth.gitlab]` section. - -``` -use_pkce = true -``` - -Grafana always uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. - -### Configure automatic login - -Set `auto_login` option to true to attempt login automatically, skipping the login screen. -This setting is ignored if multiple auth providers are configured to use auto login. - -``` -auto_login = true -``` - -### Map roles - -You can use GitLab OAuth to map roles. During mapping, Grafana checks for the presence of a role using the [JMESPath](http://jmespath.org/examples.html) specified via the `role_attribute_path` configuration option. - -For the path lookup, Grafana uses JSON obtained from querying GitLab's API [`/api/v4/user`](https://docs.gitlab.com/ee/api/users.html#list-current-user-for-normal-users) endpoint and a `groups` key containing all of the user's teams. The result of evaluating the `role_attribute_path` JMESPath expression must be a valid Grafana role, for example, `Viewer`, `Editor` or `Admin`. For more information about roles and permissions in Grafana, refer to [Roles and permissions]({{< relref "../../../../administration/roles-and-permissions" >}}). - -{{% admonition type="warning" %}} -Currently if no organization role mapping is found for a user, Grafana doesn't -update the user's organization role. This is going to change in Grafana 10. To avoid overriding manually set roles, -enable the `skip_org_role_sync` option. -See [Configure Grafana]({{< relref "../../../configure-grafana#authgitlab" >}}) for more information. -{{% /admonition %}} - -On first login, if the`role_attribute_path` property does not return a role, then the user is assigned the role -specified by [the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). -You can disable this default role assignment by setting `role_attribute_strict = true`. -It denies user access if no role or an invalid role is returned. - -{{% admonition type="warning" %}} -With Grafana 10, **on every login**, if the`role_attribute_path` property does not return a role, -then the user is assigned the role specified by -[the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). -{{% /admonition %}} - -An example Query could look like the following: - -```ini -role_attribute_path = is_admin && 'Admin' || 'Viewer' -``` - -This allows every GitLab Admin to be an Admin in Grafana. - #### Map roles using groups -Groups can also be used to map roles. Group name (lowercased and unique) is used instead of display name for identifying groups - -For instance, if you have a group with display name 'Example-Group' you can use the following snippet to -ensure those members inherit the role 'Editor'. +In this example, the user from GitLab group 'example-group' have been granted the `Editor` role. +All other users are granted the `Viewer` role. ```ini role_attribute_path = contains(groups[*], 'example-group') && 'Editor' || 'Viewer' ``` -Note: If a match is found in other fields, groups will be ignored. +#### Map server administrator role -#### Map server administrator privileges +In this example, the user with email `admin@company.com` has been granted the `Admin` organization role as well as the Grafana server admin role. +All other users are granted the `Viewer` role. -> Available in Grafana v9.2 and later versions. - -If the application role received by Grafana is `GrafanaAdmin`, Grafana grants the user server administrator privileges. -This is useful if you want to grant server administrator privileges to a subset of users. -Grafana also assigns the user the `Admin` role of the default organization. - -The setting `allow_assign_grafana_admin` under `[auth.gitlab]` must be set to `true` for this to work. -If the setting is set to `false`, the user is assigned the role of `Admin` of the default organization, but not server administrator privileges. - -```ini -allow_assign_grafana_admin = true +```bash +role_attribute_path = email=='admin@company.com' && 'GrafanaAdmin' || 'Viewer' ``` -Example: +## Configure team synchronization -```ini -role_attribute_path = is_admin && 'GrafanaAdmin' || 'Viewer' -``` +> **Note:** Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}) and [Grafana Cloud](/docs/grafana-cloud/). -### Team Sync (Enterprise only) +By using Team Sync, you can map GitLab groups to teams within Grafana. This will automatically assign users to the appropriate teams. +Teams for each user are synchronized when the user logs in. -> Only available in Grafana Enterprise v6.4+ +GitLab groups are referenced by the group name. For example, `developers`. To reference a subgroup `frontend`, use `developers/frontend`. +Note that in GitLab, the group or subgroup name does not always match its display name, especially if the display name contains spaces or special characters. +Make sure you always use the group or subgroup name as it appears in the URL of the group or subgroup. -With Team Sync you can map your GitLab groups to teams in Grafana so that your users will automatically be added to -the correct teams. +To learn more about Team Sync, refer to [Configure team sync]({{< relref "../../configure-team-sync" >}}). -Your GitLab groups can be referenced in the same way as `allowed_groups`, like `example` or `foo/bar`. +## Example of GitLab configuration in Grafana -[Learn more about Team Sync]({{< relref "../../configure-team-sync" >}}) +This section includes an example of GitLab configuration in the Grafana configuration file. -## Skip organization role sync - -To prevent the sync of organization roles from GitLab, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana. -This also impacts the `allow_assign_grafana_admin` setting by not syncing the Grafana admin role from GitLab. - -```ini +```bash [auth.gitlab] -# .. -# prevents the sync of org roles from Github -skip_org_role_sync = true -`` +enabled = true +allow_sign_up = true +auto_login = false +client_id = YOUR_GITLAB_APPLICATION_ID +client_secret = YOUR_GITLAB_APPLICATION_SECRET +scopes = openid email profile +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +role_attribute_path = contains(groups[*], 'example-group') && 'Editor' || 'Viewer' +role_attribute_strict = false +allow_assign_grafana_admin = false +allowed_groups = ["admins", "software engineers", "developers/frontend"] +allowed_domains = mycompany.com mycompany.org +tls_skip_verify_insecure = false +use_pkce = true +use_refresh_token = true ``` diff --git a/docs/sources/setup-grafana/configure-security/configure-team-sync.md b/docs/sources/setup-grafana/configure-security/configure-team-sync.md index 09267daa8a1..2f74e9137db 100644 --- a/docs/sources/setup-grafana/configure-security/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-security/configure-team-sync.md @@ -30,7 +30,8 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea - [Auth Proxy]({{< relref "./configure-authentication/auth-proxy#team-sync-enterprise-only" >}}) - [Azure AD]({{< relref "./configure-authentication/azuread#team-sync-enterprise-only" >}}) - [GitHub OAuth]({{< relref "./configure-authentication/github#configure-team-synchronization" >}}) -- [GitLab OAuth]({{< relref "./configure-authentication/gitlab#team-sync-enterprise-only" >}}) +- [GitLab OAuth]({{< relref "./configure-authentication/gitlab#configure-team-synchronization" >}}) +- [Google OAuth]({{< relref "./configure-authentication/google#configure-team-sync-for-google-oauth" >}}) - [LDAP]({{< relref "./configure-authentication/enhanced-ldap#ldap-group-synchronization-for-teams" >}}) - [Okta]({{< relref "./configure-authentication/okta#configure-team-synchronization-enterprise-only" >}}) - [SAML]({{< relref "./configure-authentication/saml#configure-team-sync" >}}) diff --git a/e2e/cloud-plugins-suite/azure-monitor.spec.ts b/e2e/cloud-plugins-suite/azure-monitor.spec.ts index 43528131776..c0f31cfe758 100644 --- a/e2e/cloud-plugins-suite/azure-monitor.spec.ts +++ b/e2e/cloud-plugins-suite/azure-monitor.spec.ts @@ -126,6 +126,10 @@ const addAzureMonitorVariable = ( .input() .find('input') .type(`${options?.namespace}{enter}`); + e2eSelectors.variableEditor.region + .input() + .find('input') + .type(`${options?.region}{enter}`); break; case AzureQueryType.MetricNamesQuery: e2eSelectors.variableEditor.subscription @@ -302,10 +306,14 @@ e2e.scenario({ subscription: '$subscription', resourceGroup: '$resourceGroups', }); + addAzureMonitorVariable('region', AzureQueryType.LocationsQuery, false, { + subscription: '$subscription', + }); addAzureMonitorVariable('resource', AzureQueryType.ResourceNamesQuery, false, { subscription: '$subscription', resourceGroup: '$resourceGroups', namespace: '$namespace', + region: '$region', }); e2e.pages.Dashboard.SubMenu.submenuItemLabels('subscription').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('grafanalabs-datasources-dev').click(); @@ -319,6 +327,8 @@ e2e.scenario({ .parent() .find('input') .type('microsoft.storage/storageaccounts{downArrow}{enter}'); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('button').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('input').type('uk south{downArrow}{enter}'); e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource').parent().find('button').click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource') .parent() @@ -333,6 +343,7 @@ e2e.scenario({ e2eSelectors.queryEditor.resourcePicker.advanced.subscription.input().find('input').type('$subscription'); e2eSelectors.queryEditor.resourcePicker.advanced.resourceGroup.input().find('input').type('$resourceGroups'); e2eSelectors.queryEditor.resourcePicker.advanced.namespace.input().find('input').type('$namespaces'); + e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region'); e2eSelectors.queryEditor.resourcePicker.advanced.resource.input().find('input').type('$resource'); e2eSelectors.queryEditor.resourcePicker.apply.button().click(); e2eSelectors.queryEditor.metricsQueryEditor.metricName.input().find('input').type('Transactions{enter}'); diff --git a/e2e/dashboards-suite/new-query-variable.spec.ts b/e2e/dashboards-suite/new-query-variable.spec.ts index 2bae25ad442..3a1a4712d78 100644 --- a/e2e/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/dashboards-suite/new-query-variable.spec.ts @@ -38,10 +38,9 @@ describe('Variables - Query - Add variable', () => { e2e().get('label').contains('Show on dashboard').should('be.visible'); e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect() - .should('be.visible') - .within((select) => { - e2e.components.Select.singleValue().should('have.text', 'gdev-testdata'); - }); + .get('input[placeholder="gdev-testdata"]') + .scrollIntoView() + .should('be.visible'); e2e().get('label').contains('Refresh').scrollIntoView().should('be.visible'); e2e().get('label').contains('On dashboard load').scrollIntoView().should('be.visible'); @@ -89,7 +88,7 @@ describe('Variables - Query - Add variable', () => { e2e().get('[placeholder="Descriptive text"]').should('be.visible').clear().type('a description'); - e2e.components.DataSourcePicker.inputV2().should('be.visible').type('gdev-testdata{enter}'); + e2e.components.DataSourcePicker.container().should('be.visible').type('gdev-testdata{enter}'); e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput() .should('be.visible') @@ -137,7 +136,7 @@ describe('Variables - Query - Add variable', () => { e2e().get('[placeholder="Descriptive text"]').should('be.visible').clear().type('a description'); - e2e.components.DataSourcePicker.inputV2().type('gdev-testdata{enter}'); + e2e.components.DataSourcePicker.container().type('gdev-testdata{enter}'); e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput() .should('be.visible') diff --git a/go.mod b/go.mod index e61e4031503..b669b543f26 100644 --- a/go.mod +++ b/go.mod @@ -394,6 +394,8 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/term v0.10.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect @@ -485,8 +487,6 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler // grpc v1.46.0 removed "WithBalancerName()" API, still in use by weaveworks/commons. replace google.golang.org/grpc => google.golang.org/grpc v1.45.0 -replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3 - // Use 1.10.6 of pq to avoid a change in 1.10.7 that has certificate validation issues. https://github.com/grafana/grafana/issues/65816 replace github.com/lib/pq => github.com/lib/pq v1.10.6 diff --git a/go.sum b/go.sum index 7ea1ef6cdb1..c7788fe323f 100644 --- a/go.sum +++ b/go.sum @@ -45,12 +45,103 @@ cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFO cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= @@ -61,6 +152,7 @@ cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQH cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ= @@ -70,27 +162,310 @@ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.1 h1:7hm1bRqGCA1GBRQUrp831TwJ9TWhP+tvLuP497CQS2g= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -101,10 +476,66 @@ cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKu cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= @@ -112,6 +543,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg= filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= @@ -190,6 +622,7 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbP github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -235,7 +668,10 @@ github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE= github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -259,8 +695,10 @@ github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3 github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -384,6 +822,7 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= @@ -440,6 +879,7 @@ github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtM github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230112175826-46e39c7b9b43 h1:XP+uhjN0yBCN/tPkr8Z0BNDc5rZam9RG6UWyf2FrSQ0= @@ -609,6 +1049,7 @@ github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.11.0 h1:jtLewhRR2vMRNnq2ZZUoCjUlgut+Y0+sDDWPOfwOi1o= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -678,6 +1119,7 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= @@ -703,6 +1145,7 @@ github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -816,6 +1259,8 @@ github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbN github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -1099,6 +1544,7 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -1644,6 +2090,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -1652,9 +2099,11 @@ github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/knadh/koanf v0.14.1-0.20201201075439-e0853799f9ec/go.mod h1:H5mEFsTeWizwFXHKtsITL5ipsLTuAMQoGuQpp+1JL9U= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= @@ -1787,6 +2236,7 @@ github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOq github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= @@ -1805,6 +2255,8 @@ github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJys github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo= github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -2021,9 +2473,11 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -2119,6 +2573,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE= github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= @@ -2159,6 +2614,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= @@ -2427,6 +2883,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= @@ -2646,6 +3104,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= @@ -2659,7 +3118,13 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -2848,6 +3313,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= @@ -3007,6 +3473,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -3038,6 +3505,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -3058,6 +3526,7 @@ golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -3088,6 +3557,7 @@ golang.org/x/tools v0.0.0-20190104182027-498d95493402/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190111214448-fc1d57b08d7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190118193359-16909d206f00/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -3158,6 +3628,7 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -3167,9 +3638,11 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= @@ -3194,6 +3667,7 @@ gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= @@ -3201,6 +3675,7 @@ gonum.org/v1/netlib v0.0.0-20191229114700-bbb4dff026f8/go.mod h1:2IgXn/sJaRbePPB gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.0.0-20200111075622-4abb28f724d5/go.mod h1:+HbaZVpsa73UwN7kXGCECULRHovLRJjH+t5cFPgxErs= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -3246,6 +3721,7 @@ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/S google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= @@ -3253,6 +3729,7 @@ google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3p google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= @@ -3260,7 +3737,10 @@ google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91 google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= @@ -3274,8 +3754,166 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3 h1:SeX3QUcBj3fciwnfPT9kt5gBhFy/FCZtYZ+I/RB8agc= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e h1:Ao9GzfUMPH3zjVfzXG5rlWlk+Q8MXWKwWpwVQE1MXfw= +google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -3288,6 +3926,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= @@ -3378,6 +4018,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= k8s.io/api v0.27.1 h1:Z6zUGQ1Vd10tJ+gHcNNNgkV5emCyW+v2XTmn+CLjSd0= @@ -3412,11 +4053,45 @@ k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt k8s.io/utils v0.0.0-20230308161112-d77c459e9343/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/latest.json b/latest.json index b8596242adc..c004d8d6be8 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "10.0.1", - "testing": "10.0.1" + "stable": "10.0.3", + "testing": "10.0.3" } diff --git a/lerna.json b/lerna.json index 78f2726b208..ea142398f53 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "npmClient": "yarn", "useWorkspaces": true, "packages": ["packages/*"], - "version": "10.1.0-pre" + "version": "10.2.0-pre" } diff --git a/package.json b/package.json index 41f6faa3edb..b9efa587f92 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "repository": "github:grafana/grafana", "scripts": { "build": "yarn i18n:compile && NODE_ENV=production webpack --progress --config scripts/webpack/webpack.prod.js", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index f1e107ae0e3..f11a265e670 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -36,7 +36,7 @@ }, "dependencies": { "@braintree/sanitize-url": "6.0.2", - "@grafana/schema": "10.1.0-pre", + "@grafana/schema": "10.2.0-pre", "@types/d3-interpolate": "^3.0.0", "@types/string-hash": "1.1.1", "d3-interpolate": "3.0.1", diff --git a/packages/grafana-data/src/dataframe/processDataFrame.test.ts b/packages/grafana-data/src/dataframe/processDataFrame.test.ts index 4d5c6485004..2e23be6e171 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.test.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.test.ts @@ -8,6 +8,7 @@ import { guessFieldTypes, isDataFrame, isTableData, + reverseDataFrame, sortDataFrame, toDataFrame, toLegacyResponseData, @@ -360,19 +361,45 @@ describe('sorted DataFrame', () => { { name: 'fist', type: FieldType.time, values: [1, 2, 3] }, { name: 'second', type: FieldType.string, values: ['a', 'b', 'c'] }, { name: 'third', type: FieldType.number, values: [2000, 3000, 1000] }, + { name: 'fourth', type: FieldType.time, values: [1, 2, 3], nanos: [10, 20, 30] }, ], }); it('Should sort numbers', () => { const sorted = sortDataFrame(frame, 0, true); expect(sorted.length).toEqual(3); expect(sorted.fields[0].values).toEqual([3, 2, 1]); + expect(sorted.fields[0].nanos).toBeUndefined(); expect(sorted.fields[1].values).toEqual(['c', 'b', 'a']); + expect(sorted.fields[1].nanos).toBeUndefined(); + expect(sorted.fields[3].values).toEqual([3, 2, 1]); + expect(sorted.fields[3].nanos).toEqual([30, 20, 10]); }); it('Should sort strings', () => { const sorted = sortDataFrame(frame, 1, true); expect(sorted.length).toEqual(3); expect(sorted.fields[0].values).toEqual([3, 2, 1]); + expect(sorted.fields[0].nanos).toBeUndefined(); expect(sorted.fields[1].values).toEqual(['c', 'b', 'a']); + expect(sorted.fields[1].nanos).toBeUndefined(); + expect(sorted.fields[3].values).toEqual([3, 2, 1]); + expect(sorted.fields[3].nanos).toEqual([30, 20, 10]); + }); +}); + +describe('reverse DataFrame', () => { + const frame = toDataFrame({ + fields: [ + { name: 'fist', type: FieldType.time, values: [1, 2, 3], nanos: [10, 20, 30] }, + { name: 'third', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + it('should reverse dataframe', () => { + const rev = reverseDataFrame(frame); + expect(rev.length).toEqual(3); + expect(rev.fields[0].values).toEqual([3, 2, 1]); + expect(rev.fields[0].nanos).toEqual([30, 20, 10]); + expect(rev.fields[1].values).toEqual(['c', 'b', 'a']); + expect(rev.fields[1].nanos).toBeUndefined(); }); }); diff --git a/packages/grafana-data/src/dataframe/processDataFrame.ts b/packages/grafana-data/src/dataframe/processDataFrame.ts index 09e67d8349d..402f4962508 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.ts @@ -431,10 +431,17 @@ export function sortDataFrame(data: DataFrame, sortIndex?: number, reverse = fal return { ...data, fields: data.fields.map((f) => { - return { + const newF = { ...f, values: f.values.map((v, i) => f.values[index[i]]), }; + + // only add .nanos if it exists + const { nanos } = f; + if (nanos !== undefined) { + newF.nanos = nanos.map((n, i) => nanos[index[i]]); + } + return newF; }), }; } @@ -448,10 +455,20 @@ export function reverseDataFrame(data: DataFrame): DataFrame { fields: data.fields.map((f) => { const values = [...f.values]; values.reverse(); - return { + + const newF = { ...f, values, }; + + // only add .nanos if it exists + const { nanos } = f; + if (nanos !== undefined) { + const revNanos = [...nanos]; + revNanos.reverse(); + newF.nanos = revNanos; + } + return newF; }), }; } diff --git a/packages/grafana-data/src/geo/layer.ts b/packages/grafana-data/src/geo/layer.ts index 4ad67866fbb..c51750c3600 100644 --- a/packages/grafana-data/src/geo/layer.ts +++ b/packages/grafana-data/src/geo/layer.ts @@ -5,6 +5,7 @@ import { ReactNode } from 'react'; import { MapLayerOptions, FrameGeometrySourceMode } from '@grafana/schema'; import { EventBus } from '../events'; +import { StandardEditorContext } from '../field/standardFieldConfigEditorRegistry'; import { GrafanaTheme2 } from '../themes'; import { PanelData } from '../types'; import { PanelOptionsEditorBuilder } from '../utils'; @@ -39,7 +40,10 @@ export interface MapLayerHandler { /** * Show custom elements in the panel edit UI */ - registerOptionsUI?: (builder: PanelOptionsEditorBuilder>) => void; + registerOptionsUI?: ( + builder: PanelOptionsEditorBuilder>, + context: StandardEditorContext + ) => void; } /** diff --git a/packages/grafana-data/src/transformations/transformers.ts b/packages/grafana-data/src/transformations/transformers.ts index 218e5f8e9c0..a087bd9a467 100644 --- a/packages/grafana-data/src/transformations/transformers.ts +++ b/packages/grafana-data/src/transformations/transformers.ts @@ -6,6 +6,7 @@ import { filterFieldsTransformer, filterFramesTransformer } from './transformers import { filterFieldsByNameTransformer } from './transformers/filterByName'; import { filterFramesByRefIdTransformer } from './transformers/filterByRefId'; import { filterByValueTransformer } from './transformers/filterByValue'; +import { formatTimeTransformer } from './transformers/formatTime'; import { groupByTransformer } from './transformers/groupBy'; import { groupingToMatrixTransformer } from './transformers/groupingToMatrix'; import { histogramTransformer } from './transformers/histogram'; @@ -29,6 +30,7 @@ export const standardTransformers = { filterFramesTransformer, filterFramesByRefIdTransformer, filterByValueTransformer, + formatTimeTransformer, orderFieldsTransformer, organizeFieldsTransformer, reduceTransformer, diff --git a/packages/grafana-data/src/transformations/transformers/formatTime.test.ts b/packages/grafana-data/src/transformations/transformers/formatTime.test.ts new file mode 100644 index 00000000000..5a63b53bfe7 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/formatTime.test.ts @@ -0,0 +1,89 @@ +import { toDataFrame } from '../../dataframe/processDataFrame'; +import { FieldType } from '../../types/dataFrame'; +import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; + +import { createTimeFormatter, formatTimeTransformer } from './formatTime'; + +describe('Format Time Transformer', () => { + beforeAll(() => { + mockTransformationsRegistry([formatTimeTransformer]); + }); + + it('will convert time to formatted string', () => { + const options = { + timeField: 'time', + outputFormat: 'YYYY-MM', + useTimezone: false, + }; + + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const frame = toDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, 1691011200000], + }, + ], + }); + + const newFrame = formatter(frame.fields); + expect(newFrame[0].values).toEqual(['2021-02', '2023-07', '2023-04', '2023-07', '2023-08']); + }); + + it('will handle formats with times', () => { + const options = { + timeField: 'time', + outputFormat: 'YYYY-MM h:mm:ss a', + useTimezone: false, + }; + + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const frame = toDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, 1691011200000], + }, + ], + }); + + const newFrame = formatter(frame.fields); + expect(newFrame[0].values).toEqual([ + '2021-02 1:46:40 am', + '2023-07 2:00:00 pm', + '2023-04 3:20:00 pm', + '2023-07 5:34:49 pm', + '2023-08 3:20:00 pm', + ]); + }); + + it('will handle null times', () => { + const options = { + timeField: 'time', + outputFormat: 'YYYY-MM h:mm:ss a', + useTimezone: false, + }; + + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const frame = toDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, null], + }, + ], + }); + + const newFrame = formatter(frame.fields); + expect(newFrame[0].values).toEqual([ + '2021-02 1:46:40 am', + '2023-07 2:00:00 pm', + '2023-04 3:20:00 pm', + '2023-07 5:34:49 pm', + 'Invalid date', + ]); + }); +}); diff --git a/packages/grafana-data/src/transformations/transformers/formatTime.ts b/packages/grafana-data/src/transformations/transformers/formatTime.ts new file mode 100644 index 00000000000..66406f0ef48 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/formatTime.ts @@ -0,0 +1,76 @@ +import moment from 'moment-timezone'; +import { map } from 'rxjs/operators'; + +import { getTimeZone, getTimeZoneInfo } from '../../datetime'; +import { Field, FieldType } from '../../types'; +import { DataTransformerInfo } from '../../types/transformations'; + +import { DataTransformerID } from './ids'; + +export interface FormatTimeTransformerOptions { + timeField: string; + outputFormat: string; + useTimezone: boolean; +} + +export const formatTimeTransformer: DataTransformerInfo = { + id: DataTransformerID.formatTime, + name: 'Format Time', + description: 'Set the output format of a time field', + defaultOptions: { timeField: '', outputFormat: '', useTimezone: true }, + operator: (options) => (source) => + source.pipe( + map((data) => { + // If a field and a format are configured + // then format the time output + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + + if (!Array.isArray(data) || data.length === 0) { + return data; + } + + return data.map((frame) => ({ + ...frame, + fields: formatter(frame.fields), + })); + }) + ), +}; + +/** + * @internal + */ +export const createTimeFormatter = + (timeField: string, outputFormat: string, useTimezone: boolean) => (fields: Field[]) => { + const tz = getTimeZone(); + + return fields.map((field) => { + // Find the configured field + if (field.name === timeField) { + // Update values to use the configured format + const newVals = field.values.map((value) => { + const date = moment(value); + + // Apply configured timezone if the + // option has been set. Otherwise + // use the date directly + if (useTimezone) { + const info = getTimeZoneInfo(tz, value); + const realTz = info !== undefined ? info.ianaName : 'UTC'; + + return date.tz(realTz).format(outputFormat); + } else { + return date.format(outputFormat); + } + }); + + return { + ...field, + type: FieldType.string, + values: newVals, + }; + } + + return field; + }); + }; diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index bce099ea1ca..b3cb08dce24 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -37,4 +37,5 @@ export enum DataTransformerID { limit = 'limit', partitionByValues = 'partitionByValues', timeSeriesTable = 'timeSeriesTable', + formatTime = 'formatTime', } diff --git a/packages/grafana-data/src/transformations/transformers/joinByField.ts b/packages/grafana-data/src/transformations/transformers/joinByField.ts index e6c1d386fa6..55a703f62b3 100644 --- a/packages/grafana-data/src/transformations/transformers/joinByField.ts +++ b/packages/grafana-data/src/transformations/transformers/joinByField.ts @@ -8,8 +8,9 @@ import { DataTransformerID } from './ids'; import { joinDataFrames } from './joinDataFrames'; export enum JoinMode { - outer = 'outer', + outer = 'outer', // best for time series, non duplicated join on values inner = 'inner', + outerTabular = 'outerTabular', // best for tabular data where the join on value can be duplicated } export interface JoinByFieldOptions { diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts index 70dddf23aaa..bcdf26f7628 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts @@ -2,6 +2,8 @@ import { toDataFrame } from '../../dataframe/processDataFrame'; import { getFieldDisplayName } from '../../field'; import { DataFrame, FieldType } from '../../types/dataFrame'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; +import { fieldMatchers } from '../matchers'; +import { FieldMatcherID } from '../matchers/ids'; import { calculateFieldTransformer } from './calculateField'; import { JoinMode } from './joinByField'; @@ -28,6 +30,8 @@ describe('align frames', () => { ], }); + // the following does not work for tabular joins where the joined on field value is duplicated + // the time will never have a dupicated time which is joined on it('should perform an outer join', () => { const out = joinDataFrames({ frames: [series1, series2] })!; expect( @@ -130,6 +134,89 @@ describe('align frames', () => { }); }); + describe('join tabular data by chosen field', () => { + // join on gender where there are multiple values, duplicate values which can increase the rows + + const tableData1 = toDataFrame({ + fields: [ + { name: 'gender', type: FieldType.string, values: ['MALE', 'MALE', 'MALE', 'FEMALE', 'FEMALE', 'FEMALE'] }, + { + name: 'day', + type: FieldType.string, + values: ['Wednesday', 'Tuesday', 'Monday', 'Wednesday', 'Tuesday', 'Monday'], + }, + { name: 'count', type: FieldType.number, values: [18, 72, 13, 17, 71, 7] }, + ], + }); + const tableData2 = toDataFrame({ + fields: [ + { name: 'gender', type: FieldType.string, values: ['MALE', 'FEMALE'] }, + { name: 'count', type: FieldType.number, values: [103, 95] }, + ], + }); + + it('should perform an outer join with duplicated values to join on', () => { + const out = joinDataFrames({ + frames: [tableData1, tableData2], + joinBy: fieldMatchers.get(FieldMatcherID.byName).get('gender'), + mode: JoinMode.outerTabular, + })!; + expect( + out.fields.map((f) => ({ + name: f.name, + values: f.values, + })) + ).toMatchInlineSnapshot(` + [ + { + "name": "gender", + "values": [ + "MALE", + "MALE", + "MALE", + "FEMALE", + "FEMALE", + "FEMALE", + ], + }, + { + "name": "day", + "values": [ + "Wednesday", + "Tuesday", + "Monday", + "Wednesday", + "Tuesday", + "Monday", + ], + }, + { + "name": "count", + "values": [ + 18, + 72, + 13, + 17, + 71, + 7, + ], + }, + { + "name": "count", + "values": [ + 103, + 103, + 103, + 95, + 95, + 95, + ], + }, + ] + `); + }); + }); + it('unsorted input keep indexes', () => { //---------- const series1 = toDataFrame({ diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index de8a0b163f1..411708e3b92 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -151,6 +151,10 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { const nullModes: JoinNullMode[][] = []; const allData: AlignedData[] = []; const originalFields: Field[] = []; + // store frame field order for tabular data join + const originalFieldsOrderByFrame: number[][] = []; + // all other fields that are not the join on are in the 1+ position (join is always the 0) + let fieldsOrder = 1; const joinFieldMatcher = getJoinMatcher(options); for (let frameIndex = 0; frameIndex < options.frames.length; frameIndex++) { @@ -163,6 +167,7 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { const nullModesFrame: JoinNullMode[] = [NULL_REMOVE]; let join: Field | undefined = undefined; let fields: Field[] = []; + let frameFieldsOrder = []; for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) { const field = frame.fields[fieldIndex]; @@ -220,12 +225,22 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { originalFields.push(field); // clear field displayName state delete field.state?.displayName; + // store frame field order for tabular data join + frameFieldsOrder.push(fieldsOrder); + fieldsOrder++; } - + // store frame field order for tabular data join + originalFieldsOrderByFrame.push(frameFieldsOrder); allData.push(a); } - const joined = join(allData, nullModes, options.mode); + let joined: Array> = []; + + if (options.mode === JoinMode.outerTabular) { + joined = joinOuterTabular(allData, originalFieldsOrderByFrame, originalFields.length, nullModes); + } else { + joined = join(allData, nullModes, options.mode); + } return { // ...options.data[0], // keep name, meta? @@ -237,6 +252,98 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { }; } +// The following full outer join allows for multiple/duplicated joined fields values where as the performant join from uplot creates a unique set of field values to be joined on +// http://www.silota.com/docs/recipes/sql-join-tutorial-javascript-examples.html +// The frame field value which is used join on is sorted to the 0 position of each table data in both tables and nullModes +// (not sure if we need nullModes) for nullModes, the field to join on is given NULL_REMOVE and all other fields are given NULL_EXPAND +function joinOuterTabular( + tables: AlignedData[], + originalFieldsOrderByFrame: number[][], + numberOfFields: number, + nullModes?: number[][] +) { + // we will iterate through all frames and check frames for matches preventing duplicates. + // we will store each matched frame "row" or field values at the same index in the following hash. + let duplicateHash: { [key: string]: Array } = {}; + + // iterate through the tables (frames) + // for each frame we get the field data where the data in the 0 pos is the value to join on + for (let tableIdx = 0; tableIdx < tables.length; tableIdx++) { + // the table (frame) to check for matches in other tables + let table = tables[tableIdx]; + // the field value to join on (the join value is always in the 0 position) + let joinOnTableField = table[0]; + + // now we iterate through the other table (frame) data to look for matches + for (let otherTablesIdx = 0; otherTablesIdx < tables.length; otherTablesIdx++) { + // do not match on the same table + if (otherTablesIdx === tableIdx) { + continue; + } + + let otherTable = tables[otherTablesIdx]; + let otherTableJoinOnField = otherTable[0]; + + // iterate through the field to join on from the first table + for ( + let joinTableFieldValuesIdx = 0; + joinTableFieldValuesIdx < joinOnTableField.length; + joinTableFieldValuesIdx++ + ) { + // create the joined data + // this has the orignalFields length and should start out undefined + // joined row + number of other fields in each frame + // the order of each field is important in how we + // 1 check for duplicates + // 2 transform the row back into fields for the joined frame + // 3 when there is no match for the row we keep the vals undefined + const tableJoinOnValue = joinOnTableField[joinTableFieldValuesIdx]; + const allOtherFields = numberOfFields - 1; + let joinedRow: Array = [tableJoinOnValue].concat(new Array(allOtherFields)); + + let tableFieldValIdx = 0; + for (let fieldsIdx = 1; fieldsIdx < table.length; fieldsIdx++) { + const joinRowIdx = originalFieldsOrderByFrame[tableIdx][tableFieldValIdx]; + joinedRow[joinRowIdx] = table[fieldsIdx][joinTableFieldValuesIdx]; + tableFieldValIdx++; + } + + for (let otherTableValuesIdx = 0; otherTableValuesIdx < otherTableJoinOnField.length; otherTableValuesIdx++) { + if (joinOnTableField[joinTableFieldValuesIdx] === otherTableJoinOnField[otherTableValuesIdx]) { + let tableFieldValIdx = 0; + for (let fieldsIdx = 1; fieldsIdx < otherTable.length; fieldsIdx++) { + const joinRowIdx = originalFieldsOrderByFrame[otherTablesIdx][tableFieldValIdx]; + joinedRow[joinRowIdx] = otherTable[fieldsIdx][otherTableValuesIdx]; + tableFieldValIdx++; + } + + break; + } + } + + // prevent duplicates by entering rows in a hash where keys are the rows + duplicateHash[JSON.stringify(joinedRow)] = joinedRow; + } + } + } + + // transform the joined rows into data for a dataframe + let data: Array> = []; + for (let field = 0; field < numberOfFields; field++) { + data.push(new Array(0)); + } + + for (let key in duplicateHash) { + const row = duplicateHash[key]; + + for (let valIdx = 0; valIdx < row.length; valIdx++) { + data[valIdx].push(row[valIdx]); + } + } + + return data; +} + //-------------------------------------------------------------------------------- // Below here is copied from uplot (MIT License) // https://github.com/leeoniya/uPlot/blob/master/src/utils.js#L325 diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 570d7400ec5..f28214c2e4c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -110,10 +110,12 @@ export interface FeatureToggles { transformationsRedesign?: boolean; toggleLabelsInLogsUI?: boolean; mlExpressions?: boolean; - disableTraceQLStreaming?: boolean; + traceQLStreaming?: boolean; grafanaAPIServer?: boolean; featureToggleAdminPage?: boolean; awsAsyncQueryCaching?: boolean; splitScopes?: boolean; azureMonitorDataplane?: boolean; + prometheusConfigOverhaulAuth?: boolean; + configurableSchedulerTick?: boolean; } diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 175f65417fb..9f631f17e6e 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 572988f638a..3df323cd340 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -307,7 +307,7 @@ export const Components = { * @deprecated use inputV2 instead */ input: () => 'input[id="data-source-picker"]', - inputV2: 'Select a data source', + inputV2: 'data-testid Select a data source', }, TimeZonePicker: { /** diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index caf6cae2d7f..f934fce091d 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana End-to-End Test Library", "keywords": [ "cli", @@ -63,7 +63,7 @@ "@babel/core": "7.22.1", "@babel/preset-env": "7.22.4", "@cypress/webpack-preprocessor": "5.17.0", - "@grafana/e2e-selectors": "10.1.0-pre", + "@grafana/e2e-selectors": "10.2.0-pre", "@grafana/tsconfig": "^1.2.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", "babel-loader": "9.1.2", diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index 71fd2085f4b..d4a645f4df5 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -1,7 +1,7 @@ { "name": "@grafana/eslint-plugin", "description": "ESLint rules for use within the Grafana repo. Not suitable (or supported) for external use.", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "main": "./index.cjs", "author": "Grafana Labs", "license": "Apache-2.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 4dce798898f..dc68119355d 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/runtime", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -37,10 +37,10 @@ "postpack": "mv package.json.bak package.json" }, "dependencies": { - "@grafana/data": "10.1.0-pre", - "@grafana/e2e-selectors": "10.1.0-pre", + "@grafana/data": "10.2.0-pre", + "@grafana/e2e-selectors": "10.2.0-pre", "@grafana/faro-web-sdk": "1.1.0", - "@grafana/ui": "10.1.0-pre", + "@grafana/ui": "10.2.0-pre", "history": "4.10.1", "lodash": "4.17.21", "rxjs": "7.8.0", diff --git a/packages/grafana-runtime/src/components/DataSourcePicker.tsx b/packages/grafana-runtime/src/components/DataSourcePicker.tsx index 8d527350366..b53e83f124d 100644 --- a/packages/grafana-runtime/src/components/DataSourcePicker.tsx +++ b/packages/grafana-runtime/src/components/DataSourcePicker.tsx @@ -188,7 +188,8 @@ export class DataSourcePicker extends PureComponent; @@ -51,10 +51,6 @@ export interface TempoQuery extends common.DataQuery { * @deprecated Query traces by span name */ spanName?: string; - /** - * Use the streaming API to get partial results as they are available - */ - streaming?: boolean; } export const defaultTempoQuery: Partial = { diff --git a/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts index 9379847c977..99910580613 100644 --- a/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum TestDataQueryType { Annotations = 'annotations', diff --git a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts index cde543411d7..1d0b5bede3d 100644 --- a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts @@ -9,7 +9,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum TextMode { Code = 'code', diff --git a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts index 34579cbf4c3..8f7ffde9faa 100644 --- a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options extends common.OptionsWithTimezones { legend: common.VizLegendOptions; diff --git a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts index efa9e95a8da..a6954740245 100644 --- a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; /** * Identical to timeseries... except it does not have timezone settings diff --git a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts index c399dabfa32..a070d6c1d39 100644 --- a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum SeriesMapping { Auto = 'auto', diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index a7467ee05e3..e66f4dda63b 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/toolkit", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana Toolkit", "keywords": [ "grafana", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ef7aef1ae37..02b239c7161 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -49,10 +49,10 @@ "dependencies": { "@emotion/css": "11.11.2", "@emotion/react": "11.11.1", - "@grafana/data": "10.1.0-pre", - "@grafana/e2e-selectors": "10.1.0-pre", + "@grafana/data": "10.2.0-pre", + "@grafana/e2e-selectors": "10.2.0-pre", "@grafana/faro-web-sdk": "1.1.0", - "@grafana/schema": "10.1.0-pre", + "@grafana/schema": "10.2.0-pre", "@leeoniya/ufuzzy": "1.0.8", "@monaco-editor/react": "4.5.1", "@popperjs/core": "2.11.6", diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx index 65617bfacb4..05cf8d84bbd 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx @@ -56,7 +56,7 @@ export function RefIDPicker({ value, data, onChange, placeholder }: Props) { const onFilterChange = useCallback( (v: SelectableValue) => { - onChange(v.value!); + onChange(v?.value!); }, [onChange] ); diff --git a/packages/grafana-ui/src/components/Modal/Modal.tsx b/packages/grafana-ui/src/components/Modal/Modal.tsx index 696458402a0..3598166fac6 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.tsx @@ -115,7 +115,7 @@ function ModalButtonRow({ leftItems, children }: { leftItems?: React.ReactNode; return (
- + {children}
diff --git a/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx index 368234d99af..08314d41474 100644 --- a/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx @@ -81,11 +81,11 @@ class UnthemedSelectOptionGroup extends PureComponent return (
- {/* TODO: fix keyboard a11y */} - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} -
+ {/*React Select doesn't support focusable option group headers, this will be skipped when using + the keyboard */} +
{label} - {' '} +
{expanded && children}
diff --git a/packages/grafana-ui/src/components/Tooltip/Popover.tsx b/packages/grafana-ui/src/components/Tooltip/Popover.tsx index ea11c2f7aef..cb760b3cf5e 100644 --- a/packages/grafana-ui/src/components/Tooltip/Popover.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popover.tsx @@ -34,18 +34,8 @@ interface Props extends Omit, 'content'> { class Popover extends PureComponent { render() { - const { - content, - show, - placement, - onMouseEnter, - onMouseLeave, - className, - wrapperClassName, - renderArrow, - referenceElement, - onKeyDown, - } = this.props; + const { content, show, placement, className, wrapperClassName, renderArrow, referenceElement, ...rest } = + this.props; return ( @@ -66,12 +56,7 @@ class Popover extends PureComponent { > {({ ref, style, placement, arrowProps, update }) => { return ( - // TODO: fix keyboard a11y - // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{ }} data-placement={placement} className={`${wrapperClassName}`} + {...rest} >
{typeof content === 'string' && content} diff --git a/packages/grafana-ui/src/themes/GlobalStyles/elements.ts b/packages/grafana-ui/src/themes/GlobalStyles/elements.ts index 322dac60be5..bb8278efe4e 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/elements.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/elements.ts @@ -43,7 +43,7 @@ export function getElementStyles(theme: GrafanaTheme2) { button: { letterSpacing: theme.typography.body.letterSpacing, - // '&:focus-visible': getFocusStyles(theme), + '&:focus-visible': getFocusStyles(theme), '&:focus': { outline: 'none', }, diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 97c08eaf6e0..b3497f5e6d4 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -38,6 +38,7 @@ import ( "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" @@ -69,10 +70,14 @@ func TestCallResource(t *testing.T) { reg := registry.ProvideService() angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) + + discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) + bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) + l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), - angularInspector, &fakes.FakeOauthService{}) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/expr/metrics.go b/pkg/expr/metrics.go index d6f8cabc3a1..4cdbdbbd7cd 100644 --- a/pkg/expr/metrics.go +++ b/pkg/expr/metrics.go @@ -23,7 +23,7 @@ func newMetrics(reg prometheus.Registerer) *metrics { Subsystem: metricsSubSystem, Name: "ds_queries_total", Help: "Number of datasource queries made via server side expression requests", - }, []string{"error", "dataplane"}), + }, []string{"error", "dataplane", "datasource_type"}), // older (No Namespace or Subsystem) expressionsQuerySummary: prometheus.NewSummaryVec( diff --git a/pkg/expr/ml.go b/pkg/expr/ml.go index 33c9e653ea8..35df536cb77 100644 --- a/pkg/expr/ml.go +++ b/pkg/expr/ml.go @@ -81,7 +81,7 @@ func (m *MLNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s * } logger.Debug("Data source queried", "responseType", responseType) useDataplane := strings.HasPrefix("dataplane-", responseType) - s.metrics.dsRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane)).Inc() + s.metrics.dsRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane), mlPluginID).Inc() }() // Execute the command and provide callback function for sending a request via plugin API. diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index eab6ff830a0..19eaccae8e3 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -235,7 +235,7 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s } logger.Debug("Data source queried", "responseType", responseType) useDataplane := strings.HasPrefix(responseType, "dataplane-") - s.metrics.dsRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane)).Inc() + s.metrics.dsRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane), dn.datasource.Type).Inc() }() resp, err := s.dataService.QueryData(ctx, req) diff --git a/pkg/plugins/manager/loader/assetpath/assetpath.go b/pkg/plugins/manager/loader/assetpath/assetpath.go index 76ee0637805..0b093b0b753 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/pluginscdn" ) @@ -22,6 +23,10 @@ func ProvideService(cdn *pluginscdn.Service) *Service { return &Service{cdn: cdn} } +func DefaultService(cfg *config.Cfg) *Service { + return &Service{cdn: pluginscdn.ProvideService(cfg)} +} + // Base returns the base path for the specified plugin. func (s *Service) Base(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { if class == plugins.ClassCore { diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 9852efcf9e7..88b7e722e8e 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -3,38 +3,35 @@ package loader import ( "context" "errors" - "fmt" - "path" - "strings" "time" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/oauth" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/util" ) var _ plugins.ErrorResolver = (*Loader)(nil) type Loader struct { - pluginFinder finder.Finder + discovery discovery.Discoverer + bootstrap bootstrap.Bootstrapper + processManager process.Service pluginRegistry registry.Service roleRegistry plugins.RoleRegistry pluginInitializer initializer.Initializer signatureValidator signature.Validator - signatureCalculator plugins.SignatureCalculator externalServiceRegistry oauth.ExternalServiceRegistry assetPath *assetpath.Service log log.Logger @@ -46,24 +43,23 @@ type Loader struct { } func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, - pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder, - roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, signatureCalculator plugins.SignatureCalculator, - angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader { + pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, + roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, + angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader { return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), - roleRegistry, assetPath, pluginFinder, signatureCalculator, angularInspector, externalServiceRegistry) + roleRegistry, assetPath, angularInspector, externalServiceRegistry, discovery, bootstrap) } func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, - processManager process.Service, roleRegistry plugins.RoleRegistry, - assetPath *assetpath.Service, pluginFinder finder.Finder, signatureCalculator plugins.SignatureCalculator, - angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader { + processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, + angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader { return &Loader{ - pluginFinder: pluginFinder, pluginRegistry: pluginRegistry, pluginInitializer: initializer.New(cfg, backendProvider, license), signatureValidator: signature.NewValidator(authorizer), - signatureCalculator: signatureCalculator, processManager: processManager, errs: make(map[string]*plugins.SignatureError), log: log.New("plugin.loader"), @@ -72,70 +68,29 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo assetPath: assetPath, angularInspector: angularInspector, externalServiceRegistry: externalServiceRegistry, + discovery: discovery, + bootstrap: bootstrap, } } func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { - found, err := l.pluginFinder.Find(ctx, src) + // + discoveredPlugins, err := l.discovery.Discover(ctx, src) if err != nil { return nil, err } + // - return l.loadPlugins(ctx, src, found) -} - -// nolint:gocyclo -func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) { - loadedPlugins := make([]*plugins.Plugin, 0, len(found)) - - for _, p := range found { - if _, exists := l.pluginRegistry.Plugin(ctx, p.Primary.JSONData.ID); exists { - l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID) - continue - } - - sig, err := l.signatureCalculator.Calculate(ctx, src, p.Primary) - if err != nil { - l.log.Warn("Could not calculate plugin signature state", "pluginID", p.Primary.JSONData.ID, "err", err) - continue - } - plugin, err := l.createPluginBase(p.Primary.JSONData, src.PluginClass(ctx), p.Primary.FS) - if err != nil { - l.log.Error("Could not create primary plugin base", "pluginID", p.Primary.JSONData.ID, "err", err) - continue - } - - plugin.Signature = sig.Status - plugin.SignatureType = sig.Type - plugin.SignatureOrg = sig.SigningOrg - - loadedPlugins = append(loadedPlugins, plugin) - - for _, c := range p.Children { - if _, exists := l.pluginRegistry.Plugin(ctx, c.JSONData.ID); exists { - l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID) - continue - } - - cp, err := l.createPluginBase(c.JSONData, plugin.Class, c.FS) - if err != nil { - l.log.Error("Could not create child plugin base", "pluginID", p.Primary.JSONData.ID, "err", err) - continue - } - cp.Parent = plugin - cp.Signature = sig.Status - cp.SignatureType = sig.Type - cp.SignatureOrg = sig.SigningOrg - - plugin.Children = append(plugin.Children, cp) - - loadedPlugins = append(loadedPlugins, cp) - } + // + bootstrappedPlugins, err := l.bootstrap.Bootstrap(ctx, src, discoveredPlugins) + if err != nil { + return nil, err } + // - // validate signatures - verifiedPlugins := make([]*plugins.Plugin, 0, len(loadedPlugins)) - for _, plugin := range loadedPlugins { + // + verifiedPlugins := make([]*plugins.Plugin, 0, len(bootstrappedPlugins)) + for _, plugin := range bootstrappedPlugins { signingError := l.signatureValidator.Validate(plugin) if signingError != nil { l.log.Warn("Skipping loading plugin due to problem with signature", @@ -149,14 +104,6 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun // clear plugin error if a pre-existing error has since been resolved delete(l.errs, plugin.ID) - // Hardcoded alias changes - switch plugin.ID { - case "grafana-pyroscope-datasource": // rebranding - plugin.Alias = "phlare" - case "debug": // panel plugin used for testing - plugin.Alias = "debugX" - } - // verify module.js exists for SystemJS to load. // CDN plugins can be loaded with plugin.json only, so do not warn for those. if !plugin.IsRenderer() && !plugin.IsCorePlugin() { @@ -173,37 +120,56 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun } } - if plugin.IsApp() { - setDefaultNavURL(plugin) - } + // detect angular for external plugins + if plugin.IsExternalPlugin() { + var err error - if plugin.Parent != nil && plugin.Parent.IsApp() { - configureAppChildPlugin(plugin.Parent, plugin) + cctx, canc := context.WithTimeout(ctx, time.Second*10) + plugin.AngularDetected, err = l.angularInspector.Inspect(cctx, plugin) + canc() + + if err != nil { + l.log.Warn("Could not inspect plugin for angular", "pluginID", plugin.ID, "err", err) + } + + // Do not initialize plugins if they're using Angular and Angular support is disabled + if plugin.AngularDetected && !l.cfg.AngularSupportEnabled { + l.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", plugin.ID) + continue + } } verifiedPlugins = append(verifiedPlugins, plugin) } + // - // initialize plugins + // initializedPlugins := make([]*plugins.Plugin, 0, len(verifiedPlugins)) for _, p := range verifiedPlugins { - // detect angular for external plugins - if p.IsExternalPlugin() { - var err error + err = l.pluginInitializer.Initialize(ctx, p) + if err != nil { + l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err) + continue + } - cctx, canc := context.WithTimeout(ctx, time.Second*10) - p.AngularDetected, err = l.angularInspector.Inspect(cctx, p) - canc() + if err = l.pluginRegistry.Add(ctx, p); err != nil { + l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + continue + } - if err != nil { - l.log.Warn("Could not inspect plugin for angular", "pluginID", p.ID, "err", err) - } + if !p.IsCorePlugin() { + l.log.Info("Plugin registered", "pluginID", p.ID) + } - // Do not initialize plugins if they're using Angular and Angular support is disabled - if p.AngularDetected && !l.cfg.AngularSupportEnabled { - l.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", p.ID) - continue - } + initializedPlugins = append(initializedPlugins, p) + } + // + + // + for _, p := range initializedPlugins { + if err = l.processManager.Start(ctx, p.ID); err != nil { + l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + continue } if p.ExternalServiceRegistration != nil && l.cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAuth) { @@ -215,27 +181,15 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun p.ExternalService = s } - err := l.pluginInitializer.Initialize(ctx, p) - if err != nil { - l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err) - continue - } - if errDeclareRoles := l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); errDeclareRoles != nil { - l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", errDeclareRoles) - } - - initializedPlugins = append(initializedPlugins, p) - } - - for _, p := range initializedPlugins { - if err := l.load(ctx, p); err != nil { - l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + if err = l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); err != nil { + l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", err) } if !p.IsCorePlugin() && !p.IsBundledPlugin() { metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature)) } } + // return initializedPlugins, nil } @@ -256,18 +210,6 @@ func (l *Loader) Unload(ctx context.Context, pluginID string) error { return nil } -func (l *Loader) load(ctx context.Context, p *plugins.Plugin) error { - if err := l.pluginRegistry.Add(ctx, p); err != nil { - return err - } - - if !p.IsCorePlugin() { - l.log.Info("Plugin registered", "pluginID", p.ID) - } - - return l.processManager.Start(ctx, p.ID) -} - func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { l.log.Debug("Stopping plugin process", "pluginId", p.ID) @@ -289,94 +231,6 @@ func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { return nil } -func (l *Loader) createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, files plugins.FS) (*plugins.Plugin, error) { - baseURL, err := l.assetPath.Base(pluginJSON, class, files.Base()) - if err != nil { - return nil, fmt.Errorf("base url: %w", err) - } - moduleURL, err := l.assetPath.Module(pluginJSON, class, files.Base()) - if err != nil { - return nil, fmt.Errorf("module url: %w", err) - } - plugin := &plugins.Plugin{ - JSONData: pluginJSON, - FS: files, - BaseURL: baseURL, - Module: moduleURL, - Class: class, - } - - plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) - if err := l.setImages(plugin); err != nil { - return nil, err - } - - return plugin, nil -} - -func (l *Loader) setImages(p *plugins.Plugin) error { - var err error - for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { - *dst, err = l.assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) - if err != nil { - return fmt.Errorf("logo: %w", err) - } - } - for i := 0; i < len(p.Info.Screenshots); i++ { - screenshot := &p.Info.Screenshots[i] - screenshot.Path, err = l.assetPath.RelativeURL(p, screenshot.Path, "") - if err != nil { - return fmt.Errorf("screenshot %d relative url: %w", i, err) - } - } - return nil -} - -func setDefaultNavURL(p *plugins.Plugin) { - // slugify pages - for _, include := range p.Includes { - if include.Slug == "" { - include.Slug = slugify.Slugify(include.Name) - } - - if !include.DefaultNav { - continue - } - - if include.Type == "page" { - p.DefaultNavURL = path.Join("/plugins/", p.ID, "/page/", include.Slug) - } - if include.Type == "dashboard" { - dboardURL := include.DashboardURLPath() - if dboardURL == "" { - p.Logger().Warn("Included dashboard is missing a UID field") - continue - } - - p.DefaultNavURL = dboardURL - } - } -} - -func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { - if !parent.IsApp() { - return - } - appSubPath := strings.ReplaceAll(strings.Replace(child.FS.Base(), parent.FS.Base(), "", 1), "\\", "/") - child.IncludedInAppID = parent.ID - child.BaseURL = parent.BaseURL - - if parent.IsCorePlugin() { - child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module" - } else { - child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module" - } -} - -func defaultLogoPath(pluginType plugins.Type) string { - return "public/img/icn-" + string(pluginType) + ".svg" -} - func (l *Loader) PluginErrors() []*plugins.Error { errs := make([]*plugins.Error, 0, len(l.errs)) for _, err := range l.errs { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index b93eac7daf0..68f3a3275ab 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -18,10 +18,11 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/org" @@ -543,61 +544,6 @@ func TestLoader_Load_CustomSource(t *testing.T) { }) } -func TestLoader_setDefaultNavURL(t *testing.T) { - t.Run("When including a dashboard with DefaultNav: true", func(t *testing.T) { - pluginWithDashboard := &plugins.Plugin{ - JSONData: plugins.JSONData{Includes: []*plugins.Includes{ - { - Type: "dashboard", - DefaultNav: true, - UID: "", - }, - }}, - } - logger := log.NewTestLogger() - pluginWithDashboard.SetLogger(logger) - - t.Run("Default nav URL is not set if dashboard UID field not is set", func(t *testing.T) { - setDefaultNavURL(pluginWithDashboard) - require.Equal(t, "", pluginWithDashboard.DefaultNavURL) - require.NotZero(t, logger.WarnLogs.Calls) - require.Equal(t, "Included dashboard is missing a UID field", logger.WarnLogs.Message) - }) - - t.Run("Default nav URL is set if dashboard UID field is set", func(t *testing.T) { - pluginWithDashboard.Includes[0].UID = "a1b2c3" - - setDefaultNavURL(pluginWithDashboard) - require.Equal(t, "/d/a1b2c3", pluginWithDashboard.DefaultNavURL) - }) - }) - - t.Run("When including a page with DefaultNav: true", func(t *testing.T) { - pluginWithPage := &plugins.Plugin{ - JSONData: plugins.JSONData{Includes: []*plugins.Includes{ - { - Type: "page", - DefaultNav: true, - Slug: "testPage", - }, - }}, - } - - t.Run("Default nav URL is set using slug", func(t *testing.T) { - setDefaultNavURL(pluginWithPage) - require.Equal(t, "/plugins/page/testPage", pluginWithPage.DefaultNavURL) - }) - - t.Run("Default nav URL is set using slugified Name field if Slug field is empty", func(t *testing.T) { - pluginWithPage.Includes[0].Slug = "" - pluginWithPage.Includes[0].Name = "My Test Page" - - setDefaultNavURL(pluginWithPage) - require.Equal(t, "/plugins/page/my-test-page", pluginWithPage.DefaultNavURL) - }) - }) -} - func TestLoader_Load_MultiplePlugins(t *testing.T) { parentDir, err := filepath.Abs("../") if err != nil { @@ -1257,13 +1203,19 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { child.Parent = parent t.Run("Load nested External plugins", func(t *testing.T) { - reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() l := newLoader(t, &config.Cfg{}, func(l *Loader) { - l.pluginRegistry = reg l.processManager = procMgr l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) + l.discovery = discovery.New(l.cfg, discovery.Opts{ + FindFilterFuncs: []discovery.FindFilterFunc{ + func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + return discovery.NewDuplicatePluginFilterStep(l.pluginRegistry).Filter(ctx, bundles) + }, + }, + }, + ) }) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ @@ -1286,7 +1238,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } - verifyState(t, expected, reg, procPrvdr, procMgr) + verifyState(t, expected, l.pluginRegistry, procPrvdr, procMgr) t.Run("Load will exclude plugins that already exist", func(t *testing.T) { got, err := l.Load(context.Background(), &fakes.FakePluginSource{ @@ -1308,7 +1260,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } - verifyState(t, expected, reg, procPrvdr, procMgr) + verifyState(t, expected, l.pluginRegistry, procPrvdr, procMgr) }) }) @@ -1466,36 +1418,15 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }) } -func Test_setPathsBasedOnApp(t *testing.T) { - t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) { - child := &plugins.Plugin{ - FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app\\datasources\\datasource"), - } - parent := &plugins.Plugin{ - JSONData: plugins.JSONData{ - Type: plugins.TypeApp, - ID: "testdata-app", - }, - Class: plugins.ClassCore, - FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app"), - BaseURL: "public/app/plugins/app/testdata-app", - } - - configureAppChildPlugin(parent, child) - - require.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module) - require.Equal(t, "testdata-app", child.IncludedInAppID) - require.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL) - }) -} - func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { angularInspector, err := angularinspector.NewStaticInspector() + reg := fakes.NewFakePluginRegistry() + assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) require.NoError(t, err) - l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), + l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), reg, fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg.DevMode), - signature.ProvideService(statickey.New()), angularInspector, &fakes.FakeOauthService{}) + assets, angularInspector, &fakes.FakeOauthService{}, + discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{})) for _, cb := range cbs { cb(l) @@ -1504,13 +1435,15 @@ func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Load return l } -func verifyState(t *testing.T, ps []*plugins.Plugin, reg *fakes.FakePluginRegistry, +func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service, procPrvdr *fakes.FakeBackendProcessProvider, procMngr *fakes.FakeProcessManager) { t.Helper() for _, p := range ps { - if !cmp.Equal(p, reg.Store[p.ID], compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, reg.Store[p.ID], compareOpts...)) + regP, exists := reg.Plugin(context.Background(), p.ID) + require.True(t, exists) + if !cmp.Equal(p, regP, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, regP, compareOpts...)) } if p.Backend { diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index f87061bbee5..9526f49b7f7 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" @@ -22,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/registry" @@ -35,6 +35,7 @@ import ( "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" @@ -120,10 +121,14 @@ func TestIntegrationPluginManager(t *testing.T) { lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) + + discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) + bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) + l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), - angularInspector, &fakes.FakeOauthService{}) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go new file mode 100644 index 00000000000..e4b8f2a45b8 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -0,0 +1,78 @@ +package bootstrap + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/manager/signature" +) + +// Bootstrapper is responsible for the Bootstrap stage of the plugin loader pipeline. +type Bootstrapper interface { + Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) +} + +// ConstructFunc is the function used for the Construct step of the Bootstrap stage. +type ConstructFunc func(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) + +// DecorateFunc is the function used for the Decorate step of the Bootstrap stage. +type DecorateFunc func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) + +// Bootstrap implements the Bootstrapper interface. +// +// The Bootstrap stage is made up of the following steps (in order): +// - Construct: Create the initial plugin structs based on the plugin(s) found in the Discovery stage. +// - Decorate: Decorate the plugins with additional metadata. +// +// The Construct step is implemented by the ConstructFunc type. +// +// The Decorate step is implemented by the DecorateFunc type. +type Bootstrap struct { + constructStep ConstructFunc + decorateSteps []DecorateFunc + log log.Logger +} + +type Opts struct { + ConstructFunc ConstructFunc + DecorateFuncs []DecorateFunc +} + +// New returns a new Bootstrap stage. +func New(cfg *config.Cfg, opts Opts) *Bootstrap { + if opts.ConstructFunc == nil { + opts.ConstructFunc = DefaultConstructFunc(signature.DefaultCalculator(), assetpath.DefaultService(cfg)) + } + + if len(opts.DecorateFuncs) == 0 { + opts.DecorateFuncs = DefaultDecorateFuncs + } + + return &Bootstrap{ + constructStep: opts.ConstructFunc, + decorateSteps: opts.DecorateFuncs, + log: log.New("plugins.bootstrap"), + } +} + +// Bootstrap will execute the Construct and Decorate steps of the Bootstrap stage. +func (b *Bootstrap) Bootstrap(ctx context.Context, src plugins.PluginSource, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) { + ps, err := b.constructStep(ctx, src, found) + if err != nil { + return nil, err + } + + for _, p := range ps { + for _, decorator := range b.decorateSteps { + p, err = decorator(ctx, p) + if err != nil { + return nil, err + } + } + } + + return ps, nil +} diff --git a/pkg/plugins/manager/pipeline/bootstrap/doc.go b/pkg/plugins/manager/pipeline/bootstrap/doc.go new file mode 100644 index 00000000000..d601296c698 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/doc.go @@ -0,0 +1,6 @@ +// Package bootstrap defines the second stage of the plugin loader pipeline. +// +// The Bootstrap stage must implement the Bootstrapper interface. +// - Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) + +package bootstrap diff --git a/pkg/plugins/manager/pipeline/bootstrap/factory.go b/pkg/plugins/manager/pipeline/bootstrap/factory.go new file mode 100644 index 00000000000..5e179b1f31c --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/factory.go @@ -0,0 +1,76 @@ +package bootstrap + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" +) + +type pluginFactoryFunc func(p plugins.FoundPlugin, pluginClass plugins.Class, sig plugins.Signature) (*plugins.Plugin, error) + +// DefaultPluginFactory is the default plugin factory used by the Construct step of the Bootstrap stage. +// +// It creates the plugin using plugin information found during the Discovery stage and makes use of the assetPath +// service to set the plugin's BaseURL, Module, Logos and Screenshots fields. +type DefaultPluginFactory struct { + assetPath *assetpath.Service +} + +// NewDefaultPluginFactory returns a new DefaultPluginFactory. +func NewDefaultPluginFactory(assetPath *assetpath.Service) *DefaultPluginFactory { + return &DefaultPluginFactory{assetPath: assetPath} +} + +func (f *DefaultPluginFactory) createPlugin(p plugins.FoundPlugin, class plugins.Class, + sig plugins.Signature) (*plugins.Plugin, error) { + baseURL, err := f.assetPath.Base(p.JSONData, class, p.FS.Base()) + if err != nil { + return nil, fmt.Errorf("base url: %w", err) + } + moduleURL, err := f.assetPath.Module(p.JSONData, class, p.FS.Base()) + if err != nil { + return nil, fmt.Errorf("module url: %w", err) + } + + plugin := &plugins.Plugin{ + JSONData: p.JSONData, + FS: p.FS, + BaseURL: baseURL, + Module: moduleURL, + Class: class, + Signature: sig.Status, + SignatureType: sig.Type, + SignatureOrg: sig.SigningOrg, + } + plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) + + if err = setImages(plugin, f.assetPath); err != nil { + return nil, err + } + + return plugin, nil +} + +func setImages(p *plugins.Plugin, assetPath *assetpath.Service) error { + var err error + for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { + *dst, err = assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) + if err != nil { + return fmt.Errorf("logo: %w", err) + } + } + for i := 0; i < len(p.Info.Screenshots); i++ { + screenshot := &p.Info.Screenshots[i] + screenshot.Path, err = assetPath.RelativeURL(p, screenshot.Path, "") + if err != nil { + return fmt.Errorf("screenshot %d relative url: %w", i, err) + } + } + return nil +} + +func defaultLogoPath(pluginType plugins.Type) string { + return fmt.Sprintf("public/img/icn-%s.svg", string(pluginType)) +} diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps.go b/pkg/plugins/manager/pipeline/bootstrap/steps.go new file mode 100644 index 00000000000..4d808b00b73 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/steps.go @@ -0,0 +1,146 @@ +package bootstrap + +import ( + "context" + "path" + "strings" + + "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/util" +) + +// DefaultConstructor implements the default ConstructFunc used for the Construct step of the Bootstrap stage. +// +// It uses a pluginFactoryFunc to create plugins and the signatureCalculator to calculate the plugin's signature state. +type DefaultConstructor struct { + pluginFactoryFunc pluginFactoryFunc + signatureCalculator plugins.SignatureCalculator + log log.Logger +} + +// DefaultConstructFunc is the default ConstructFunc used for the Construct step of the Bootstrap stage. +func DefaultConstructFunc(signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) ConstructFunc { + return NewDefaultConstructor(signatureCalculator, assetPath).Construct +} + +// DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage. +var DefaultDecorateFuncs = []DecorateFunc{ + AliasDecorateFunc, + AppDefaultNavURLDecorateFunc, + AppChildDecorateFunc, +} + +// NewDefaultConstructor returns a new DefaultConstructor. +func NewDefaultConstructor(signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *DefaultConstructor { + return &DefaultConstructor{ + pluginFactoryFunc: NewDefaultPluginFactory(assetPath).createPlugin, + signatureCalculator: signatureCalculator, + log: log.New("plugins.construct"), + } +} + +// Construct will calculate the plugin's signature state and create the plugin using the pluginFactoryFunc. +func (c *DefaultConstructor) Construct(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) { + res := make([]*plugins.Plugin, 0, len(bundles)) + + for _, bundle := range bundles { + sig, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary) + if err != nil { + c.log.Warn("Could not calculate plugin signature state", "pluginID", bundle.Primary.JSONData.ID, "err", err) + continue + } + plugin, err := c.pluginFactoryFunc(bundle.Primary, src.PluginClass(ctx), sig) + if err != nil { + c.log.Error("Could not create primary plugin base", "pluginID", bundle.Primary.JSONData.ID, "err", err) + continue + } + res = append(res, plugin) + + children := make([]*plugins.Plugin, 0, len(bundle.Children)) + for _, child := range bundle.Children { + cp, err := c.pluginFactoryFunc(*child, plugin.Class, sig) + if err != nil { + c.log.Error("Could not create child plugin base", "pluginID", child.JSONData.ID, "err", err) + continue + } + cp.Parent = plugin + plugin.Children = append(plugin.Children, cp) + + children = append(children, cp) + } + res = append(res, children...) + } + + return res, nil +} + +// AliasDecorateFunc is a DecorateFunc that sets the alias for the plugin. +func AliasDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + switch p.ID { + case "grafana-pyroscope-datasource": // rebranding + p.Alias = "phlare" + case "debug": // panel plugin used for testing + p.Alias = "debugX" + } + return p, nil +} + +// AppDefaultNavURLDecorateFunc is a DecorateFunc that sets the default nav URL for app plugins. +func AppDefaultNavURLDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if p.IsApp() { + setDefaultNavURL(p) + } + return p, nil +} + +func setDefaultNavURL(p *plugins.Plugin) { + // slugify pages + for _, include := range p.Includes { + if include.Slug == "" { + include.Slug = slugify.Slugify(include.Name) + } + + if !include.DefaultNav { + continue + } + + if include.Type == "page" { + p.DefaultNavURL = path.Join("/plugins/", p.ID, "/page/", include.Slug) + } + if include.Type == "dashboard" { + dboardURL := include.DashboardURLPath() + if dboardURL == "" { + p.Logger().Warn("Included dashboard is missing a UID field") + continue + } + + p.DefaultNavURL = dboardURL + } + } +} + +// AppChildDecorateFunc is a DecorateFunc that configures child plugins of app plugins. +func AppChildDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if p.Parent != nil && p.Parent.IsApp() { + configureAppChildPlugin(p.Parent, p) + } + return p, nil +} + +func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { + if !parent.IsApp() { + return + } + appSubPath := strings.ReplaceAll(strings.Replace(child.FS.Base(), parent.FS.Base(), "", 1), "\\", "/") + child.IncludedInAppID = parent.ID + child.BaseURL = parent.BaseURL + + if parent.IsCorePlugin() { + child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module" + } else { + child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module" + } +} diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps_test.go b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go new file mode 100644 index 00000000000..f22ed0478b1 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go @@ -0,0 +1,89 @@ +package bootstrap + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" +) + +func TestSetDefaultNavURL(t *testing.T) { + t.Run("When including a dashboard with DefaultNav: true", func(t *testing.T) { + pluginWithDashboard := &plugins.Plugin{ + JSONData: plugins.JSONData{Includes: []*plugins.Includes{ + { + Type: "dashboard", + DefaultNav: true, + UID: "", + }, + }}, + } + logger := log.NewTestLogger() + pluginWithDashboard.SetLogger(logger) + + t.Run("Default nav URL is not set if dashboard UID field not is set", func(t *testing.T) { + setDefaultNavURL(pluginWithDashboard) + require.Equal(t, "", pluginWithDashboard.DefaultNavURL) + require.NotZero(t, logger.WarnLogs.Calls) + require.Equal(t, "Included dashboard is missing a UID field", logger.WarnLogs.Message) + }) + + t.Run("Default nav URL is set if dashboard UID field is set", func(t *testing.T) { + pluginWithDashboard.Includes[0].UID = "a1b2c3" + + setDefaultNavURL(pluginWithDashboard) + require.Equal(t, "/d/a1b2c3", pluginWithDashboard.DefaultNavURL) + }) + }) + + t.Run("When including a page with DefaultNav: true", func(t *testing.T) { + pluginWithPage := &plugins.Plugin{ + JSONData: plugins.JSONData{Includes: []*plugins.Includes{ + { + Type: "page", + DefaultNav: true, + Slug: "testPage", + }, + }}, + } + + t.Run("Default nav URL is set using slug", func(t *testing.T) { + setDefaultNavURL(pluginWithPage) + require.Equal(t, "/plugins/page/testPage", pluginWithPage.DefaultNavURL) + }) + + t.Run("Default nav URL is set using slugified Name field if Slug field is empty", func(t *testing.T) { + pluginWithPage.Includes[0].Slug = "" + pluginWithPage.Includes[0].Name = "My Test Page" + + setDefaultNavURL(pluginWithPage) + require.Equal(t, "/plugins/page/my-test-page", pluginWithPage.DefaultNavURL) + }) + }) +} + +func TestSetPathsBasedOnApp(t *testing.T) { + t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) { + child := &plugins.Plugin{ + FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app\\datasources\\datasource"), + } + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ + Type: plugins.TypeApp, + ID: "testdata-app", + }, + Class: plugins.ClassCore, + FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app"), + BaseURL: "public/app/plugins/app/testdata-app", + } + + configureAppChildPlugin(parent, child) + + require.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module) + require.Equal(t, "testdata-app", child.IncludedInAppID) + require.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL) + }) +} diff --git a/pkg/plugins/manager/pipeline/discovery/discovery.go b/pkg/plugins/manager/pipeline/discovery/discovery.go new file mode 100644 index 00000000000..f24b2b5bc75 --- /dev/null +++ b/pkg/plugins/manager/pipeline/discovery/discovery.go @@ -0,0 +1,74 @@ +package discovery + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" +) + +// Discoverer is responsible for the Discovery stage of the plugin loader pipeline. +type Discoverer interface { + Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) +} + +// FindFunc is the function used for the Find step of the Discovery stage. +type FindFunc func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) + +// FindFilterFunc is the function used for the Filter step of the Discovery stage. +type FindFilterFunc func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) + +// Discovery implements the Discoverer interface. +// +// The Discovery stage is made up of the following steps (in order): +// - Find: Find plugins (from disk, remote, etc.) +// - Filter: Filter the results based on some criteria. +// +// The Find step is implemented by the FindFunc type. +// +// The Filter step is implemented by the FindFilterFunc type. +type Discovery struct { + findStep FindFunc + findFilterSteps []FindFilterFunc + log log.Logger +} + +type Opts struct { + FindFunc FindFunc + FindFilterFuncs []FindFilterFunc +} + +// New returns a new Discovery stage. +func New(cfg *config.Cfg, opts Opts) *Discovery { + if opts.FindFunc == nil { + opts.FindFunc = DefaultFindFunc(cfg) + } + + if len(opts.FindFilterFuncs) == 0 { + opts.FindFilterFuncs = []FindFilterFunc{} // no filters by default + } + + return &Discovery{ + findStep: opts.FindFunc, + findFilterSteps: opts.FindFilterFuncs, + log: log.New("plugins.discovery"), + } +} + +// Discover will execute the Find and Filter steps of the Discovery stage. +func (d *Discovery) Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { + found, err := d.findStep(ctx, src) + if err != nil { + return nil, err + } + + for _, filterStep := range d.findFilterSteps { + found, err = filterStep(ctx, src.PluginClass(ctx), found) + if err != nil { + return nil, err + } + } + + return found, nil +} diff --git a/pkg/plugins/manager/pipeline/discovery/doc.go b/pkg/plugins/manager/pipeline/discovery/doc.go new file mode 100644 index 00000000000..37d91ce8592 --- /dev/null +++ b/pkg/plugins/manager/pipeline/discovery/doc.go @@ -0,0 +1,6 @@ +// Package discovery defines the first stage of the plugin loader pipeline. + +// The Discovery stage must implement the Discoverer interface. +// - Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) + +package discovery diff --git a/pkg/plugins/manager/pipeline/discovery/steps.go b/pkg/plugins/manager/pipeline/discovery/steps.go new file mode 100644 index 00000000000..a51d2c4c2eb --- /dev/null +++ b/pkg/plugins/manager/pipeline/discovery/steps.go @@ -0,0 +1,55 @@ +package discovery + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/registry" +) + +// DefaultFindFunc is the default function used for the Find step of the Discovery stage. It will scan the local +// filesystem for plugins. +func DefaultFindFunc(cfg *config.Cfg) FindFunc { + return finder.NewLocalFinder(cfg.DevMode).Find +} + +// DuplicatePluginValidation is a filter step that will filter out any plugins that are already registered with the +// registry. This includes both the primary plugin and any child plugins, which are matched using the plugin ID field. +type DuplicatePluginValidation struct { + registry registry.Service + log log.Logger +} + +// NewDuplicatePluginFilterStep returns a new DuplicatePluginValidation. +func NewDuplicatePluginFilterStep(registry registry.Service) *DuplicatePluginValidation { + return &DuplicatePluginValidation{ + registry: registry, + log: log.New("plugins.dedupe"), + } +} + +// Filter will filter out any plugins that are already registered with the registry. +func (d *DuplicatePluginValidation) Filter(ctx context.Context, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + res := make([]*plugins.FoundBundle, 0, len(bundles)) + for _, b := range bundles { + _, exists := d.registry.Plugin(ctx, b.Primary.JSONData.ID) + if exists { + d.log.Warn("Skipping loading of plugin as it's a duplicate", "pluginID", b.Primary.JSONData.ID) + continue + } + + for _, child := range b.Children { + _, exists = d.registry.Plugin(ctx, child.JSONData.ID) + if exists { + d.log.Warn("Skipping loading of child plugin as it's a duplicate", "pluginID", child.JSONData.ID) + continue + } + } + res = append(res, b) + } + + return res, nil +} diff --git a/pkg/plugins/manager/pipeline/doc.go b/pkg/plugins/manager/pipeline/doc.go new file mode 100644 index 00000000000..9b04096e158 --- /dev/null +++ b/pkg/plugins/manager/pipeline/doc.go @@ -0,0 +1,11 @@ +// Package pipeline defines a load pipeline for Grafana plugins. +// +// A pipeline is a sequence of stages that are executed in order. Each stage is made up of a series of steps. +// A plugin loader pipeline is defined by the following stages: +// Discovery: Find plugins (e.g. from disk, remote, etc.), and [optionally] filter the results based on some criteria. +// Bootstrap: Create the plugins found in the discovery stage and enrich them with metadata. +// Verification: Verify the plugins based on some criteria (e.g. signature validation, angular detection, etc.) +// Initialization: Initialize the plugin for use (e.g. register with Grafana, etc.) +// Post-Initialization: Perform any post-initialization tasks (e.g. start the backend process, declare RBAC roles etc.) + +package pipeline diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index ffbc25230ba..93829a76809 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/setting" ) @@ -64,9 +65,20 @@ type Signature struct { var _ plugins.SignatureCalculator = &Signature{} func ProvideService(kr plugins.KeyRetriever) *Signature { + return NewCalculator(kr) +} + +func NewCalculator(kr plugins.KeyRetriever) *Signature { return &Signature{ kr: kr, - log: log.New("plugin.signature"), + log: log.New("plugins.signature"), + } +} + +func DefaultCalculator() *Signature { + return &Signature{ + kr: statickey.New(), + log: log.New("plugins.signature"), } } diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index ae597e24e2b..1a679f50288 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -47,8 +47,8 @@ var wireExtsBasicSet = wire.NewSet( authimpl.ProvideUserAuthTokenService, wire.Bind(new(auth.UserTokenService), new(*authimpl.UserAuthTokenService)), wire.Bind(new(auth.UserTokenBackgroundService), new(*authimpl.UserAuthTokenService)), - anonimpl.ProvideAnonymousSessionService, - wire.Bind(new(anonymous.Service), new(*anonimpl.AnonSessionService)), + anonimpl.ProvideAnonymousDeviceService, + wire.Bind(new(anonymous.Service), new(*anonimpl.AnonDeviceService)), licensing.ProvideService, wire.Bind(new(licensing.Licensing), new(*licensing.OSSLicensingService)), setting.ProvideProvider, diff --git a/pkg/services/anonymous/anonimpl/impl.go b/pkg/services/anonymous/anonimpl/impl.go index 2a6fba1642e..c060b948f64 100644 --- a/pkg/services/anonymous/anonimpl/impl.go +++ b/pkg/services/anonymous/anonimpl/impl.go @@ -20,12 +20,12 @@ import ( const thirtyDays = 30 * 24 * time.Hour const anonCachePrefix = "anon-session" -type AnonSession struct { +type Device struct { ip string userAgent string } -func (a *AnonSession) Key() (string, error) { +func (a *Device) Key() (string, error) { key := strings.Builder{} key.WriteString(a.ip) key.WriteString(a.userAgent) @@ -38,14 +38,14 @@ func (a *AnonSession) Key() (string, error) { return strings.Join([]string{anonCachePrefix, hex.EncodeToString(hash.Sum(nil))}, ":"), nil } -type AnonSessionService struct { +type AnonDeviceService struct { remoteCache remotecache.CacheStorage log log.Logger localCache *localcache.CacheService } -func ProvideAnonymousSessionService(remoteCache remotecache.CacheStorage, usageStats usagestats.Service) *AnonSessionService { - a := &AnonSessionService{ +func ProvideAnonymousDeviceService(remoteCache remotecache.CacheStorage, usageStats usagestats.Service) *AnonDeviceService { + a := &AnonDeviceService{ remoteCache: remoteCache, log: log.New("anonymous-session-service"), localCache: localcache.New(29*time.Minute, 15*time.Minute), @@ -56,7 +56,7 @@ func ProvideAnonymousSessionService(remoteCache remotecache.CacheStorage, usageS return a } -func (a *AnonSessionService) usageStatFn(ctx context.Context) (map[string]interface{}, error) { +func (a *AnonDeviceService) usageStatFn(ctx context.Context) (map[string]interface{}, error) { sessionCount, err := a.remoteCache.Count(ctx, anonCachePrefix) if err != nil { return nil, nil @@ -67,7 +67,7 @@ func (a *AnonSessionService) usageStatFn(ctx context.Context) (map[string]interf }, nil } -func (a *AnonSessionService) TagSession(ctx context.Context, httpReq *http.Request) error { +func (a *AnonDeviceService) TagDevice(ctx context.Context, httpReq *http.Request) error { addr := web.RemoteAddr(httpReq) ip, err := network.GetIPFromAddress(addr) if err != nil { @@ -80,12 +80,12 @@ func (a *AnonSessionService) TagSession(ctx context.Context, httpReq *http.Reque clientIPStr = "" } - anonSession := &AnonSession{ + anonDevice := &Device{ ip: clientIPStr, userAgent: httpReq.UserAgent(), } - key, err := anonSession.Key() + key, err := anonDevice.Key() if err != nil { return err } diff --git a/pkg/services/anonymous/anonimpl/impl_test.go b/pkg/services/anonymous/anonimpl/impl_test.go index d42c94733a3..db3d5d4fd29 100644 --- a/pkg/services/anonymous/anonimpl/impl_test.go +++ b/pkg/services/anonymous/anonimpl/impl_test.go @@ -12,15 +12,15 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" ) -func TestAnonSessionKey(t *testing.T) { +func TestAnonDeviceKey(t *testing.T) { testCases := []struct { name string - session *AnonSession + session *Device expected string }{ { name: "should hash correctly", - session: &AnonSession{ + session: &Device{ ip: "10.10.10.10", userAgent: "test", }, @@ -28,7 +28,7 @@ func TestAnonSessionKey(t *testing.T) { }, { name: "should hash correctly with different ip", - session: &AnonSession{ + session: &Device{ ip: "10.10.10.1", userAgent: "test", }, @@ -36,7 +36,7 @@ func TestAnonSessionKey(t *testing.T) { }, { name: "should hash correctly with different user agent", - session: &AnonSession{ + session: &Device{ ip: "10.10.10.1", userAgent: "test2", }, @@ -58,7 +58,7 @@ func TestAnonSessionKey(t *testing.T) { } } -func TestIntegrationAnonSessionService_tag(t *testing.T) { +func TestIntegrationAnonDeviceService_tag(t *testing.T) { testCases := []struct { name string req []*http.Request @@ -134,10 +134,10 @@ func TestIntegrationAnonSessionService_tag(t *testing.T) { t.Run(tc.name, func(t *testing.T) { fakeStore := remotecache.NewFakeStore(t) - anonService := ProvideAnonymousSessionService(fakeStore, &usagestats.UsageStatsMock{}) + anonService := ProvideAnonymousDeviceService(fakeStore, &usagestats.UsageStatsMock{}) for _, req := range tc.req { - err := anonService.TagSession(context.Background(), req) + err := anonService.TagDevice(context.Background(), req) require.NoError(t, err) } @@ -150,9 +150,9 @@ func TestIntegrationAnonSessionService_tag(t *testing.T) { } // Ensure that the local cache prevents request from being tagged -func TestIntegrationAnonSessionService_localCacheSafety(t *testing.T) { +func TestIntegrationAnonDeviceService_localCacheSafety(t *testing.T) { fakeStore := remotecache.NewFakeStore(t) - anonService := ProvideAnonymousSessionService(fakeStore, &usagestats.UsageStatsMock{}) + anonService := ProvideAnonymousDeviceService(fakeStore, &usagestats.UsageStatsMock{}) req := &http.Request{ Header: http.Header{ @@ -161,17 +161,17 @@ func TestIntegrationAnonSessionService_localCacheSafety(t *testing.T) { }, } - anonSession := &AnonSession{ + anonDevice := &Device{ ip: "10.30.30.2", userAgent: "test", } - key, err := anonSession.Key() + key, err := anonDevice.Key() require.NoError(t, err) anonService.localCache.SetDefault(key, true) - err = anonService.TagSession(context.Background(), req) + err = anonService.TagDevice(context.Background(), req) require.NoError(t, err) stats, err := anonService.usageStatFn(context.Background()) diff --git a/pkg/services/anonymous/anontest/fake.go b/pkg/services/anonymous/anontest/fake.go index d71050ab20f..147a5c3ed20 100644 --- a/pkg/services/anonymous/anontest/fake.go +++ b/pkg/services/anonymous/anontest/fake.go @@ -8,6 +8,6 @@ import ( type FakeAnonymousSessionService struct { } -func (f *FakeAnonymousSessionService) TagSession(ctx context.Context, httpReq *http.Request) error { +func (f *FakeAnonymousSessionService) TagDevice(ctx context.Context, httpReq *http.Request) error { return nil } diff --git a/pkg/services/anonymous/service.go b/pkg/services/anonymous/service.go index d84eea6a169..3d9fdf5969c 100644 --- a/pkg/services/anonymous/service.go +++ b/pkg/services/anonymous/service.go @@ -6,5 +6,5 @@ import ( ) type Service interface { - TagSession(context.Context, *http.Request) error + TagDevice(context.Context, *http.Request) error } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 32dae82d428..7ad7b36a559 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -57,7 +57,7 @@ func ProvideService( apikeyService apikey.Service, userService user.Service, jwtService auth.JWTVerifierService, usageStats usagestats.Service, - anonSessionService anonymous.Service, + anonDeviceService anonymous.Service, userProtectionService login.UserProtectionService, loginAttempts loginattempt.Service, quotaService quota.Service, authInfoService login.AuthInfoService, renderService rendering.Service, @@ -88,7 +88,7 @@ func ProvideService( } if s.cfg.AnonymousEnabled { - s.RegisterClient(clients.ProvideAnonymous(cfg, orgService, anonSessionService)) + s.RegisterClient(clients.ProvideAnonymous(cfg, orgService, anonDeviceService)) } var proxyClients []authn.ProxyClient diff --git a/pkg/services/authn/clients/anonymous.go b/pkg/services/authn/clients/anonymous.go index d42d4717fd4..81ffa9903df 100644 --- a/pkg/services/authn/clients/anonymous.go +++ b/pkg/services/authn/clients/anonymous.go @@ -14,20 +14,20 @@ import ( var _ authn.ContextAwareClient = new(Anonymous) -func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service, anonSessionService anonymous.Service) *Anonymous { +func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service, anonDeviceService anonymous.Service) *Anonymous { return &Anonymous{ - cfg: cfg, - log: log.New("authn.anonymous"), - orgService: orgService, - anonSessionService: anonSessionService, + cfg: cfg, + log: log.New("authn.anonymous"), + orgService: orgService, + anonDeviceService: anonDeviceService, } } type Anonymous struct { - cfg *setting.Cfg - log log.Logger - orgService org.Service - anonSessionService anonymous.Service + cfg *setting.Cfg + log log.Logger + orgService org.Service + anonDeviceService anonymous.Service } func (a *Anonymous) Name() string { @@ -54,7 +54,7 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn. a.log.Warn("tag anon session panic", "err", err) } }() - if err := a.anonSessionService.TagSession(context.Background(), httpReqCopy); err != nil { + if err := a.anonDeviceService.TagDevice(context.Background(), httpReqCopy); err != nil { a.log.Warn("failed to tag anonymous session", "error", err) } }() diff --git a/pkg/services/authn/clients/anonymous_test.go b/pkg/services/authn/clients/anonymous_test.go index 20cf7ea6a30..724dfbd2dc2 100644 --- a/pkg/services/authn/clients/anonymous_test.go +++ b/pkg/services/authn/clients/anonymous_test.go @@ -46,10 +46,10 @@ func TestAnonymous_Authenticate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { c := Anonymous{ - cfg: tt.cfg, - log: log.NewNopLogger(), - orgService: &orgtest.FakeOrgService{ExpectedOrg: tt.org, ExpectedError: tt.err}, - anonSessionService: &anontest.FakeAnonymousSessionService{}, + cfg: tt.cfg, + log: log.NewNopLogger(), + orgService: &orgtest.FakeOrgService{ExpectedOrg: tt.org, ExpectedError: tt.err}, + anonDeviceService: &anontest.FakeAnonymousSessionService{}, } identity, err := c.Authenticate(context.Background(), &authn.Request{}) diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 5b38ede7502..a0b664cf8bf 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -52,50 +52,50 @@ func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtSer tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service, apiKeyService apikey.Service, authenticator loginpkg.Authenticator, userService user.Service, orgService org.Service, oauthTokenService oauthtoken.OAuthTokenService, features *featuremgmt.FeatureManager, - authnService authn.Service, anonSessionService anonymous.Service, + authnService authn.Service, anonDeviceService anonymous.Service, ) *ContextHandler { return &ContextHandler{ - Cfg: cfg, - AuthTokenService: tokenService, - JWTAuthService: jwtService, - RemoteCache: remoteCache, - RenderService: renderService, - SQLStore: sqlStore, - tracer: tracer, - authProxy: authProxy, - authenticator: authenticator, - loginService: loginService, - apiKeyService: apiKeyService, - userService: userService, - orgService: orgService, - oauthTokenService: oauthTokenService, - features: features, - authnService: authnService, - anonSessionService: anonSessionService, - singleflight: new(singleflight.Group), + Cfg: cfg, + AuthTokenService: tokenService, + JWTAuthService: jwtService, + RemoteCache: remoteCache, + RenderService: renderService, + SQLStore: sqlStore, + tracer: tracer, + authProxy: authProxy, + authenticator: authenticator, + loginService: loginService, + apiKeyService: apiKeyService, + userService: userService, + orgService: orgService, + oauthTokenService: oauthTokenService, + features: features, + authnService: authnService, + anonDeviceService: anonDeviceService, + singleflight: new(singleflight.Group), } } // ContextHandler is a middleware. type ContextHandler struct { - Cfg *setting.Cfg - AuthTokenService auth.UserTokenService - JWTAuthService auth.JWTVerifierService - RemoteCache *remotecache.RemoteCache - RenderService rendering.Service - SQLStore db.DB - tracer tracing.Tracer - authProxy *authproxy.AuthProxy - authenticator loginpkg.Authenticator - loginService login.Service - apiKeyService apikey.Service - userService user.Service - orgService org.Service - oauthTokenService oauthtoken.OAuthTokenService - features *featuremgmt.FeatureManager - authnService authn.Service - singleflight *singleflight.Group - anonSessionService anonymous.Service + Cfg *setting.Cfg + AuthTokenService auth.UserTokenService + JWTAuthService auth.JWTVerifierService + RemoteCache *remotecache.RemoteCache + RenderService rendering.Service + SQLStore db.DB + tracer tracing.Tracer + authProxy *authproxy.AuthProxy + authenticator loginpkg.Authenticator + loginService login.Service + apiKeyService apikey.Service + userService user.Service + orgService org.Service + oauthTokenService oauthtoken.OAuthTokenService + features *featuremgmt.FeatureManager + authnService authn.Service + singleflight *singleflight.Group + anonDeviceService anonymous.Service // GetTime returns the current time. // Stubbable by tests. GetTime func() time.Time @@ -282,7 +282,7 @@ func (h *ContextHandler) initContextWithAnonymousUser(reqContext *contextmodel.R reqContext.Logger.Warn("tag anon session panic", "err", err) } }() - if err := h.anonSessionService.TagSession(context.Background(), httpReqCopy); err != nil { + if err := h.anonDeviceService.TagDevice(context.Background(), httpReqCopy); err != nil { reqContext.Logger.Warn("Failed to tag anonymous session", "error", err) } }() diff --git a/pkg/services/datasources/models.go b/pkg/services/datasources/models.go index b472b0f597d..101a789b13a 100644 --- a/pkg/services/datasources/models.go +++ b/pkg/services/datasources/models.go @@ -134,6 +134,7 @@ type UpdateDataSourceCommand struct { ReadOnly bool `json:"-"` EncryptedSecureJsonData map[string][]byte `json:"-"` UpdateSecretFn UpdateSecretFn `json:"-"` + IgnoreOldSecureJsonData bool `json:"-"` } // DeleteDataSourceCommand will delete a DataSource based on OrgID as well as the UID (preferred), ID, or Name. diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index cdfad67cd63..22eee0656cb 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -644,9 +644,11 @@ func (s *Service) fillWithSecureJSONData(ctx context.Context, cmd *datasources.U cmd.SecureJsonData = make(map[string]string) } - for k, v := range decrypted { - if _, ok := cmd.SecureJsonData[k]; !ok { - cmd.SecureJsonData[k] = v + if !cmd.IgnoreOldSecureJsonData { + for k, v := range decrypted { + if _, ok := cmd.SecureJsonData[k]; !ok { + cmd.SecureJsonData[k] = v + } } } diff --git a/pkg/services/datasources/service/datasource_test.go b/pkg/services/datasources/service/datasource_test.go index e830d2e04f3..a9049ad1275 100644 --- a/pkg/services/datasources/service/datasource_test.go +++ b/pkg/services/datasources/service/datasource_test.go @@ -187,6 +187,96 @@ func TestService_UpdateDataSource(t *testing.T) { _, err = dsService.UpdateDataSource(context.Background(), cmd) require.ErrorIs(t, err, datasources.ErrDataSourceNameExists) }) + + t.Run("should merge cmd.SecureJsonData with db data", func(t *testing.T) { + sqlStore := db.InitTestDB(t) + secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) + secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) + quotaService := quotatest.New(false, nil) + mockPermission := acmock.NewMockedPermissionsService() + dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService) + require.NoError(t, err) + + mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + + expectedDbKey := "db-secure-key" + expectedDbValue := "db-secure-value" + ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ + OrgID: 1, + Name: "test-datasource", + SecureJsonData: map[string]string{ + expectedDbKey: expectedDbValue, + }, + }) + require.NoError(t, err) + + expectedOgKey := "cmd-secure-key" + expectedOgValue := "cmd-secure-value" + + cmd := &datasources.UpdateDataSourceCommand{ + ID: ds.ID, + OrgID: ds.OrgID, + Name: "test-datasource-updated", + SecureJsonData: map[string]string{ + expectedOgKey: expectedOgValue, + }, + } + + ds, err = dsService.UpdateDataSource(context.Background(), cmd) + require.NoError(t, err) + + secret, err := dsService.DecryptedValues(context.Background(), ds) + require.NoError(t, err) + + assert.Equal(t, secret[expectedDbKey], expectedDbValue) + assert.Equal(t, secret[expectedOgKey], expectedOgValue) + }) + + t.Run("should preserve cmd.SecureJsonData when cmd.IgnoreOldSecureJsonData=true", func(t *testing.T) { + sqlStore := db.InitTestDB(t) + secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) + secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) + quotaService := quotatest.New(false, nil) + mockPermission := acmock.NewMockedPermissionsService() + dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService) + require.NoError(t, err) + + mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + + notExpectedDbKey := "db-secure-key" + dbValue := "db-secure-value" + ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ + OrgID: 1, + Name: "test-datasource", + SecureJsonData: map[string]string{ + notExpectedDbKey: dbValue, + }, + }) + require.NoError(t, err) + + expectedOgKey := "cmd-secure-key" + expectedOgValue := "cmd-secure-value" + + cmd := &datasources.UpdateDataSourceCommand{ + ID: ds.ID, + OrgID: ds.OrgID, + Name: "test-datasource-updated", + SecureJsonData: map[string]string{ + expectedOgKey: expectedOgValue, + }, + IgnoreOldSecureJsonData: true, + } + + ds, err = dsService.UpdateDataSource(context.Background(), cmd) + require.NoError(t, err) + + secret, err := dsService.DecryptedValues(context.Background(), ds) + require.NoError(t, err) + + assert.Equal(t, secret[expectedOgKey], expectedOgValue) + _, ok := secret[notExpectedDbKey] + assert.False(t, ok) + }) } func TestService_NameScopeResolver(t *testing.T) { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 45b7bd0dcd4..1c113caf336 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -166,9 +166,11 @@ var ( Owner: grafanaPluginsPlatformSquad, }, { + // Some plugins rely on topnav feature flag being enabled, so we cannot remove this until we + // can afford the breaking change, or we've detemined no one else is relying on it Name: "topnav", - Description: "Enables new top navigation and page layouts", - Stage: FeatureStageGeneralAvailability, + Description: "Enables topnav support in external plugins. The new Grafana navigation cannot be disabled.", + Stage: FeatureStageDeprecated, Expression: "true", // enabled by default Owner: grafanaFrontendPlatformSquad, }, @@ -348,7 +350,7 @@ var ( { Name: "influxdbBackendMigration", Description: "Query InfluxDB InfluxQL without the proxy", - Stage: FeatureStageExperimental, + Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaObservabilityMetricsSquad, }, @@ -611,8 +613,9 @@ var ( { Name: "transformationsRedesign", Description: "Enables the transformations redesign", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, + Expression: "true", // enabled by default Owner: grafanaObservabilityMetricsSquad, }, { @@ -630,8 +633,8 @@ var ( Owner: grafanaAlertingSquad, }, { - Name: "disableTraceQLStreaming", - Description: "Disables the option to stream the response of TraceQL queries of the Tempo data source", + Name: "traceQLStreaming", + Description: "Enables response streaming of TraceQL queries of the Tempo data source", Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityTracesAndProfilingSquad, @@ -672,5 +675,20 @@ var ( Owner: grafanaPartnerPluginsSquad, Expression: "true", // on by default }, + { + Name: "prometheusConfigOverhaulAuth", + Description: "Update the Prometheus configuration page with the new auth component", + Stage: FeatureStageExperimental, + Owner: grafanaObservabilityMetricsSquad, + }, + { + Name: "configurableSchedulerTick", + Description: "Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaAlertingSquad, + RequiresRestart: true, + HideFromDocs: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 2de9a3a14c6..6e380fb330b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -23,7 +23,7 @@ scenes,experimental,@grafana/dashboards-squad,false,false,false,true disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,false,true,false logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false,false dataConnectionsConsole,GA,@grafana/plugins-platform-backend,false,false,false,false -topnav,GA,@grafana/grafana-frontend-platform,false,false,false,false +topnav,deprecated,@grafana/grafana-frontend-platform,false,false,false,false grpcServer,preview,@grafana/grafana-app-platform-squad,false,false,false,false entityStore,experimental,@grafana/grafana-app-platform-squad,true,false,false,false cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false,false @@ -49,7 +49,7 @@ gcomOnlyExternalOrgRoleSync,GA,@grafana/grafana-authnz-team,false,false,false,fa prometheusMetricEncyclopedia,GA,@grafana/observability-metrics,false,false,false,true timeSeriesTable,experimental,@grafana/app-o11y,false,false,false,true prometheusResourceBrowserCache,GA,@grafana/observability-metrics,false,false,false,true -influxdbBackendMigration,experimental,@grafana/observability-metrics,false,false,false,true +influxdbBackendMigration,preview,@grafana/observability-metrics,false,false,false,true clientTokenRotation,experimental,@grafana/grafana-authnz-team,false,false,false,false prometheusDataplane,GA,@grafana/observability-metrics,false,false,false,false lokiMetricDataplane,GA,@grafana/observability-logs,false,false,false,false @@ -88,12 +88,14 @@ vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,false,true prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-metrics,false,false,false,true logsExploreTableVisualisation,experimental,@grafana/observability-logs,false,false,false,true awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false,false -transformationsRedesign,experimental,@grafana/observability-metrics,false,false,false,true +transformationsRedesign,GA,@grafana/observability-metrics,false,false,false,true toggleLabelsInLogsUI,experimental,@grafana/observability-logs,false,false,false,true mlExpressions,experimental,@grafana/alerting-squad,false,false,false,false -disableTraceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,false,true +traceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,false,true grafanaAPIServer,experimental,@grafana/grafana-app-platform-squad,false,false,false,false featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,false,true,false awsAsyncQueryCaching,experimental,@grafana/aws-datasources,false,false,false,false splitScopes,preview,@grafana/grafana-authnz-team,false,false,true,false azureMonitorDataplane,GA,@grafana/partner-datasources,false,false,false,false +prometheusConfigOverhaulAuth,experimental,@grafana/observability-metrics,false,false,false,false +configurableSchedulerTick,experimental,@grafana/alerting-squad,false,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 17f4e076494..d7137fdf903 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -104,7 +104,7 @@ const ( FlagDataConnectionsConsole = "dataConnectionsConsole" // FlagTopnav - // Enables new top navigation and page layouts + // Enables topnav support in external plugins. The new Grafana navigation cannot be disabled. FlagTopnav = "topnav" // FlagGrpcServer @@ -375,9 +375,9 @@ const ( // Enable support for Machine Learning in server-side expressions FlagMlExpressions = "mlExpressions" - // FlagDisableTraceQLStreaming - // Disables the option to stream the response of TraceQL queries of the Tempo data source - FlagDisableTraceQLStreaming = "disableTraceQLStreaming" + // FlagTraceQLStreaming + // Enables response streaming of TraceQL queries of the Tempo data source + FlagTraceQLStreaming = "traceQLStreaming" // FlagGrafanaAPIServer // Enable Kubernetes API Server for Grafana resources @@ -398,4 +398,12 @@ const ( // FlagAzureMonitorDataplane // Adds dataplane compliant frame metadata in the Azure Monitor datasource FlagAzureMonitorDataplane = "azureMonitorDataplane" + + // FlagPrometheusConfigOverhaulAuth + // Update the Prometheus configuration page with the new auth component + FlagPrometheusConfigOverhaulAuth = "prometheusConfigOverhaulAuth" + + // FlagConfigurableSchedulerTick + // Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval + FlagConfigurableSchedulerTick = "configurableSchedulerTick" ) diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 2e75aeb482c..5f7798195e6 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -30,8 +30,8 @@ func TestFeatureToggleFiles(t *testing.T) { t.Run("check registry constraints", func(t *testing.T) { for _, flag := range standardFeatureFlags { - if flag.Expression == "true" && flag.Stage != FeatureStageGeneralAvailability { - t.Errorf("only FeatureStageGeneralAvailability features can be enabled by default. See: %s", flag.Name) + if flag.Expression == "true" && !(flag.Stage == FeatureStageGeneralAvailability || flag.Stage == FeatureStageDeprecated) { + t.Errorf("only FeatureStageGeneralAvailability or FeatureStageDeprecated features can be enabled by default. See: %s", flag.Name) } if flag.RequiresDevMode && flag.Stage != FeatureStageExperimental { t.Errorf("only alpha features can require dev mode. See: %s", flag.Name) diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 95bbcb3cad4..00dc8248459 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -56,7 +56,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon cmd.FolderID = folder.ID } } - element, err := l.createLibraryElement(c.Req.Context(), c.SignedInUser, cmd) + element, err := l.CreateElement(c.Req.Context(), c.SignedInUser, cmd) if err != nil { return toLibraryElementError(err, "Failed to create library element") } diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 86ed8ae6e75..2d8bff6bea1 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -120,12 +120,22 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn return model.LibraryElementDTO{}, model.ErrLibraryElementUIDTooLong } } + + updatedModel := cmd.Model + var err error + if cmd.Kind == int64(model.PanelElement) { + updatedModel, err = l.addUidToLibraryPanel(cmd.Model, createUID) + if err != nil { + return model.LibraryElementDTO{}, err + } + } + element := model.LibraryElement{ OrgID: signedInUser.OrgID, FolderID: cmd.FolderID, UID: createUID, Name: cmd.Name, - Model: cmd.Model, + Model: updatedModel, Version: 1, Kind: cmd.Kind, @@ -140,7 +150,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn return model.LibraryElementDTO{}, err } - err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { + err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil { return err } @@ -228,7 +238,7 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn } // getLibraryElements gets a Library Element where param == value -func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles, cmd model.GetLibraryElementCommand) ([]model.LibraryElementDTO, error) { +func (l *LibraryElementService) getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles, cmd model.GetLibraryElementCommand) ([]model.LibraryElementDTO, error) { libraryElements := make([]model.LibraryElementWithMeta, 0) recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported() @@ -267,6 +277,14 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed leDtos := make([]model.LibraryElementDTO, len(libraryElements)) for i, libraryElement := range libraryElements { + var updatedModel json.RawMessage + if libraryElement.Kind == int64(model.PanelElement) { + updatedModel, err = l.addUidToLibraryPanel(libraryElement.Model, libraryElement.UID) + if err != nil { + return []model.LibraryElementDTO{}, err + } + } + leDtos[i] = model.LibraryElementDTO{ ID: libraryElement.ID, OrgID: libraryElement.OrgID, @@ -277,7 +295,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed Kind: libraryElement.Kind, Type: libraryElement.Type, Description: libraryElement.Description, - Model: libraryElement.Model, + Model: updatedModel, Version: libraryElement.Version, Meta: model.LibraryElementDTOMeta{ FolderName: libraryElement.FolderName, @@ -304,7 +322,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed // getLibraryElementByUid gets a Library Element by uid. func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { - libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: cmd.UID}}, l.features, cmd) + libraryElements, err := l.getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: cmd.UID}}, l.features, cmd) if err != nil { return model.LibraryElementDTO{}, err } @@ -317,7 +335,7 @@ func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signed // getLibraryElementByName gets a Library Element by name. func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser *user.SignedInUser, name string) ([]model.LibraryElementDTO, error) { - return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features, + return l.getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features, model.GetLibraryElementCommand{ FolderName: dashboards.RootFolderName, }) diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index 3a0c4aa1e2b..4abc2666547 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -2,6 +2,7 @@ package libraryelements import ( "context" + "encoding/json" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" @@ -75,3 +76,24 @@ func (l *LibraryElementService) DisconnectElementsFromDashboard(c context.Contex func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser *user.SignedInUser, folderUID string) error { return l.deleteLibraryElementsInFolderUID(c, signedInUser, folderUID) } + +func (l *LibraryElementService) addUidToLibraryPanel(model []byte, newUid string) (json.RawMessage, error) { + var modelMap map[string]interface{} + err := json.Unmarshal(model, &modelMap) + if err != nil { + return nil, err + } + + if libraryPanel, ok := modelMap["libraryPanel"].(map[string]interface{}); ok { + if uid, ok := libraryPanel["uid"]; ok && uid == "" { + libraryPanel["uid"] = newUid + } + } + + updatedModel, err := json.Marshal(modelMap) + if err != nil { + return nil, err + } + + return updatedModel, nil +} diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 8e93bdf3ced..3858c4e003d 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -4,7 +4,6 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/correlations" - "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" @@ -18,16 +17,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead) authConfigUIAvailable := s.license.FeatureEnabled("saml") - if hasAccess(datasources.ConfigurationPageAccess) { - configNodes = append(configNodes, &navtree.NavLink{ - Text: "Data sources", - Icon: "database", - SubTitle: "Add and configure data sources", - Id: "datasources", - Url: s.cfg.AppSubURL + "/datasources", - }) - } - // FIXME: while we don't have a permissions for listing plugins the legacy check has to stay as a default if pluginaccesscontrol.ReqCanAdminPlugins(s.cfg)(c) || hasAccess(pluginaccesscontrol.AdminAccessEvaluator) { configNodes = append(configNodes, &navtree.NavLink{ diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 71a0aee28cd..fd0e1ea6d91 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -324,8 +324,6 @@ func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.Q for _, next := range c.Data { datasourceUIDsForRefIDs[next.RefID] = next.DatasourceUID } - // datasourceExprUID is a special DatasourceUID for expressions - datasourceExprUID := strconv.FormatInt(expr.DatasourceID, 10) result := ExecutionResults{Results: make(map[string]data.Frames)} for refID, res := range execResp.Responses { @@ -347,7 +345,7 @@ func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.Q hasNoFrames := len(res.Frames) == 0 hasNoFields := len(res.Frames) == 1 && len(res.Frames[0].Fields) == 0 if hasNoFrames || hasNoFields { - if s, ok := datasourceUIDsForRefIDs[refID]; ok && s != datasourceExprUID { + if s, ok := datasourceUIDsForRefIDs[refID]; ok && expr.NodeTypeFromDatasourceUID(s) == expr.TypeDatasourceNode { // TODO perhaps extract datasource UID from ML expression too. result.NoData[refID] = s } } diff --git a/pkg/services/ngalert/eval/eval_test.go b/pkg/services/ngalert/eval/eval_test.go index 8db9eacded9..ae05282c6f5 100644 --- a/pkg/services/ngalert/eval/eval_test.go +++ b/pkg/services/ngalert/eval/eval_test.go @@ -562,11 +562,23 @@ func TestEvaluate(t *testing.T) { Data: []models.AlertQuery{{ RefID: "A", DatasourceUID: "test", + }, { + RefID: "B", + DatasourceUID: expr.DatasourceUID, + }, { + RefID: "C", + DatasourceUID: expr.OldDatasourceUID, + }, { + RefID: "D", + DatasourceUID: expr.MLDatasourceUID, }}, }, resp: backend.QueryDataResponse{ Responses: backend.Responses{ "A": {Frames: nil}, + "B": {Frames: []*data.Frame{{Fields: nil}}}, + "C": {Frames: nil}, + "D": {Frames: []*data.Frame{{Fields: nil}}}, }, }, expected: Results{{ diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index fef8ac9966b..0fe3835e7d6 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -130,6 +130,7 @@ func NewScheduler(cfg SchedulerCfg, stateManager *state.Manager) *schedule { } func (sch *schedule) Run(ctx context.Context) error { + sch.log.Info("Starting scheduler", "tickInterval", sch.baseInterval) t := ticker.New(sch.clock, sch.baseInterval, sch.metrics.Ticker) defer t.Stop() diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index b7c6697f59e..9d3b8daf4fe 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -282,6 +282,27 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu // Add the instance to the log context to help correlate log lines for a state logger = logger.New("instance", result.Instance) + // if the current state is Error but the result is different, then we need o clean up the extra labels + // that were added after the state key was calculated + // https://github.com/grafana/grafana/blob/1df4d332c982dc5e394201bb2ef35b442727ce63/pkg/services/ngalert/state/state.go#L298-L311 + // Usually, it happens in the case of classic conditions when the evalResult does not have labels. + // + // This is temporary change to make sure that the labels are not persistent in the state after it was in Error state + // TODO yuri. Remove it in https://github.com/grafana/grafana/pull/68142 + if currentState.State == eval.Error && result.State != eval.Error { + // This is possible because state was updated after the CacheID was calculated. + _, curOk := currentState.Labels["ref_id"] + _, resOk := result.Instance["ref_id"] + if curOk && !resOk { + delete(currentState.Labels, "ref_id") + } + _, curOk = currentState.Labels["datasource_uid"] + _, resOk = result.Instance["datasource_uid"] + if curOk && !resOk { + delete(currentState.Labels, "datasource_uid") + } + } + switch result.State { case eval.Normal: logger.Debug("Setting next state", "handler", "resultNormal") diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index b58894506f0..f01520255d4 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -2240,6 +2240,86 @@ func TestProcessEvalResults(t *testing.T) { }, }, }, + { + desc: "classic condition, execution Error as Error (alerting -> query error -> alerting)", + alertRule: &models.AlertRule{ + OrgID: 1, + Title: "test_title", + UID: "test_alert_rule_uid", + NamespaceUID: "test_namespace_uid", + Annotations: map[string]string{}, + Labels: map[string]string{"label": "test"}, + Data: []models.AlertQuery{ + { + RefID: "A", + DatasourceUID: "test-datasource-uid", + }, + }, + IntervalSeconds: 10, + ExecErrState: models.ErrorErrState, + }, + expectedAnnotations: 2, + evalResults: []eval.Results{ + { + eval.Result{ + Instance: data.Labels{}, + State: eval.Alerting, + EvaluatedAt: evaluationTime, + EvaluationDuration: evaluationDuration, + }, + eval.Result{ + Instance: data.Labels{}, + State: eval.Error, + Error: expr.MakeQueryError("A", "test-datasource-uid", errors.New("this is an error")), + EvaluatedAt: evaluationTime.Add(10 * time.Second), + EvaluationDuration: evaluationDuration, + }, + eval.Result{ + Instance: data.Labels{}, + State: eval.Alerting, + EvaluatedAt: evaluationTime.Add(20 * time.Second), + EvaluationDuration: evaluationDuration, + }, + }, + }, + expectedStates: map[string]*state.State{ + `[["__alert_rule_namespace_uid__","test_namespace_uid"],["__alert_rule_uid__","test_alert_rule_uid"],["alertname","test_title"],["label","test"]]`: { + AlertRuleUID: "test_alert_rule_uid", + OrgID: 1, + CacheID: `[["__alert_rule_namespace_uid__","test_namespace_uid"],["__alert_rule_uid__","test_alert_rule_uid"],["alertname","test_title"],["label","test"]]`, + Labels: data.Labels{ + "__alert_rule_namespace_uid__": "test_namespace_uid", + "__alert_rule_uid__": "test_alert_rule_uid", + "alertname": "test_title", + "label": "test", + }, + Values: make(map[string]float64), + State: eval.Alerting, + Results: []state.Evaluation{ + { + EvaluationTime: evaluationTime, + EvaluationState: eval.Alerting, + Values: make(map[string]*float64), + }, + { + EvaluationTime: evaluationTime.Add(10 * time.Second), + EvaluationState: eval.Error, + Values: make(map[string]*float64), + }, + { + EvaluationTime: evaluationTime.Add(20 * time.Second), + EvaluationState: eval.Alerting, + Values: make(map[string]*float64), + }, + }, + StartsAt: evaluationTime.Add(20 * time.Second), + EndsAt: evaluationTime.Add(110 * time.Second), + LastEvaluationTime: evaluationTime.Add(20 * time.Second), + EvaluationDuration: evaluationDuration, + Annotations: map[string]string{}, + }, + }, + }, } for _, tc := range testCases { diff --git a/pkg/services/pluginsintegration/pipeline/discovery.go b/pkg/services/pluginsintegration/pipeline/discovery.go new file mode 100644 index 00000000000..c4e29033a7b --- /dev/null +++ b/pkg/services/pluginsintegration/pipeline/discovery.go @@ -0,0 +1,33 @@ +package pipeline + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/registry" +) + +func ProvideDiscoveryStage(cfg *config.Cfg, pluginFinder finder.Finder, pluginRegistry registry.Service) *discovery.Discovery { + return discovery.New(cfg, discovery.Opts{ + FindFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { + return pluginFinder.Find(ctx, src) + }, + FindFilterFuncs: []discovery.FindFilterFunc{ + func(ctx context.Context, _ plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + return discovery.NewDuplicatePluginFilterStep(pluginRegistry).Filter(ctx, bundles) + }, + }, + }) +} + +func ProvideBootstrapStage(cfg *config.Cfg, signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *bootstrap.Bootstrap { + return bootstrap.New(cfg, bootstrap.Opts{ + ConstructFunc: bootstrap.DefaultConstructFunc(signatureCalculator, assetPath), + DecorateFuncs: bootstrap.DefaultDecorateFuncs, + }) +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 554b171338a..9630b8290b6 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -2,6 +2,7 @@ package pluginsintegration import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" @@ -14,6 +15,8 @@ import ( pAngularInspector "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -34,6 +37,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" @@ -57,6 +61,11 @@ var WireSet = wire.NewSet( pluginscdn.ProvideService, assetpath.ProvideService, + pipeline.ProvideDiscoveryStage, + wire.Bind(new(discovery.Discoverer), new(*discovery.Discovery)), + pipeline.ProvideBootstrapStage, + wire.Bind(new(bootstrap.Bootstrapper), new(*bootstrap.Bootstrap)), + angularpatternsstore.ProvideService, angulardetectorsprovider.ProvideDynamic, angularinspector.ProvideService, diff --git a/pkg/services/provisioning/datasources/types.go b/pkg/services/provisioning/datasources/types.go index 69c7b0d7259..0ccaf68fe5c 100644 --- a/pkg/services/provisioning/datasources/types.go +++ b/pkg/services/provisioning/datasources/types.go @@ -242,21 +242,22 @@ func createUpdateCommand(ds *upsertDataSourceFromConfig, id int64) *datasources. } return &datasources.UpdateDataSourceCommand{ - ID: id, - UID: ds.UID, - OrgID: ds.OrgID, - Name: ds.Name, - Type: ds.Type, - Access: datasources.DsAccess(ds.Access), - URL: ds.URL, - User: ds.User, - Database: ds.Database, - BasicAuth: ds.BasicAuth, - BasicAuthUser: ds.BasicAuthUser, - WithCredentials: ds.WithCredentials, - IsDefault: ds.IsDefault, - JsonData: jsonData, - SecureJsonData: ds.SecureJSONData, - ReadOnly: !ds.Editable, + ID: id, + UID: ds.UID, + OrgID: ds.OrgID, + Name: ds.Name, + Type: ds.Type, + Access: datasources.DsAccess(ds.Access), + URL: ds.URL, + User: ds.User, + Database: ds.Database, + BasicAuth: ds.BasicAuth, + BasicAuthUser: ds.BasicAuthUser, + WithCredentials: ds.WithCredentials, + IsDefault: ds.IsDefault, + JsonData: jsonData, + SecureJsonData: ds.SecureJSONData, + ReadOnly: !ds.Editable, + IgnoreOldSecureJsonData: true, } } diff --git a/pkg/services/sqlstore/migrations/ualert/dash_alert.go b/pkg/services/sqlstore/migrations/ualert/dash_alert.go index f6276507e33..707aea94cee 100644 --- a/pkg/services/sqlstore/migrations/ualert/dash_alert.go +++ b/pkg/services/sqlstore/migrations/ualert/dash_alert.go @@ -55,7 +55,8 @@ func (m *migration) slurpDashAlerts() ([]dashAlert, error) { for i := range dashAlerts { err = json.Unmarshal(dashAlerts[i].Settings, &dashAlerts[i].ParsedSettings) if err != nil { - return nil, err + da := dashAlerts[i] + return nil, fmt.Errorf("failed to parse alert rule ID:%d, name:'%s', orgID:%d: %w", da.Id, da.Name, da.OrgId, err) } } diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index c594a76315a..65573f44856 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -287,6 +287,27 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { uaCfg.BaseInterval = SchedulerBaseInterval + // The base interval of the scheduler for evaluating alerts. + // 1. It is used by the internal scheduler's timer to tick at this interval. + // 2. to spread evaluations of rules that need to be evaluated at the current tick T. In other words, the evaluation of rules at the tick T will be evenly spread in the interval from T to T+scheduler_tick_interval. + // For example, if there are 100 rules that need to be evaluated at tick T, and the base interval is 10s, rules will be evaluated every 100ms. + // 3. It increases delay between rule updates and state reset. + // NOTE: + // 1. All alert rule intervals should be times of this interval. Otherwise, the rules will not be evaluated. It is not recommended to set it lower than 10s or odd numbers. Recommended: 10s, 30s, 1m + // 2. The increasing of the interval will affect how slow alert rule updates will reset the state, and therefore reset notification. Higher the interval - slower propagation of the changes. + baseInterval, err := gtime.ParseDuration(valueAsString(ua, "scheduler_tick_interval", SchedulerBaseInterval.String())) + if cfg.IsFeatureToggleEnabled("configurableSchedulerTick") { // use literal to avoid cycle imports + if err != nil { + return fmt.Errorf("failed to parse setting 'scheduler_tick_interval' as duration: %w", err) + } + if baseInterval != SchedulerBaseInterval { + cfg.Logger.Warn("Scheduler tick interval is changed to non-default", "interval", baseInterval, "default", SchedulerBaseInterval) + } + uaCfg.BaseInterval = baseInterval + } else if baseInterval != SchedulerBaseInterval { + cfg.Logger.Warn("Scheduler tick interval is changed to non-default but the feature flag is not enabled. Using default.", "interval", baseInterval, "default", SchedulerBaseInterval) + } + uaMinInterval, err := gtime.ParseDuration(valueAsString(ua, "min_interval", uaCfg.BaseInterval.String())) if err != nil || uaMinInterval == uaCfg.BaseInterval { // unified option is invalid duration or equals the default // if the legacy option is invalid, fallback to 10 (unified alerting min interval default) diff --git a/pkg/setting/setting_unified_alerting_test.go b/pkg/setting/setting_unified_alerting_test.go index 92fa07648ad..af9b73678d6 100644 --- a/pkg/setting/setting_unified_alerting_test.go +++ b/pkg/setting/setting_unified_alerting_test.go @@ -39,6 +39,39 @@ func TestCfg_ReadUnifiedAlertingSettings(t *testing.T) { require.Len(t, cfg.UnifiedAlerting.HAPeers, 3) require.ElementsMatch(t, []string{"hostname1:9090", "hostname2:9090", "hostname3:9090"}, cfg.UnifiedAlerting.HAPeers) } + + t.Run("should read 'scheduler_tick_interval'", func(t *testing.T) { + tmp := cfg.IsFeatureToggleEnabled + t.Cleanup(func() { + cfg.IsFeatureToggleEnabled = tmp + }) + cfg.IsFeatureToggleEnabled = func(key string) bool { return key == "configurableSchedulerTick" } + + s, err := cfg.Raw.NewSection("unified_alerting") + require.NoError(t, err) + _, err = s.NewKey("scheduler_tick_interval", "1m") + require.NoError(t, err) + _, err = s.NewKey("min_interval", "3m") + require.NoError(t, err) + + require.NoError(t, cfg.ReadUnifiedAlertingSettings(cfg.Raw)) + require.Equal(t, time.Minute, cfg.UnifiedAlerting.BaseInterval) + require.Equal(t, 3*time.Minute, cfg.UnifiedAlerting.MinInterval) + + t.Run("and fail if it is wrong", func(t *testing.T) { + _, err = s.NewKey("scheduler_tick_interval", "test") + require.NoError(t, err) + + require.Error(t, cfg.ReadUnifiedAlertingSettings(cfg.Raw)) + }) + + t.Run("and use default if not specified", func(t *testing.T) { + s.DeleteKey("scheduler_tick_interval") + require.NoError(t, cfg.ReadUnifiedAlertingSettings(cfg.Raw)) + + require.Equal(t, SchedulerBaseInterval, cfg.UnifiedAlerting.BaseInterval) + }) + }) } func TestUnifiedAlertingSettings(t *testing.T) { diff --git a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go index 771f71d6e09..9a1e4d8c9cc 100644 --- a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go @@ -121,9 +121,6 @@ type TempoQuery struct { // @deprecated Query traces by span name SpanName *string `json:"spanName,omitempty"` - - // Use the streaming API to get partial results as they are available - Streaming *bool `json:"streaming,omitempty"` } // TempoQueryType search = Loki search, nativeSearch = Tempo search for backwards compatibility diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json index 3c7a04c738c..533437505ce 100644 --- a/plugins-bundled/internal/input-datasource/package.json +++ b/plugins-bundled/internal/input-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@grafana-plugins/input-datasource", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Input Datasource", "private": true, "repository": { @@ -28,8 +28,8 @@ "webpack": "5.76.0" }, "dependencies": { - "@grafana/data": "10.1.0-pre", - "@grafana/ui": "10.1.0-pre", + "@grafana/data": "10.2.0-pre", + "@grafana/ui": "10.2.0-pre", "react": "18.2.0", "tslib": "2.5.0" } diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 78e11c61ed5..baff647e306 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -31,6 +31,9 @@ export interface NestedFolderPickerProps { /* Folder UID to show as selected */ value?: string; + /** Show an invalid state around the folder picker */ + invalid?: boolean; + /* Whether to show the root 'Dashboards' (formally General) folder as selectable */ showRootFolder?: boolean; @@ -43,7 +46,13 @@ export interface NestedFolderPickerProps { const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const]; -export function NestedFolderPicker({ value, showRootFolder = true, excludeUIDs, onChange }: NestedFolderPickerProps) { +export function NestedFolderPicker({ + value, + invalid, + showRootFolder = true, + excludeUIDs, + onChange, +}: NestedFolderPickerProps) { const styles = useStyles2(getStyles); const dispatch = useDispatch(); const selectedFolder = useGetFolderQuery(value || skipToken); @@ -212,6 +221,7 @@ export function NestedFolderPicker({ value, showRootFolder = true, excludeUIDs, return ( : null} placeholder={label ?? t('browse-dashboards.folder-picker.search-placeholder', 'Search folders')} value={search} + invalid={invalid} className={styles.search} onKeyDown={handleKeyDown} onChange={(e) => setSearch(e.currentTarget.value)} diff --git a/public/app/core/components/NestedFolderPicker/Trigger.tsx b/public/app/core/components/NestedFolderPicker/Trigger.tsx index 5814f7e307d..fb3a8581ca2 100644 --- a/public/app/core/components/NestedFolderPicker/Trigger.tsx +++ b/public/app/core/components/NestedFolderPicker/Trigger.tsx @@ -3,18 +3,20 @@ import React, { forwardRef, ReactNode, ButtonHTMLAttributes } from 'react'; import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, Icon, getInputStyles } from '@grafana/ui'; +import { Icon, getInputStyles, useTheme2 } from '@grafana/ui'; import { focusCss } from '@grafana/ui/src/themes/mixins'; import { Text } from '@grafana/ui/src/unstable'; import { Trans } from 'app/core/internationalization'; interface TriggerProps extends ButtonHTMLAttributes { isLoading: boolean; + invalid?: boolean; label?: ReactNode; } -function Trigger({ isLoading, label, ...rest }: TriggerProps, ref: React.ForwardedRef) { - const styles = useStyles2(getStyles); +function Trigger({ isLoading, invalid, label, ...rest }: TriggerProps, ref: React.ForwardedRef) { + const theme = useTheme2(); + const styles = getStyles(theme, invalid); return (
@@ -52,8 +54,8 @@ function Trigger({ isLoading, label, ...rest }: TriggerProps, ref: React.Forward export default forwardRef(Trigger); -const getStyles = (theme: GrafanaTheme2) => { - const baseStyles = getInputStyles({ theme }); +const getStyles = (theme: GrafanaTheme2, invalid = false) => { + const baseStyles = getInputStyles({ theme, invalid }); return { wrapper: baseStyles.wrapper, diff --git a/public/app/core/components/TraceToLogs/TagMappingInput.tsx b/public/app/core/components/TraceToLogs/TagMappingInput.tsx index 8757260a45a..3b99a5b26c2 100644 --- a/public/app/core/components/TraceToLogs/TagMappingInput.tsx +++ b/public/app/core/components/TraceToLogs/TagMappingInput.tsx @@ -4,9 +4,11 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { SegmentInput, useStyles2, InlineLabel, Icon } from '@grafana/ui'; +import { TraceToLogsTag } from './TraceToLogsSettings'; + interface Props { - values: Array<{ key: string; value?: string }>; - onChange: (values: Array<{ key: string; value?: string }>) => void; + values: TraceToLogsTag[]; + onChange: (values: TraceToLogsTag[]) => void; id?: string; } diff --git a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx index 56e68a6002b..e5c01230233 100644 --- a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx +++ b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx @@ -3,19 +3,24 @@ import React, { useCallback, useMemo } from 'react'; import { DataSourceJsonData, DataSourceInstanceSettings, DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { ConfigSection } from '@grafana/experimental'; -import { DataSourcePicker } from '@grafana/runtime'; import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui'; import { ConfigDescriptionLink } from 'app/core/components/ConfigDescriptionLink'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { IntervalInput } from '../IntervalInput/IntervalInput'; import { TagMappingInput } from './TagMappingInput'; +export interface TraceToLogsTag { + key: string; + value?: string; +} + // @deprecated use getTraceToLogsOptions to get the v2 version of this config from jsonData export interface TraceToLogsOptions { datasourceUid?: string; tags?: string[]; - mappedTags?: Array<{ key: string; value?: string }>; + mappedTags?: TraceToLogsTag[]; mapTagNamesEnabled?: boolean; spanStartTimeShift?: string; spanEndTimeShift?: string; @@ -26,7 +31,7 @@ export interface TraceToLogsOptions { export interface TraceToLogsOptionsV2 { datasourceUid?: string; - tags?: Array<{ key: string; value?: string }>; + tags?: TraceToLogsTag[]; spanStartTimeShift?: string; spanEndTimeShift?: string; filterByTraceID?: boolean; @@ -151,7 +156,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) { diff --git a/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx index 9ba33fd7dc6..a9595c984d0 100644 --- a/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx +++ b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx @@ -2,14 +2,15 @@ import { css } from '@emotion/css'; import React from 'react'; import { + DataSourceInstanceSettings, DataSourceJsonData, DataSourcePluginOptionsEditorProps, GrafanaTheme2, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; import { ConfigSection } from '@grafana/experimental'; -import { DataSourcePicker } from '@grafana/runtime'; import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { ConfigDescriptionLink } from '../ConfigDescriptionLink'; import { IntervalInput } from '../IntervalInput/IntervalInput'; @@ -52,7 +53,7 @@ export function TraceToMetricsSettings({ options, onOptionsChange }: Props) { current={options.jsonData.tracesToMetrics?.datasourceUid} noDefault={true} width={40} - onChange={(ds) => + onChange={(ds: DataSourceInstanceSettings) => updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', { ...options.jsonData.tracesToMetrics, datasourceUid: ds.uid, diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts index 67040babaf8..238f28d0ff3 100644 --- a/public/app/features/alerting/unified/api/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -1,6 +1,7 @@ import { isEmpty } from 'lodash'; import { dispatch } from 'app/store/store'; +import { ReceiversStateDTO } from 'app/types/alerting'; import { AlertmanagerAlert, @@ -226,5 +227,30 @@ export const alertmanagerApi = alertingApi.injectEndpoints({ }), invalidatesTags: ['AlertmanagerConfiguration'], }), + + // Grafana Managed Alertmanager only + getContactPointsStatus: build.query({ + query: () => ({ + url: `/api/alertmanager/${getDatasourceAPIUid(GRAFANA_RULES_SOURCE_NAME)}/config/api/v1/receivers`, + }), + // this transformer basically fixes the weird "0001-01-01T00:00:00.000Z" and "0001-01-01T00:00:00.00Z" timestamps + // and sets both last attempt and duration to an empty string to indicate there hasn't been an attempt yet + transformResponse: (response: ReceiversStateDTO[]) => { + const isLastNotifyNullDate = (lastNotify: string) => lastNotify.startsWith('0001-01-01'); + + return response.map((receiversState) => ({ + ...receiversState, + integrations: receiversState.integrations.map((integration) => { + const noAttempt = isLastNotifyNullDate(integration.lastNotifyAttempt); + + return { + ...integration, + lastNotifyAttempt: noAttempt ? '' : integration.lastNotifyAttempt, + lastNotifyAttemptDuration: noAttempt ? '' : integration.lastNotifyAttemptDuration, + }; + }), + })); + }, + }), }), }); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx new file mode 100644 index 00000000000..62eaecef77b --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx @@ -0,0 +1,90 @@ +import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { noop } from 'lodash'; +import React from 'react'; +import { TestProvider } from 'test/helpers/TestProvider'; + +import { selectors } from '@grafana/e2e-selectors'; + +import { disableRBAC } from '../../mocks'; +import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; + +import ContactPoints, { ContactPoint } from './ContactPoints.v2'; + +import './__mocks__/server'; + +/** + * There are lots of ways in which we test our pages and components. Here's my opinionated approach to testing them. + * + * Use MSW to mock API responses, you can copy the JSON results from the network panel and use them in a __mocks__ folder. + * + * 1. Make sure we have "presentation" components we can test without mocking data, + * test these if they have some logic in them (hiding / showing things) and sad paths. + * + * 2. For testing the "container" components, check if data fetching is working as intended (you can use loading state) + * and check if we're not in an error state (although you can test for that too for sad path). + * + * 3. Write tests for the hooks we call in the "container" components + * if those have any logic or data structure transformations in them. + */ +describe('ContactPoints', () => { + beforeAll(() => { + disableRBAC(); + }); + + it('should show / hide loading states', async () => { + render( + + + , + { wrapper: TestProvider } + ); + + await waitFor(async () => { + await expect(screen.getByText('Loading...')).toBeInTheDocument(); + await waitForElementToBeRemoved(screen.getByText('Loading...')); + await expect(screen.queryByTestId(selectors.components.Alert.alertV2('error'))).not.toBeInTheDocument(); + }); + + expect(screen.getByText('grafana-default-email')).toBeInTheDocument(); + expect(screen.getAllByTestId('contact-point')).toHaveLength(4); + }); +}); + +describe('ContactPoint', () => { + it('should call delete when clicked and not disabled', async () => { + const onDelete = jest.fn(); + + render(); + + const moreActions = screen.getByTestId('more-actions'); + await userEvent.click(moreActions); + + const deleteButton = screen.getByRole('menuitem', { name: /delete/i }); + await userEvent.click(deleteButton); + + expect(onDelete).toHaveBeenCalledWith('my-contact-point'); + }); + + it('should disabled buttons', async () => { + render(); + + const moreActions = screen.getByTestId('more-actions'); + const editAction = screen.getByTestId('edit-action'); + + expect(moreActions).toHaveProperty('disabled', true); + expect(editAction).toHaveProperty('disabled', true); + }); + + it('should disabled buttons when provisioned', async () => { + render(); + + expect(screen.getByText(/provisioned/i)).toBeInTheDocument(); + + const moreActions = screen.getByTestId('more-actions'); + const editAction = screen.getByTestId('edit-action'); + + expect(moreActions).toHaveProperty('disabled', true); + expect(editAction).toHaveProperty('disabled', true); + }); +}); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx index 4c10828a51a..feee11ab5dd 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx @@ -1,104 +1,129 @@ import { css } from '@emotion/css'; -import React from 'react'; +import { SerializedError } from '@reduxjs/toolkit'; +import { uniqueId, upperFirst } from 'lodash'; +import React, { ReactNode } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { dateTime, GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { Button, Dropdown, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui'; +import { Alert, Button, Dropdown, Icon, LoadingPlaceholder, Menu, Tooltip, useStyles2 } from '@grafana/ui'; import { Text } from '@grafana/ui/src/unstable'; import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap'; -import { GrafanaNotifierType } from 'app/types/alerting'; +import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts'; +import { GrafanaNotifierType, NotifierStatus } from 'app/types/alerting'; +import { useAlertmanager } from '../../state/AlertmanagerContext'; import { INTEGRATION_ICONS } from '../../types/contact-points'; import { MetaText } from '../MetaText'; import { ProvisioningBadge } from '../Provisioning'; import { Spacer } from '../Spacer'; import { Strong } from '../Strong'; +import { useDeleteContactPointModal } from './Modals'; +import { RECEIVER_STATUS_KEY, useContactPointsWithStatus, useDeleteContactPoint } from './useContactPoints'; +import { getReceiverDescription, isProvisioned, ReceiverConfigWithStatus } from './utils'; + const ContactPoints = () => { + const { selectedAlertmanager } = useAlertmanager(); + const { isLoading, error, contactPoints } = useContactPointsWithStatus(selectedAlertmanager!); + const { deleteTrigger, updateAlertmanagerState } = useDeleteContactPoint(selectedAlertmanager!); + + const [DeleteModal, showDeleteModal] = useDeleteContactPointModal(deleteTrigger, updateAlertmanagerState.isLoading); + + if (error) { + // TODO fix this type casting, when error comes from "getContactPointsStatus" it probably won't be a SerializedError + return {(error as SerializedError).message}; + } + + if (isLoading) { + return ; + } + + return ( + <> + + {contactPoints.map((contactPoint) => { + const contactPointKey = selectedAlertmanager + contactPoint.name; + const provisioned = isProvisioned(contactPoint); + const disabled = updateAlertmanagerState.isLoading; + + return ( + + ); + })} + + {DeleteModal} + + ); +}; + +interface ContactPointProps { + name: string; + disabled?: boolean; + provisioned?: boolean; + receivers: ReceiverConfigWithStatus[]; + onDelete: (name: string) => void; +} + +export const ContactPoint = ({ + name, + disabled = false, + provisioned = false, + receivers, + onDelete, +}: ContactPointProps) => { const styles = useStyles2(getStyles); return ( - -
- - -
- -
-
-
+
+ + +
+ {receivers?.map((receiver) => { + const diagnostics = receiver[RECEIVER_STATUS_KEY]; + const sendingResolved = !Boolean(receiver.disableResolveMessage); -
- - -
- - - - -
-
-
- -
- - -
- -
-
-
- -
- - -
- -
-
-
- -
- - -
- - - - -
-
-
- -
- - -
- - - -
-
-
- + return ( + + ); + })} +
+
+
); }; interface ContactPointHeaderProps { name: string; - provenance?: string; + disabled?: boolean; + provisioned?: boolean; policies?: string[]; // some array of policies that refer to this contact point + onDelete: (name: string) => void; } const ContactPointHeader = (props: ContactPointHeaderProps) => { - const { name, provenance, policies = [] } = props; - + const { name, disabled = false, provisioned = false, policies = [], onDelete } = props; const styles = useStyles2(getStyles); - const isProvisioned = Boolean(provenance); + + const disableActions = disabled || provisioned; return (
@@ -112,12 +137,12 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { is used by {policies.length} notification policies ) : ( - is not used + is not used in any policy )} - {isProvisioned && } + {provisioned && } ( {children} @@ -129,7 +154,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { size="sm" icon="edit" type="button" - disabled={isProvisioned} + disabled={disableActions} aria-label="edit-action" data-testid="edit-action" > @@ -141,7 +166,13 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { - + onDelete(name)} + /> } > @@ -152,6 +183,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { type="button" aria-label="more-actions" data-testid="more-actions" + disabled={disableActions} /> @@ -161,16 +193,19 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { interface ContactPointReceiverProps { type: GrafanaNotifierType | string; - description?: string; - error?: string; + description?: ReactNode; sendingResolved?: boolean; + diagnostics?: NotifierStatus; } const ContactPointReceiver = (props: ContactPointReceiverProps) => { - const { type, description, error, sendingResolved = true } = props; + const { type, description, diagnostics, sendingResolved = true } = props; const styles = useStyles2(getStyles); const iconName = INTEGRATION_ICONS[type]; + const hasMetadata = diagnostics !== undefined; + // TODO get the actual name of the type from /ngalert if grafanaManaged AM + const receiverName = receiverTypeNames[type] ?? upperFirst(type); return (
@@ -180,7 +215,7 @@ const ContactPointReceiver = (props: ContactPointReceiverProps) => { {iconName && } - {type} + {receiverName} {description && ( @@ -190,43 +225,71 @@ const ContactPointReceiver = (props: ContactPointReceiverProps) => { )}
-
- - {error ? ( - <> - {/* TODO we might need an error variant for MetaText, dito for success */} - {/* TODO show error details on hover or elsewhere */} - - - - - Last delivery attempt failed - - - - - - ) : ( + {hasMetadata && } + +
+ ); +}; + +interface ContactPointReceiverMetadata { + sendingResolved: boolean; + diagnostics: NotifierStatus; +} + +const ContactPointReceiverMetadataRow = (props: ContactPointReceiverMetadata) => { + const { diagnostics, sendingResolved } = props; + const styles = useStyles2(getStyles); + + const failedToSend = Boolean(diagnostics.lastNotifyAttemptError); + const lastDeliveryAttempt = dateTime(diagnostics.lastNotifyAttempt); + const lastDeliveryAttemptDuration = diagnostics.lastNotifyAttemptDuration; + const hasDeliveryAttempt = lastDeliveryAttempt.isValid(); + + return ( +
+ + {/* this is shown when the last delivery failed – we don't show any additional metadata */} + {failedToSend ? ( + <> + {/* TODO we might need an error variant for MetaText, dito for success */} + + + + + Last delivery attempt failed + + + + + + ) : ( + <> + {/* this is shown when we have a last delivery attempt */} + {hasDeliveryAttempt && ( <> - Last delivery attempt 25 minutes ago + Last delivery attempt{' '} + + + {lastDeliveryAttempt.locale('en').fromNow()} + + - took 2s + took {lastDeliveryAttemptDuration} )} + {/* when we have no last delivery attempt */} + {!hasDeliveryAttempt && No delivery attempts} + {/* this is only shown for contact points that only want "firing" updates */} {!sendingResolved && ( Delivering only firing notifications )} - -
+ + )}
); diff --git a/public/app/features/alerting/unified/components/contact-points/Modals.tsx b/public/app/features/alerting/unified/components/contact-points/Modals.tsx new file mode 100644 index 00000000000..6c4fe8ec7df --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/Modals.tsx @@ -0,0 +1,89 @@ +import React, { useCallback, useMemo, useState } from 'react'; + +import { Button, Modal, ModalProps } from '@grafana/ui'; + +type ModalHook = [JSX.Element, (item: T) => void, () => void]; + +/** + * This hook controls the delete modal for contact points, showing loading and error states when appropriate + */ +export const useDeleteContactPointModal = ( + handleDelete: (name: string) => Promise, + isLoading: boolean +): ModalHook => { + const [showModal, setShowModal] = useState(false); + const [contactPoint, setContactPoint] = useState(); + const [error, setError] = useState(); + + const handleDismiss = useCallback(() => { + if (isLoading) { + return; + } + + setContactPoint(undefined); + setShowModal(false); + setError(undefined); + }, [isLoading]); + + const handleShow = useCallback((name: string) => { + setContactPoint(name); + setShowModal(true); + setError(undefined); + }, []); + + const handleSubmit = useCallback(() => { + if (contactPoint) { + handleDelete(contactPoint) + .then(() => setShowModal(false)) + .catch(setError); + } + }, [handleDelete, contactPoint]); + + const modalElement = useMemo(() => { + if (error) { + return ; + } + + return ( + +

Deleting this contact point will permanently remove it.

+

Are you sure you want to delete this contact point?

+ + + + + +
+ ); + }, [error, handleDismiss, handleSubmit, isLoading, showModal]); + + return [modalElement, handleShow, handleDismiss]; +}; + +interface ErrorModalProps extends Pick { + error: unknown; +} +const ErrorModal = ({ isOpen, onDismiss, error }: ErrorModalProps) => ( + +

Failed to update your configuration:

+

+ {String(error)} +

+
+); diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json new file mode 100644 index 00000000000..b5d9f3f2c82 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json @@ -0,0 +1,71 @@ +{ + "template_files": {}, + "alertmanager_config": { + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "xeKQrBrnk", + "name": "grafana-default-email", + "type": "email", + "disableResolveMessage": false, + "settings": { "addresses": "gilles.demey@grafana.com", "singleEmail": false }, + "secureFields": {} + } + ] + }, + { + "name": "provisioned-contact-point", + "grafana_managed_receiver_configs": [ + { + "uid": "s8SdCVjnk", + "name": "provisioned-contact-point", + "type": "email", + "disableResolveMessage": false, + "settings": { "addresses": "gilles.demey@grafana.com", "singleEmail": false }, + "secureFields": {}, + "provenance": "api" + } + ] + }, + { + "name": "lotsa-emails", + "grafana_managed_receiver_configs": [ + { + "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", + "name": "lotsa-emails", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", + "singleEmail": false + }, + "secureFields": {} + } + ] + }, + { + "name": "Slack with multiple channels", + "grafana_managed_receiver_configs": [ + { + "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", + "name": "Slack with multiple channels", + "type": "slack", + "disableResolveMessage": false, + "settings": { "recipient": "test-alerts" }, + "secureFields": { "token": true } + }, + { + "uid": "b286a3be-f690-49e2-8605-b075cbace2df", + "name": "Slack with multiple channels", + "type": "slack", + "disableResolveMessage": false, + "settings": { "recipient": "test-alerts2" }, + "secureFields": { "token": true } + } + ] + } + ] + } +} diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json new file mode 100644 index 00000000000..d6ceb50db23 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json @@ -0,0 +1,57 @@ +[ + { + "active": true, + "integrations": [ + { + "lastNotifyAttempt": "2023-07-02T21:35:34.841+02:00", + "lastNotifyAttemptDuration": "1ms", + "lastNotifyAttemptError": "failed to send notification to email addresses: gilles.demey@grafana.com: dial tcp 192.168.1.21:1025: connect: connection refused", + "name": "email", + "sendResolved": true + } + ], + "name": "grafana-default-email" + }, + { + "active": false, + "integrations": [ + { + "lastNotifyAttempt": "0001-01-01T00:00:00.000Z", + "lastNotifyAttemptDuration": "0s", + "name": "email", + "sendResolved": true + } + ], + "name": "provisioned-contact-point" + }, + { + "active": false, + "integrations": [ + { + "lastNotifyAttempt": "0001-01-01T00:00:00.000Z", + "lastNotifyAttemptDuration": "0s", + "name": "email", + "sendResolved": true + } + ], + "name": "lotsa-emails" + }, + { + "active": false, + "integrations": [ + { + "lastNotifyAttempt": "0001-01-01T00:00:00.000Z", + "lastNotifyAttemptDuration": "0s", + "name": "slack", + "sendResolved": true + }, + { + "lastNotifyAttempt": "0001-01-01T00:00:00.000Z", + "lastNotifyAttemptDuration": "0s", + "name": "slack", + "sendResolved": true + } + ], + "name": "Slack with multiple channels" + } +] diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts b/public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts new file mode 100644 index 00000000000..951a7bc1eff --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts @@ -0,0 +1,24 @@ +import { rest } from 'msw'; + +import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; +import { ReceiversStateDTO } from 'app/types'; + +import { setupMswServer } from '../../../mockApi'; + +import alertmanagerMock from './alertmanager.config.mock.json'; +import receiversMock from './receivers.mock.json'; + +const server = setupMswServer(); + +server.use( + // this endpoint is a grafana built-in alertmanager + rest.get('/api/alertmanager/grafana/config/api/v1/alerts', (_req, res, ctx) => + res(ctx.json(alertmanagerMock)) + ), + // this endpoint is only available for the built-in alertmanager + rest.get('/api/alertmanager/grafana/config/api/v1/receivers', (_req, res, ctx) => + res(ctx.json(receiversMock)) + ) +); + +export default server; diff --git a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap new file mode 100644 index 00000000000..81bfcb128f2 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useContactPoints should return contact points with status 1`] = ` +{ + "contactPoints": [ + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "grafana-default-email", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "xeKQrBrnk", + Symbol(receiver_status): { + "lastNotifyAttempt": "2023-07-02T21:35:34.841+02:00", + "lastNotifyAttemptDuration": "1ms", + "lastNotifyAttemptError": "failed to send notification to email addresses: gilles.demey@grafana.com: dial tcp 192.168.1.21:1025: connect: connection refused", + "name": "email", + "sendResolved": true, + }, + }, + ], + "name": "grafana-default-email", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "provisioned-contact-point", + "provenance": "api", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "s8SdCVjnk", + Symbol(receiver_status): { + "lastNotifyAttempt": "", + "lastNotifyAttemptDuration": "", + "name": "email", + "sendResolved": true, + }, + }, + ], + "name": "provisioned-contact-point", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "lotsa-emails", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", + Symbol(receiver_status): { + "lastNotifyAttempt": "", + "lastNotifyAttemptDuration": "", + "name": "email", + "sendResolved": true, + }, + }, + ], + "name": "lotsa-emails", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts", + }, + "type": "slack", + "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", + Symbol(receiver_status): { + "lastNotifyAttempt": "", + "lastNotifyAttemptDuration": "", + "name": "slack", + "sendResolved": true, + }, + }, + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts2", + }, + "type": "slack", + "uid": "b286a3be-f690-49e2-8605-b075cbace2df", + Symbol(receiver_status): { + "lastNotifyAttempt": "", + "lastNotifyAttemptDuration": "", + "name": "slack", + "sendResolved": true, + }, + }, + ], + "name": "Slack with multiple channels", + }, + ], + "error": undefined, + "isLoading": false, +} +`; diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx new file mode 100644 index 00000000000..ca7ff791f9e --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx @@ -0,0 +1,18 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { TestProvider } from 'test/helpers/TestProvider'; + +import './__mocks__/server'; +import { useContactPointsWithStatus } from './useContactPoints'; + +describe('useContactPoints', () => { + it('should return contact points with status', async () => { + const { result } = renderHook(() => useContactPointsWithStatus('grafana'), { + wrapper: TestProvider, + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current).toMatchSnapshot(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx index 32244114cd9..095c586afec 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx @@ -3,21 +3,87 @@ * and (if available) it will also fetch the status from the Grafana Managed status endpoint */ -import { NotifierType, NotifierStatus } from 'app/types'; +import { produce } from 'immer'; +import { remove } from 'lodash'; -// A Contact Point has 1 or more integrations -// each integration can have additional metadata assigned to it -export interface ContactPoint { - notifiers: T[]; +import { alertmanagerApi } from '../../api/alertmanagerApi'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; + +import { enhanceContactPointsWithStatus } from './utils'; + +export const RECEIVER_STATUS_KEY = Symbol('receiver_status'); +const RECEIVER_STATUS_POLLING_INTERVAL = 10 * 1000; // 10 seconds + +/** + * This hook will combine data from two endpoints; + * 1. the alertmanager config endpoint where the definition of the receivers are + * 2. (if available) the alertmanager receiver status endpoint, currently Grafana Managed only + */ +export function useContactPointsWithStatus(selectedAlertmanager: string) { + const isGrafanaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; + + // fetch receiver status if we're dealing with a Grafana Managed Alertmanager + const fetchContactPointsStatus = alertmanagerApi.endpoints.getContactPointsStatus.useQuery(undefined, { + // TODO these don't seem to work since we've not called setupListeners() + refetchOnFocus: true, + refetchOnReconnect: true, + // re-fetch status every so often for up-to-date information + pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL, + // skip fetching receiver statuses if not Grafana AM + skip: !isGrafanaManagedAlertmanager, + }); + + // fetch the latest config from the Alertmanager + const fetchAlertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery( + selectedAlertmanager, + { + refetchOnFocus: true, + refetchOnReconnect: true, + selectFromResult: (result) => ({ + ...result, + contactPoints: result.data ? enhanceContactPointsWithStatus(result.data, fetchContactPointsStatus.data) : [], + }), + } + ); + + // TODO kinda yucky to combine hooks like this, better alternative? + const error = fetchAlertmanagerConfiguration.error ?? fetchContactPointsStatus.error; + const isLoading = fetchAlertmanagerConfiguration.isLoading || fetchContactPointsStatus.isLoading; + + const contactPoints = fetchAlertmanagerConfiguration.contactPoints; + + return { + error, + isLoading, + contactPoints, + }; } -interface Notifier { - type: NotifierType; -} +export function useDeleteContactPoint(selectedAlertmanager: string) { + const [fetchAlertmanagerConfig] = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useLazyQuery(); + const [updateAlertManager, updateAlertmanagerState] = + alertmanagerApi.endpoints.updateAlertmanagerConfiguration.useMutation(); -// Grafana Managed contact points have receivers with additional diagnostics -export interface NotifierWithDiagnostics extends Notifier { - status: NotifierStatus; -} + const deleteTrigger = (contactPointName: string) => { + return fetchAlertmanagerConfig(selectedAlertmanager).then(({ data }) => { + if (!data) { + return; + } -export function useContactPoints(AlertManagerSourceName: string) {} + const newConfig = produce(data, (draft) => { + remove(draft?.alertmanager_config?.receivers ?? [], (receiver) => receiver.name === contactPointName); + return draft; + }); + + return updateAlertManager({ + selectedAlertmanager, + config: newConfig, + }).unwrap(); + }); + }; + + return { + deleteTrigger, + updateAlertmanagerState, + }; +} diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts new file mode 100644 index 00000000000..d9e42a44f1f --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -0,0 +1,93 @@ +import { split } from 'lodash'; +import { ReactNode } from 'react'; + +import { + AlertManagerCortexConfig, + GrafanaManagedContactPoint, + GrafanaManagedReceiverConfig, +} from 'app/plugins/datasource/alertmanager/types'; +import { NotifierStatus, ReceiversStateDTO } from 'app/types'; + +import { extractReceivers } from '../../utils/receivers'; + +import { RECEIVER_STATUS_KEY } from './useContactPoints'; + +export function isProvisioned(contactPoint: GrafanaManagedContactPoint) { + // for some reason the provenance is on the receiver and not the entire contact point + const provenance = contactPoint.grafana_managed_receiver_configs?.find((receiver) => receiver.provenance)?.provenance; + + return Boolean(provenance); +} + +// TODO we should really add some type information to these receiver settings... +export function getReceiverDescription(receiver: GrafanaManagedReceiverConfig): ReactNode | undefined { + switch (receiver.type) { + case 'email': { + const hasEmailAddresses = 'addresses' in receiver.settings; // when dealing with alertmanager email_configs we don't normalize the settings + return hasEmailAddresses ? summarizeEmailAddresses(receiver.settings['addresses']) : undefined; + } + case 'slack': { + const channelName = receiver.settings['recipient']; + return channelName ? `#${channelName}` : undefined; + } + case 'kafka': { + const topicName = receiver.settings['kafkaTopic']; + return topicName; + } + default: + return undefined; + } +} + +// input: foo+1@bar.com, foo+2@bar.com, foo+3@bar.com, foo+4@bar.com +// output: foo+1@bar.com, foo+2@bar.com, +2 more +function summarizeEmailAddresses(addresses: string): string { + const MAX_ADDRESSES_SHOWN = 3; + const SUPPORTED_SEPARATORS = /,|;|\\n/; + + const emails = addresses.trim().split(SUPPORTED_SEPARATORS); + const notShown = emails.length - MAX_ADDRESSES_SHOWN; + + const truncatedAddresses = split(addresses, SUPPORTED_SEPARATORS, MAX_ADDRESSES_SHOWN); + if (notShown > 0) { + truncatedAddresses.push(`+${notShown} more`); + } + + return truncatedAddresses.join(', '); +} + +// Grafana Managed contact points have receivers with additional diagnostics +export interface ReceiverConfigWithStatus extends GrafanaManagedReceiverConfig { + // we're using a symbol here so we'll never have a conflict on keys for a receiver + // we also specify that the diagnostics might be "undefined" for vanilla Alertmanager + [RECEIVER_STATUS_KEY]?: NotifierStatus | undefined; +} + +export interface ContactPointWithStatus extends GrafanaManagedContactPoint { + grafana_managed_receiver_configs: ReceiverConfigWithStatus[]; +} + +/** + * This function adds the status information for each of the integrations (contact point types) in a contact point + * 1. we iterate over all contact points + * 2. for each contact point we "enhance" it with the status or "undefined" for vanilla Alertmanager + */ +export function enhanceContactPointsWithStatus( + result: AlertManagerCortexConfig, + status: ReceiversStateDTO[] = [] +): ContactPointWithStatus[] { + const contactPoints = result.alertmanager_config.receivers ?? []; + + return contactPoints.map((contactPoint) => { + const receivers = extractReceivers(contactPoint); + const statusForReceiver = status.find((status) => status.name === contactPoint.name); + + return { + ...contactPoint, + grafana_managed_receiver_configs: receivers.map((receiver, index) => ({ + ...receiver, + [RECEIVER_STATUS_KEY]: statusForReceiver?.integrations[index], + })), + }; + }); +} diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index bc45af1f128..684a090ebfd 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useMemo, useState } from 'react'; import { LoadingPlaceholder } from '@grafana/ui'; import { AlertManagerCortexConfig, + GrafanaManagedContactPoint, GrafanaManagedReceiverConfig, - Receiver, TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; import { useDispatch } from 'app/types'; @@ -32,7 +32,7 @@ import { TestContactPointModal } from './TestContactPointModal'; interface Props { alertManagerSourceName: string; config: AlertManagerCortexConfig; - existing?: Receiver; + existing?: GrafanaManagedContactPoint; } const defaultChannelValues: GrafanaChannelValues = Object.freeze({ diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts index 9c1c245c501..099a73b8fc8 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts @@ -1,4 +1,4 @@ -import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types'; +import { GrafanaManagedContactPoint } from '../../../../../../plugins/datasource/alertmanager/types'; import { SupportedPlugin } from '../../../types/pluginBridges'; export interface AmRouteReceiver { @@ -7,7 +7,7 @@ export interface AmRouteReceiver { grafanaAppReceiverType?: SupportedPlugin; } -export interface ReceiverWithTypes extends Receiver { +export interface ReceiverWithTypes extends GrafanaManagedContactPoint { grafanaAppReceiverType?: SupportedPlugin; } export const GRAFANA_APP_RECEIVERS_SOURCE_IMAGE: Record = { diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index e8715f33eb0..038a776fb35 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -21,7 +21,13 @@ import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelect import { deleteRuleAction, saveRuleFormAction } from '../../state/actions'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { initialAsyncRequestState } from '../../utils/redux'; -import { getDefaultFormValues, getDefaultQueries, MINUTE, rulerRuleToFormValues } from '../../utils/rule-form'; +import { + getDefaultFormValues, + getDefaultQueries, + MINUTE, + normalizeDefaultAnnotations, + rulerRuleToFormValues, +} from '../../utils/rule-form'; import * as ruleId from '../../utils/rule-id'; import { CloudEvaluationBehavior } from './CloudEvaluationBehavior'; @@ -50,7 +56,7 @@ const AlertRuleNameInput = () => { const ruleFormType = watch('type'); return ( - + { } = useFormContext(); const type = watch('type'); + const dataSourceName = watch('dataSourceName'); // cloud recording rules do not have alert conditions if (type === RuleFormType.cloudRecording) { @@ -28,8 +30,11 @@ export const CloudEvaluationBehavior = () => { } return ( - - + +
{ />
+ {type === RuleFormType.cloudAlerting && dataSourceName && ( + + )} +
); diff --git a/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx b/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx index 0b255db5692..cd2c21eb57b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { useAsync } from 'react-use'; import { DataSourceInstanceSettings } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { dispatch } from 'app/store/store'; import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler'; @@ -28,14 +28,6 @@ export function CloudRulesSourcePicker({ value, ...props }: Props): JSX.Element ); return ( - + ); } diff --git a/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx index ad777757ee6..be14d996704 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx @@ -56,11 +56,12 @@ export function DetailsStep() { return ( - {(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) && - dataSourceName && } + {ruleFormType === RuleFormType.cloudRecording && dataSourceName && ( + + )} {type !== RuleFormType.cloudRecording && } diff --git a/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx b/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx index 64bb430b056..4b88fed73e9 100644 --- a/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx @@ -182,7 +182,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf Pending period diff --git a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx index 0b0c6780189..f3c4cf41984 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -86,7 +86,7 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { return ( + {/* This is the cloud data source selector */} {(type === RuleFormType.cloudRecording || type === RuleFormType.cloudAlerting) && ( @@ -436,7 +439,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P /> {/* Expression Queries */} Expressions -
Manipulate data returned from queries with math and other operations
+
Manipulate data returned from queries with math and other operations.
= { }; export const annotationDescriptions: Record = { - [Annotation.description]: 'Description of what the alert rule does', - [Annotation.summary]: 'Short summary of what happened and why', - [Annotation.runbookURL]: 'Webpage where you keep your runbook for the alert', + [Annotation.description]: 'Description of what the alert rule does.', + [Annotation.summary]: 'Short summary of what happened and why.', + [Annotation.runbookURL]: 'Webpage where you keep your runbook for the alert.', [Annotation.dashboardUID]: '', [Annotation.panelID]: '', [Annotation.alertId]: '', diff --git a/public/app/features/alerting/unified/utils/receiver-form.ts b/public/app/features/alerting/unified/utils/receiver-form.ts index b3de19969fa..a80459e354b 100644 --- a/public/app/features/alerting/unified/utils/receiver-form.ts +++ b/public/app/features/alerting/unified/utils/receiver-form.ts @@ -2,6 +2,8 @@ import { isArray, isNil, omitBy } from 'lodash'; import { AlertManagerCortexConfig, + AlertmanagerReceiver, + GrafanaManagedContactPoint, GrafanaManagedReceiverConfig, Receiver, Route, @@ -18,7 +20,7 @@ import { } from '../types/receiver-form'; export function grafanaReceiverToFormValues( - receiver: Receiver, + receiver: GrafanaManagedContactPoint, notifiers: NotifierDTO[] ): [ReceiverFormValues, GrafanaChannelMap] { const channelMap: GrafanaChannelMap = {}; @@ -92,7 +94,7 @@ export function formValuesToCloudReceiver( values: ReceiverFormValues, defaults: CloudChannelValues ): Receiver { - const recv: Receiver = { + const recv: AlertmanagerReceiver = { name: values.name, }; values.items.forEach(({ __id, type, settings, sendResolved }) => { @@ -101,11 +103,10 @@ export function formValuesToCloudReceiver( send_resolved: sendResolved ?? defaults.sendResolved, }); - const configsKey = `${type}_configs`; - if (!recv[configsKey]) { - recv[configsKey] = [channel]; + if (!(`${type}_configs` in recv)) { + recv[`${type}_configs`] = [channel]; } else { - (recv[configsKey] as unknown[]).push(channel); + (recv[`${type}_configs`] as unknown[]).push(channel); } }); return recv; diff --git a/public/app/features/alerting/unified/utils/receivers.ts b/public/app/features/alerting/unified/utils/receivers.ts index c599e45514b..11bed1bf7fc 100644 --- a/public/app/features/alerting/unified/utils/receivers.ts +++ b/public/app/features/alerting/unified/utils/receivers.ts @@ -1,4 +1,4 @@ -import { capitalize } from 'lodash'; +import { capitalize, isEmpty, times } from 'lodash'; import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts'; import { GrafanaManagedReceiverConfig, Receiver } from 'app/plugins/datasource/alertmanager/types'; @@ -9,7 +9,7 @@ import { NotifierDTO } from 'app/types'; type NotifierTypeCounts = Record; // name : count export function extractNotifierTypeCounts(receiver: Receiver, grafanaNotifiers: NotifierDTO[]): NotifierTypeCounts { - if (receiver['grafana_managed_receiver_configs']) { + if ('grafana_managed_receiver_configs' in receiver) { return getGrafanaNotifierTypeCounts(receiver.grafana_managed_receiver_configs ?? [], grafanaNotifiers); } return getCortexAlertManagerNotifierTypeCounts(receiver); @@ -29,6 +29,45 @@ function getCortexAlertManagerNotifierTypeCounts(receiver: Receiver): NotifierTy }, {}); } +/** + * This function will extract the integrations that have been defined for either grafana managed contact point + * or vanilla Alertmanager receiver. + * + * It will attempt to normalize the data structure to how they have been defined for Grafana managed contact points. + * That way we can work with the same data structure in the UI. + * + * We don't normalize the configuration settings and those are blank for vanilla Alertmanager receivers. + * + * Example input: + * { name: 'my receiver', email_configs: [{ from: "foo@bar.com" }] } + * + * Example output: + * { name: 'my receiver', grafana_managed_receiver_configs: [{ type: 'email', settings: {} }] } + */ +export function extractReceivers(receiver: Receiver): GrafanaManagedReceiverConfig[] { + if ('grafana_managed_receiver_configs' in receiver) { + return receiver.grafana_managed_receiver_configs ?? []; + } + + const integrations = Object.entries(receiver) + .filter(([key]) => key !== 'grafana_managed_receiver_configs' && key.endsWith('_configs')) + .filter(([_, value]) => Array.isArray(value) && !isEmpty(value)) + .reduce((acc: GrafanaManagedReceiverConfig[], [key, value]) => { + const type = key.replace('_configs', ''); + + const configs = times(value.length, () => ({ + name: receiver.name, + type: type, + settings: [], // we don't normalize the configuration values + disableResolveMessage: false, + })); + + return acc.concat(configs); + }, []); + + return integrations; +} + function getGrafanaNotifierTypeCounts( configs: GrafanaManagedReceiverConfig[], grafanaNotifiers: NotifierDTO[] diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index bb5f3af1ad6..63eb52d6a68 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -94,7 +94,7 @@ export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO { throw new Error(`unexpected rule type: ${type}`); } -function listifyLabelsOrAnnotations( +export function listifyLabelsOrAnnotations( item: Labels | Annotations | undefined, addEmpty: boolean ): Array<{ key: string; value: string }> { @@ -106,7 +106,7 @@ function listifyLabelsOrAnnotations( } //make sure default annotations are always shown in order even if empty -function normalizeDefaultAnnotations(annotations: Array<{ key: string; value: string }>) { +export function normalizeDefaultAnnotations(annotations: Array<{ key: string; value: string }>) { const orderedAnnotations = [...annotations]; const defaultAnnotationKeys = defaultAnnotations.map((annotation) => annotation.key); @@ -179,6 +179,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF return { ...defaultFormValues, ...alertingRuleValues, + annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)), type: RuleFormType.cloudAlerting, dataSourceName: ruleSourceName, namespace, diff --git a/public/app/features/auth-config/AuthConfigPage.tsx b/public/app/features/auth-config/AuthConfigPage.tsx index 64e16ff6628..88d51a33a18 100644 --- a/public/app/features/auth-config/AuthConfigPage.tsx +++ b/public/app/features/auth-config/AuthConfigPage.tsx @@ -88,7 +88,6 @@ export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSet displayName={provider.displayName} authType={provider.type} enabled={providerStatuses[provider.id]?.enabled} - configFoundInIniFile={providerStatuses[provider.id]?.configFoundInIniFile} configPath={provider.configPath} onClick={() => { onProviderCardClick(provider); @@ -103,9 +102,6 @@ export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSet buttonIcon="plus-circle" buttonLink={getProviderUrl(firstAvailableProvider)} buttonTitle={`Configure ${firstAvailableProvider.type}`} - description={`Important: if you have ${firstAvailableProvider.type} configuration enabled via the .ini file Grafana is using it. - Configuring ${firstAvailableProvider.type} via UI will take precedence over any configuration in the .ini file. - No changes will be written into .ini file.`} onClick={onCTAClick} /> )} @@ -118,7 +114,6 @@ export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSet displayName={provider.displayName} authType={provider.protocol} enabled={providerStatuses[provider.id]?.enabled} - configFoundInIniFile={providerStatuses[provider.id]?.configFoundInIniFile} configPath={provider.configPath} /> ))} diff --git a/public/app/features/auth-config/components/ProviderCard.tsx b/public/app/features/auth-config/components/ProviderCard.tsx index f15df0783a0..680f51916f0 100644 --- a/public/app/features/auth-config/components/ProviderCard.tsx +++ b/public/app/features/auth-config/components/ProviderCard.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Badge, Card, useStyles2, Icon, Tooltip } from '@grafana/ui'; +import { Badge, Card, useStyles2 } from '@grafana/ui'; import { BASE_PATH } from '../constants'; @@ -12,43 +12,19 @@ type Props = { providerId: string; displayName: string; enabled: boolean; - configFoundInIniFile?: boolean; configPath?: string; authType?: string; badges?: JSX.Element[]; onClick?: () => void; }; -export function ProviderCard({ - providerId, - displayName, - enabled, - configFoundInIniFile, - configPath, - authType, - badges, - onClick, -}: Props) { +export function ProviderCard({ providerId, displayName, enabled, configPath, authType, badges, onClick }: Props) { const styles = useStyles2(getStyles); configPath = BASE_PATH + (configPath || providerId); return ( onClick && onClick()}> {displayName} - {configFoundInIniFile && ( - <> - - - <> - - Configuration found in .ini file - - - - - )}
{authType && } {enabled ? : } @@ -82,11 +58,5 @@ export const getStyles = (theme: GrafanaTheme2) => { color: ${theme.colors.text.primary}; margin: 0; `, - initext: css` - font-size: ${theme.typography.bodySmall.fontSize}; - color: ${theme.colors.text.secondary}; - padding: ${theme.spacing(1)} 0; // Add some padding - max-width: 90%; // Add a max-width to prevent text from stretching too wide - `, }; }; diff --git a/public/app/features/connections/pages/DataSourceDashboardsPage.tsx b/public/app/features/connections/pages/DataSourceDashboardsPage.tsx index 6b2164c03bd..56095d29358 100644 --- a/public/app/features/connections/pages/DataSourceDashboardsPage.tsx +++ b/public/app/features/connections/pages/DataSourceDashboardsPage.tsx @@ -3,16 +3,15 @@ import { useParams } from 'react-router-dom'; import { Page } from 'app/core/components/Page/Page'; import { DataSourceDashboards } from 'app/features/datasources/components/DataSourceDashboards'; -import { useDataSourceSettingsNav } from 'app/features/datasources/state'; + +import { useDataSourceSettingsNav } from '../hooks/useDataSourceSettingsNav'; export function DataSourceDashboardsPage() { const { uid } = useParams<{ uid: string }>(); - const params = new URLSearchParams(location.search); - const pageId = params.get('page'); - const nav = useDataSourceSettingsNav(uid, pageId); + const { navId, pageNav } = useDataSourceSettingsNav(); return ( - + diff --git a/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx b/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx index f692b547d02..5e3aed350a8 100644 --- a/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx +++ b/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; import { Card, Field, FieldSet, Input, useStyles2 } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getVariableUsageInfo } from '../../explore/utils/links'; diff --git a/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx b/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx index 2643e7cfb49..9b66bea2a4b 100644 --- a/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx +++ b/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { DataSourceInstanceSettings } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; import { Field, FieldSet } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { QueryEditorField } from './QueryEditorField'; import { useCorrelationsFormContext } from './correlationsFormContext'; diff --git a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx index 2cf14413013..1e1daac9c33 100644 --- a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx +++ b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx @@ -11,7 +11,7 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Stack } from '@grafana/experimental'; -import { DataSourcePicker, getDataSourceSrv, locationService } from '@grafana/runtime'; +import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { AnnotationPanelFilter } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; import { Button, @@ -27,6 +27,7 @@ import { import { ColorValueEditor } from 'app/core/components/OptionsUI/color'; import config from 'app/core/config'; import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DashboardModel } from '../../state/DashboardModel'; diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index b84ac5caefb..10b4448d88f 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -164,8 +164,8 @@ it('replaces datasource ref in library panel', async () => { if ('error' in exported) { throw new Error('error should not be returned when making exportable json'); } - expect(exported.__elements['c46a6b49-de40-43b3-982c-1b5e1ec084a4'].model.datasource.uid).toBe('${DS_GFDB}'); - expect(exported.__inputs[0].name).toBe('DS_GFDB'); + expect(exported.__elements!['c46a6b49-de40-43b3-982c-1b5e1ec084a4'].model.datasource.uid).toBe('${DS_GFDB}'); + expect(exported.__inputs![0].name).toBe('DS_GFDB'); }); it('If a panel queries has no datasource prop ignore it', async () => { diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts index 96f9ce2fa69..c57069f339d 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts @@ -13,12 +13,21 @@ import { VariableOption, VariableRefresh } from '../../../variables/types'; import { DashboardModel } from '../../state/DashboardModel'; import { GridPos } from '../../state/PanelModel'; -interface Input { +export interface InputUsage { + libraryPanels?: LibraryPanel[]; +} + +export interface LibraryPanel { + name: string; + uid: string; +} +export interface Input { name: string; type: string; label: string; value: any; description: string; + usage?: InputUsage; } interface Requires { @@ -30,20 +39,17 @@ interface Requires { }; } -interface ExternalDashboard { - __inputs: Input[]; - __elements: Record; - __requires: Array; +export interface ExternalDashboard { + __inputs?: Input[]; + __elements?: Record; + __requires?: Array; panels: Array; } interface PanelWithExportableLibraryPanel { gridPos: GridPos; id: number; - libraryPanel: { - name: string; - uid: string; - }; + libraryPanel: LibraryPanel; } function isExportableLibraryPanel(p: any): p is PanelWithExportableLibraryPanel { @@ -58,6 +64,7 @@ interface DataSources { type: string; pluginId: string; pluginName: string; + usage?: InputUsage; }; } @@ -132,7 +139,10 @@ export class DashboardExporter { return; } - const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); + const libraryPanel = obj.libraryPanel; + const libraryPanelSuffix = !!libraryPanel ? '-for-library-panel' : ''; + let refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase() + libraryPanelSuffix.toUpperCase(); + datasources[refName] = { name: refName, label: ds.name, @@ -140,8 +150,18 @@ export class DashboardExporter { type: 'datasource', pluginId: ds.meta?.id, pluginName: ds.meta?.name, + usage: datasources[refName]?.usage, }; + if (!!libraryPanel) { + const libPanels = datasources[refName]?.usage?.libraryPanels || []; + libPanels.push({ name: libraryPanel.name, uid: libraryPanel.uid }); + + datasources[refName].usage = { + libraryPanels: libPanels, + }; + } + obj.datasource = { type: ds.meta.id, uid: '${' + refName + '}' }; }); }; diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx index aac37c4968c..5b9ba51f778 100644 --- a/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx +++ b/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx @@ -4,6 +4,7 @@ import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import React from 'react'; import { DataSourceInstanceSettings, DataSourcePluginMeta, PluginMetaInfo, PluginType } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { ModalRoot, ModalsProvider } from '@grafana/ui'; import config from 'app/core/config'; import { defaultFileUploadQuery } from 'app/plugins/datasource/grafana/types'; @@ -145,7 +146,10 @@ describe('DataSourceDropdown', () => { it('should display the current selected DS in the selector', async () => { getInstanceSettingsMock.mockReturnValue(mockDS2); render(); - expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', mockDS2.name); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( + 'placeholder', + mockDS2.name + ); expect(screen.getByAltText(`${mockDS2.meta.name} logo`)).toBeVisible(); }); @@ -166,7 +170,10 @@ describe('DataSourceDropdown', () => { it('should display the default DS as selected when `current` is not set', async () => { getInstanceSettingsMock.mockReturnValue(mockDS2); render(); - expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', mockDS2.name); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( + 'placeholder', + mockDS2.name + ); expect(screen.getByAltText(`${mockDS2.meta.name} logo`)).toBeVisible(); }); @@ -180,12 +187,15 @@ describe('DataSourceDropdown', () => { it('should disable the dropdown when `disabled` is true', () => { render(); - expect(screen.getByTestId('Select a data source')).toBeDisabled(); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toBeDisabled(); }); it('should assign the correct `id` to the input element to pair it with a label', () => { render(); - expect(screen.getByTestId('Select a data source')).toHaveAttribute('id', 'custom.input.id'); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( + 'id', + 'custom.input.id' + ); }); it('should not set the default DS when setting `noDefault` to true and `current` is not provided', () => { @@ -195,7 +205,10 @@ describe('DataSourceDropdown', () => { // Doesn't try to get the default DS expect(getListMock).not.toBeCalled(); expect(getInstanceSettingsMock).not.toBeCalled(); - expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', 'Select data source'); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( + 'placeholder', + 'Select data source' + ); }); }); diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx index 78d7fe9c16e..39fa1071752 100644 --- a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx +++ b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import { useDialog } from '@react-aria/dialog'; +import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { usePopper } from 'react-popper'; @@ -70,55 +71,25 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { ...restProps } = props; + const styles = useStyles2((theme: GrafanaTheme2) => getStylesDropdown(theme, props)); const [isOpen, setOpen] = useState(false); const [inputHasFocus, setInputHasFocus] = useState(false); - const [markerElement, setMarkerElement] = useState(); - const [selectorElement, setSelectorElement] = useState(); const [filterTerm, setFilterTerm] = useState(''); - const openDropdown = () => { - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.OPEN_DROPDOWN }); - setOpen(true); - markerElement?.focus(); - }; + const { onKeyDown, keyboardEvents } = useKeyNavigationListener(); + const ref = useRef(null); + + // Used to position the popper correctly and to bring back the focus when navigating from footer to input + const [markerElement, setMarkerElement] = useState(); + // Used to position the popper correctly + const [selectorElement, setSelectorElement] = useState(); + // Used to move the focus to the footer when tabbing from the input + const [footerRef, setFooterRef] = useState(); const currentDataSourceInstanceSettings = useDatasource(current); + const grafanaDS = useDatasource('-- Grafana --'); const currentValue = Boolean(!current && noDefault) ? undefined : currentDataSourceInstanceSettings; const prefixIcon = filterTerm && isOpen ? : ; - const { onKeyDown, keyboardEvents } = useKeyNavigationListener(); - - useEffect(() => { - const sub = keyboardEvents.subscribe({ - next: (keyEvent) => { - switch (keyEvent?.code) { - case 'ArrowDown': { - openDropdown(); - keyEvent.preventDefault(); - break; - } - case 'ArrowUp': - openDropdown(); - keyEvent.preventDefault(); - break; - case 'Escape': - onClose(); - markerElement?.focus(); - keyEvent.preventDefault(); - } - }, - }); - return () => sub.unsubscribe(); - }); - const grafanaDS = useDatasource('-- Grafana --'); - - const onClickAddCSV = () => { - if (!grafanaDS) { - return; - } - - onChange(grafanaDS, [defaultFileUploadQuery]); - }; - const popper = usePopper(markerElement, selectorElement, { placement: 'bottom-start', modifiers: [ @@ -136,9 +107,9 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { const onClose = useCallback(() => { setFilterTerm(''); setOpen(false); - }, [setOpen]); + markerElement?.focus(); + }, [setOpen, markerElement]); - const ref = useRef(null); const { overlayProps, underlayProps } = useOverlay( { onClose: onClose, @@ -150,9 +121,72 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { }, ref ); - const { dialogProps } = useDialog({}, ref); + const { dialogProps } = useDialog( + { + 'aria-label': 'Opened data source picker list', + }, + ref + ); - const styles = useStyles2((theme: GrafanaTheme2) => getStylesDropdown(theme, props)); + function openDropdown() { + reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.OPEN_DROPDOWN }); + setOpen(true); + markerElement?.focus(); + } + + function onClickAddCSV() { + if (!grafanaDS) { + return; + } + + onChange(grafanaDS, [defaultFileUploadQuery]); + } + + function onKeyDownInput(keyEvent: React.KeyboardEvent) { + // From the input, it navigates to the footer + if (keyEvent.key === 'Tab' && !keyEvent.shiftKey && isOpen) { + keyEvent.preventDefault(); + footerRef?.focus(); + } + // From the input, if we navigate back, it closes the dropdown + if (keyEvent.key === 'Tab' && keyEvent.shiftKey && isOpen) { + onClose(); + } + onKeyDown(keyEvent); + } + + function onNavigateOutsiteFooter(e: React.KeyboardEvent) { + // When navigating back, the dropdown keeps open and the input element is focused. + if (e.shiftKey) { + e.preventDefault(); + markerElement?.focus(); + // When navigating forward, the dropdown closes and and the element next to the input element is focused. + } else { + onClose(); + } + } + + useEffect(() => { + const sub = keyboardEvents.subscribe({ + next: (keyEvent) => { + switch (keyEvent?.code) { + case 'ArrowDown': + openDropdown(); + keyEvent.preventDefault(); + break; + case 'ArrowUp': + openDropdown(); + keyEvent.preventDefault(); + break; + case 'Escape': + onClose(); + keyEvent.preventDefault(); + break; + } + }, + }); + return () => sub.unsubscribe(); + }); return (
@@ -163,18 +197,17 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { id={inputId || 'data-source-picker'} className={inputHasFocus ? undefined : styles.input} data-testid={selectors.components.DataSourcePicker.inputV2} + aria-label="Select a data source" prefix={currentValue ? prefixIcon : undefined} suffix={} placeholder={hideTextValue ? '' : dataSourceLabel(currentValue) || placeholder} - onClick={openDropdown} onFocus={() => { setInputHasFocus(true); }} onBlur={() => { setInputHasFocus(false); - onClose(); }} - onKeyDown={onKeyDown} + onKeyDown={onKeyDownInput} value={filterTerm} onChange={(e) => { openDropdown(); @@ -187,19 +220,16 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { {isOpen ? (
- {/* TODO: fix keyboard a11y */} - {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} -
{ - e.preventDefault(); /** Need to prevent default here to stop onMouseDown to trigger onBlur of the input element */ - }} - > +
{ onClose(); if (ds.uid !== currentValue?.uid) { @@ -208,13 +238,9 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { } }} onClose={onClose} - current={currentValue} - style={popper.styles.popper} - ref={setSelectorElement} onClickAddCSV={onClickAddCSV} - {...restProps} onDismiss={onClose} - {...popper.attributes.popper} + onNavigateOutsiteFooter={onNavigateOutsiteFooter} />
@@ -249,10 +275,13 @@ export interface PickerContentProps extends DataSourceDropdownProps { filterTerm?: string; onClose: () => void; onDismiss: () => void; + footerRef: (element: HTMLElement | null) => void; + onNavigateOutsiteFooter: (e: React.KeyboardEvent) => void; } const PickerContent = React.forwardRef((props, ref) => { - const { filterTerm, onChange, onClose, onClickAddCSV, current, filter, uploadFile } = props; + const { filterTerm, onChange, onClose, onClickAddCSV, current, filter } = props; + const changeCallback = useCallback( (ds: DataSourceInstanceSettings) => { onChange(ds); @@ -285,50 +314,14 @@ const PickerContent = React.forwardRef((prop } > -
- - {({ showModal, hideModal }) => ( - - )} - - {uploadFile && config.featureToggles.editPanelCSVDragAndDrop && ( - - )} -
+ +
+
); }); @@ -359,3 +352,84 @@ function getStylesPickerContent(theme: GrafanaTheme2) { `, }; } + +export interface FooterProps extends PickerContentProps {} + +function Footer({ onClose, onChange, onClickAddCSV, ...props }: FooterProps) { + const styles = useStyles2(getStylesFooter); + const isUploadFileEnabled = props.uploadFile && config.featureToggles.editPanelCSVDragAndDrop; + + const onKeyDownLastButton = (e: React.KeyboardEvent) => { + if (e.key === 'Tab') { + props.onNavigateOutsiteFooter(e); + } + }; + const onKeyDownFirstButton = (e: React.KeyboardEvent) => { + if (e.key === 'Tab' && e.shiftKey) { + props.onNavigateOutsiteFooter(e); + } + }; + + return ( +
+ + {({ showModal, hideModal }) => ( + + )} + + {isUploadFileEnabled && ( + + )} +
+ ); +} + +function getStylesFooter(theme: GrafanaTheme2) { + return { + footer: css` + flex: 0; + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + padding: ${theme.spacing(1.5)}; + border-top: 1px solid ${theme.colors.border.weak}; + background-color: ${theme.colors.background.secondary}; + `, + }; +} diff --git a/public/app/features/datasources/pages/EditDataSourcePage.test.tsx b/public/app/features/datasources/pages/EditDataSourcePage.test.tsx index 544e1cc01d8..4ad10721056 100644 --- a/public/app/features/datasources/pages/EditDataSourcePage.test.tsx +++ b/public/app/features/datasources/pages/EditDataSourcePage.test.tsx @@ -4,7 +4,7 @@ import { Store } from 'redux'; import { TestProvider } from 'test/helpers/TestProvider'; import { LayoutModes } from '@grafana/data'; -import { setAngularLoader, config, setPluginExtensionGetter } from '@grafana/runtime'; +import { setAngularLoader, setPluginExtensionGetter } from '@grafana/runtime'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { configureStore } from 'app/store/configureStore'; @@ -111,7 +111,6 @@ describe('', () => { }); it('should show updated action buttons when topnav is on', async () => { - config.featureToggles.topnav = true; setup(uid, store); await waitFor(() => { diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index bf0b0920c31..d55a300b348 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -1,24 +1,24 @@ import { css } from '@emotion/css'; -import { inRange } from 'lodash'; -import React, { useEffect, useState } from 'react'; -import { useWindowSize } from 'react-use'; +import React, { useEffect } from 'react'; import { ErrorBoundaryAlert } from '@grafana/ui'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useNavModel } from 'app/core/hooks/useNavModel'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { useDispatch, useSelector } from 'app/types'; +import { useSelector } from 'app/types'; import { ExploreQueryParams } from 'app/types/explore'; import { ExploreActions } from './ExploreActions'; import { ExplorePaneContainer } from './ExplorePaneContainer'; import { useExplorePageTitle } from './hooks/useExplorePageTitle'; +import { useSplitSizeUpdater } from './hooks/useSplitSizeUpdater'; import { useStateSync } from './hooks/useStateSync'; import { useTimeSrvFix } from './hooks/useTimeSrvFix'; -import { splitSizeUpdateAction } from './state/main'; import { isSplit, selectPanesEntries } from './state/selectors'; +const MIN_PANE_WIDTH = 200; + const styles = { pageScrollbarWrapper: css` width: 100%; @@ -38,13 +38,9 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor // if we were to update the URL on state change, the title would not match the URL. // Ultimately the URL is the single source of truth from which state is derived, the page title is not different useExplorePageTitle(props.queryParams); - const dispatch = useDispatch(); const { keybindings, chrome } = useGrafana(); const navModel = useNavModel('explore'); - const [rightPaneWidthRatio, setRightPaneWidthRatio] = useState(0.5); - const { width: windowWidth } = useWindowSize(); - const minWidth = 200; - const exploreState = useSelector((state) => state.explore); + const { updateSplitSize, widthCalc } = useSplitSizeUpdater(MIN_PANE_WIDTH); const panes = useSelector(selectPanesEntries); const hasSplit = useSelector(isSplit); @@ -59,33 +55,6 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor keybindings.setupTimeRangeBindings(false); }, [keybindings]); - const updateSplitSize = (size: number) => { - const evenSplitWidth = windowWidth / 2; - const areBothSimilar = inRange(size, evenSplitWidth - 100, evenSplitWidth + 100); - if (areBothSimilar) { - dispatch(splitSizeUpdateAction({ largerExploreId: undefined })); - } else { - dispatch( - splitSizeUpdateAction({ - largerExploreId: size > evenSplitWidth ? panes[1][0] : panes[0][0], - }) - ); - } - - setRightPaneWidthRatio(size / windowWidth); - }; - - let widthCalc = 0; - if (hasSplit) { - if (!exploreState.evenSplitPanes && exploreState.maxedExploreId) { - widthCalc = exploreState.maxedExploreId === panes[1][0] ? windowWidth - minWidth : minWidth; - } else if (exploreState.evenSplitPanes) { - widthCalc = Math.floor(windowWidth / 2); - } else if (rightPaneWidthRatio !== undefined) { - widthCalc = windowWidth * rightPaneWidthRatio; - } - } - return (
@@ -93,8 +62,8 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor {
{/* Width should be full width minus logsnavigation and padding */} { }; return ( undefined} timeZone={'utc'} @@ -140,26 +131,3 @@ describe('LogsTable', () => { }); }); }); - -const makeLog = (overrides: Partial): LogRowModel => { - const uid = overrides.uid || '1'; - const entry = `log message ${uid}`; - return { - uid, - entryFieldIndex: 0, - rowIndex: 0, - dataFrame: new MutableDataFrame(), - logLevel: LogLevel.debug, - entry, - hasAnsi: false, - hasUnescapedContent: false, - labels: {}, - raw: entry, - timeFromNow: '', - timeEpochMs: 1, - timeEpochNs: '1000000', - timeLocal: '', - timeUtc: '', - ...overrides, - }; -}; diff --git a/public/app/features/explore/Logs/LogsTable.tsx b/public/app/features/explore/Logs/LogsTable.tsx index 96f3f1c36f9..f4a7c75d828 100644 --- a/public/app/features/explore/Logs/LogsTable.tsx +++ b/public/app/features/explore/Logs/LogsTable.tsx @@ -6,7 +6,6 @@ import { applyFieldOverrides, DataFrame, Field, - LogRowModel, LogsSortOrder, sortDataFrame, SplitOpen, @@ -16,7 +15,7 @@ import { } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Table } from '@grafana/ui'; -import { shouldRemoveField } from 'app/features/logs/components/logParser'; +import { separateVisibleFields } from 'app/features/logs/components/logParser'; import { parseLogsFrame } from 'app/features/logs/logsFrame'; import { getFieldLinksForExplore } from '../utils/links'; @@ -28,7 +27,6 @@ interface Props { splitOpen: SplitOpen; range: TimeRange; logsSortOrder: LogsSortOrder; - rows: LogRowModel[]; } const getTableHeight = memoizeOne((dataFrames: DataFrame[] | undefined) => { @@ -40,7 +38,7 @@ const getTableHeight = memoizeOne((dataFrames: DataFrame[] | undefined) => { }); export const LogsTable: React.FunctionComponent = (props) => { - const { timeZone, splitOpen, range, logsSortOrder, width, logsFrames, rows } = props; + const { timeZone, splitOpen, range, logsSortOrder, width, logsFrames } = props; const [tableFrame, setTableFrame] = useState(undefined); @@ -129,18 +127,17 @@ export const LogsTable: React.FunctionComponent = (props) => { }); // remove fields that should not be displayed - dataFrame.fields.forEach((field: Field, index: number) => { - const row = rows[0]; // we just take the first row as the relevant row - if (shouldRemoveField(field, index, row, false, false)) { - transformations.push({ - id: 'organize', - options: { - excludeByName: { - [field.name]: true, - }, + + const hiddenFields = separateVisibleFields(dataFrame, { keepBody: true, keepTimestamp: true }).hidden; + hiddenFields.forEach((field: Field, index: number) => { + transformations.push({ + id: 'organize', + options: { + excludeByName: { + [field.name]: true, }, - }); - } + }, + }); }); if (transformations.length > 0) { const [transformedDataFrame] = await lastValueFrom(transformDataFrame(transformations, [dataFrame])); @@ -150,7 +147,7 @@ export const LogsTable: React.FunctionComponent = (props) => { } }; prepare(); - }, [prepareTableFrame, logsFrames, logsSortOrder, rows]); + }, [prepareTableFrame, logsFrames, logsSortOrder]); if (!tableFrame) { return null; diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index d9d9c35c55d..7fe502a3fb4 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -61,7 +61,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}]}' + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\", service_namespace=\\"namespace1\\"}","refId":""}]}' )}` ); }); @@ -163,7 +163,7 @@ describe('createSpanLinkFactory', () => { datasource: 'loki1_uid', queries: [ { - expr: '{cluster="cluster1", hostname="hostname1"} |="7946b05c2e2e4e5a" |="6605c7b08e715d6c"', + expr: '{cluster="cluster1", hostname="hostname1", service_namespace="namespace1"} |="7946b05c2e2e4e5a" |="6605c7b08e715d6c"', refId: '', }, ], @@ -265,10 +265,7 @@ describe('createSpanLinkFactory', () => { createTraceSpan({ process: { serviceName: 'service', - tags: [ - { key: 'service.name', value: 'serviceName' }, - { key: 'k8s.pod.name', value: 'podName' }, - ], + tags: [{ key: 'k8s.pod.name', value: 'podName' }], }, }) ); @@ -351,7 +348,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}]}' + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" service_namespace=\\"namespace1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}]}' )}` ); }); @@ -692,7 +689,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toContain( - `datasource":"${searchUID}","queries":[{"query":"cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]` + `datasource":"${searchUID}","queries":[{"query":"cluster:\\"cluster1\\" AND hostname:\\"hostname1\\" AND service_namespace:\\"namespace1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]` ); }); @@ -731,7 +728,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` + `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster:\\"cluster1\\" AND hostname:\\"hostname1\\" AND service_namespace:\\"namespace1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` )}` ); }); @@ -882,7 +879,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toContain( - `datasource":"${searchUID}","queries":[{"query":"cluster=\\"cluster1\\" AND hostname=\\"hostname1\\"","refId":""}]` + `datasource":"${searchUID}","queries":[{"query":"cluster=\\"cluster1\\" AND hostname=\\"hostname1\\" AND service_namespace=\\"namespace1\\"","refId":""}]` ); }); @@ -921,7 +918,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster=\\"cluster1\\" AND hostname=\\"hostname1\\"","refId":""}]}` + `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster=\\"cluster1\\" AND hostname=\\"hostname1\\" AND service_namespace=\\"namespace1\\"","refId":""}]}` )}` ); }); @@ -1145,7 +1142,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"cluster=\\"cluster1\\" OR hostname=\\"hostname1\\" or \\"7946b05c2e2e4e5a\\" or \\"6605c7b08e715d6c\\"","refId":""}]}' + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"cluster=\\"cluster1\\" OR hostname=\\"hostname1\\" OR service_namespace=\\"namespace1\\" or \\"7946b05c2e2e4e5a\\" or \\"6605c7b08e715d6c\\"","refId":""}]}' )}` ); }); @@ -1326,6 +1323,10 @@ function createTraceSpan(overrides: Partial = {}) { key: 'label2', value: 'val2', }, + { + key: 'service.namespace', + value: 'namespace1', + }, ], }, ...overrides, diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 26f1ecb1c1e..d0a4d181393 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -17,7 +17,7 @@ import { import { getTemplateSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Icon } from '@grafana/ui'; -import { TraceToLogsOptionsV2 } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; +import { TraceToLogsOptionsV2, TraceToLogsTag } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricQuery, TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { PromQuery } from 'app/plugins/datasource/prometheus/types'; @@ -115,7 +115,18 @@ export function createSpanLinkFactory({ /** * Default keys to use when there are no configured tags. */ -const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod'].map((k) => ({ key: k })); +const defaultKeys = [ + 'cluster', + 'hostname', + 'namespace', + 'pod', + 'service.name', + 'service.namespace', + 'deployment.environment', +].map((k) => ({ + key: k, + value: k.includes('.') ? k.replace('.', '_') : undefined, +})); function legacyCreateSpanLinkFactory( splitOpenFn: SplitOpen, @@ -149,7 +160,8 @@ function legacyCreateSpanLinkFactory( // deprecated blob format and we can map the link easily in data frame. if (logsDataSourceSettings && traceToLogsOptions) { const customQuery = traceToLogsOptions.customQuery ? traceToLogsOptions.query : undefined; - const tagsToUse = traceToLogsOptions.tags || defaultKeys; + const tagsToUse = + traceToLogsOptions.tags && traceToLogsOptions.tags.length > 0 ? traceToLogsOptions.tags : defaultKeys; switch (logsDataSourceSettings?.type) { case 'loki': tags = getFormattedTags(span, tagsToUse); @@ -480,7 +492,7 @@ function getQueryForFalconLogScale(span: TraceSpan, options: TraceToLogsOptionsV */ function getFormattedTags( span: TraceSpan, - tags: Array<{ key: string; value?: string }>, + tags: TraceToLogsTag[], { labelValueSign = '=', joinBy = ', ' }: { labelValueSign?: string; joinBy?: string } = {} ) { // In order, try to use mapped tags -> tags -> default tags diff --git a/public/app/features/explore/hooks/useSplitSizeUpdater.test.tsx b/public/app/features/explore/hooks/useSplitSizeUpdater.test.tsx new file mode 100644 index 00000000000..82f83e08fca --- /dev/null +++ b/public/app/features/explore/hooks/useSplitSizeUpdater.test.tsx @@ -0,0 +1,57 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import React, { ReactNode } from 'react'; +import { TestProvider } from 'test/helpers/TestProvider'; + +import { initialExploreState } from 'app/features/explore/state/main'; +import { makeExplorePaneState } from 'app/features/explore/state/utils'; +import { configureStore } from 'app/store/configureStore'; + +import { splitSizeUpdateAction } from '../state/main'; + +import { useSplitSizeUpdater } from './useSplitSizeUpdater'; + +describe('useSplitSizeUpdater', () => { + it('dispatches correct action and calculates widthCalc correctly', () => { + const store = configureStore({ + explore: { + ...initialExploreState, + panes: { + left: makeExplorePaneState(), + right: makeExplorePaneState(), + }, + }, + }); + + const minWidth = 200; + + const dispatchMock = jest.fn().mockImplementation(store.dispatch); + + const { result } = renderHook(() => useSplitSizeUpdater(minWidth), { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); + + // 1. Panes have similar width + act(() => { + result.current.updateSplitSize(450); + + expect(dispatchMock).toHaveBeenCalledWith(splitSizeUpdateAction({ largerExploreId: undefined })); + expect(result.current.widthCalc).toBe(512); + }); + + // 2. Left pane is larger + act(() => { + result.current.updateSplitSize(300); + }); + expect(dispatchMock).toHaveBeenCalledWith(splitSizeUpdateAction({ largerExploreId: 'left' })); + expect(result.current.widthCalc).toBe(300); + + // 3. Right pane is larger + act(() => { + result.current.updateSplitSize(700); + }); + expect(dispatchMock).toHaveBeenCalledWith(splitSizeUpdateAction({ largerExploreId: 'right' })); + expect(result.current.widthCalc).toBe(700); + }); +}); diff --git a/public/app/features/explore/hooks/useSplitSizeUpdater.ts b/public/app/features/explore/hooks/useSplitSizeUpdater.ts new file mode 100644 index 00000000000..ea2f6279b8a --- /dev/null +++ b/public/app/features/explore/hooks/useSplitSizeUpdater.ts @@ -0,0 +1,47 @@ +import { inRange } from 'lodash'; +import { useState } from 'react'; +import { useWindowSize } from 'react-use'; + +import { useDispatch, useSelector } from 'app/types'; + +import { splitSizeUpdateAction } from '../state/main'; +import { isSplit, selectPanesEntries } from '../state/selectors'; + +export const useSplitSizeUpdater = (minWidth: number) => { + const dispatch = useDispatch(); + const { width: windowWidth } = useWindowSize(); + const panes = useSelector(selectPanesEntries); + const hasSplit = useSelector(isSplit); + const [rightPaneWidthRatio, setRightPaneWidthRatio] = useState(0.5); + + const exploreState = useSelector((state) => state.explore); + + const updateSplitSize = (size: number) => { + const evenSplitWidth = windowWidth / 2; + const areBothSimilar = inRange(size, evenSplitWidth - 100, evenSplitWidth + 100); + if (areBothSimilar) { + dispatch(splitSizeUpdateAction({ largerExploreId: undefined })); + } else { + dispatch( + splitSizeUpdateAction({ + largerExploreId: size > evenSplitWidth ? panes[1][0] : panes[0][0], + }) + ); + } + + setRightPaneWidthRatio(size / windowWidth); + }; + + let widthCalc = 0; + if (hasSplit) { + if (!exploreState.evenSplitPanes && exploreState.maxedExploreId) { + widthCalc = exploreState.maxedExploreId === panes[1][0] ? windowWidth - minWidth : minWidth; + } else if (exploreState.evenSplitPanes) { + widthCalc = Math.floor(windowWidth / 2); + } else if (rightPaneWidthRatio !== undefined) { + widthCalc = windowWidth * rightPaneWidthRatio; + } + } + + return { updateSplitSize, widthCalc }; +}; diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 8f823fe9178..88426eade59 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -614,6 +614,94 @@ describe('reducer', () => { queryKeys: ['initialRow-0', 'mockKey-1'], } as unknown as ExploreItemState); }); + + describe('addQueryRow', () => { + it('adds a query from root datasource if root is not mixed and there are no queries', async () => { + const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({ + ...defaultInitialState, + explore: { + ...defaultInitialState.explore, + panes: { + left: { + ...defaultInitialState.explore.panes.left, + queries: [], + datasourceInstance: { + meta: { + mixed: false, + }, + getRef() { + return { type: 'loki', uid: 'uid-loki' }; + }, + }, + }, + }, + }, + } as unknown as Partial); + + await dispatch(addQueryRow('left', 0)); + + expect(getState().explore.panes.left?.queries).toEqual([ + expect.objectContaining({ datasource: { type: 'loki', uid: 'uid-loki' } }), + ]); + }); + + it('adds a query from root datasource if root is not mixed and there are queries without a datasource specified', async () => { + const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({ + ...defaultInitialState, + explore: { + panes: { + left: { + ...defaultInitialState.explore.panes.left, + queries: [{ expr: 1 }], + datasourceInstance: { + meta: { + mixed: false, + }, + getRef() { + return { type: 'loki', uid: 'uid-loki' }; + }, + }, + }, + }, + }, + } as unknown as Partial); + + await dispatch(addQueryRow('left', 0)); + + expect(getState().explore.panes.left?.queries).toEqual([ + expect.anything(), + expect.objectContaining({ datasource: { type: 'loki', uid: 'uid-loki' } }), + ]); + }); + + it('adds a query from default datasource if root is mixed and there are no queries', async () => { + const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({ + ...defaultInitialState, + explore: { + panes: { + left: { + ...defaultInitialState.explore.panes.left, + queries: [], + datasourceInstance: { + meta: { + mixed: true, + }, + getRef() { + return { type: 'mixed', uid: '-- Mixed --' }; + }, + }, + }, + }, + }, + } as unknown as Partial); + + await dispatch(addQueryRow('left', 0)); + + expect(getState().explore.panes.left?.queries).toEqual([ + expect.objectContaining({ datasource: { type: 'postgres', uid: 'ds1' } }), + ]); + }); + }); }); describe('caching', () => { diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index 9081c363ace..bb789d82639 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -1,6 +1,6 @@ import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit'; import deepEqual from 'fast-deep-equal'; -import { flatten, groupBy, head, map, mapValues, snakeCase, zipObject } from 'lodash'; +import { findLast, flatten, groupBy, head, map, mapValues, snakeCase, zipObject } from 'lodash'; import { combineLatest, identity, Observable, of, SubscriptionLike, Unsubscribable } from 'rxjs'; import { mergeMap, throttleTime } from 'rxjs/operators'; @@ -249,26 +249,24 @@ export const clearCacheAction = createAction('explore/clearCa /** * Adds a query row after the row with the given index. */ -export function addQueryRow(exploreId: string, index: number): ThunkResult { +export function addQueryRow(exploreId: string, index: number): ThunkResult> { return async (dispatch, getState) => { - const queries = getState().explore.panes[exploreId]!.queries; + const pane = getState().explore.panes[exploreId]!; let datasourceOverride = undefined; - // if this is the first query being added, check for a root datasource - // if it's not mixed, send it as an override. generateEmptyQuery doesn't have access to state - if (queries.length === 0) { - const rootDatasource = getState().explore.panes[exploreId]!.datasourceInstance; - if (!rootDatasource?.meta.mixed) { - datasourceOverride = rootDatasource; - } + // if we are not in mixed mode, use root datasource + if (!pane.datasourceInstance?.meta.mixed) { + datasourceOverride = pane.datasourceInstance?.getRef(); + } else { + // else try to get the datasource from the last query that defines one, falling back to the default datasource + datasourceOverride = findLast(pane.queries, (query) => !!query.datasource)?.datasource || undefined; } - const query = await generateEmptyQuery(queries, index, datasourceOverride?.getRef()); + const query = await generateEmptyQuery(pane.queries, index, datasourceOverride); dispatch(addQueryRowAction({ exploreId, index, query })); }; } - /** * Cancel running queries */ diff --git a/public/app/features/geo/utils/frameVectorSource.ts b/public/app/features/geo/utils/frameVectorSource.ts index 8f85f420af5..25cf679a44c 100644 --- a/public/app/features/geo/utils/frameVectorSource.ts +++ b/public/app/features/geo/utils/frameVectorSource.ts @@ -9,7 +9,7 @@ import { getGeometryField, LocationFieldMatchers } from './location'; export interface FrameVectorSourceOptions {} export class FrameVectorSource extends VectorSource { - constructor(private location: LocationFieldMatchers) { + constructor(public location: LocationFieldMatchers) { super({}); } diff --git a/public/app/features/logs/components/logParser.test.ts b/public/app/features/logs/components/logParser.test.ts index 4219c3dc8cb..27fcbe390f9 100644 --- a/public/app/features/logs/components/logParser.test.ts +++ b/public/app/features/logs/components/logParser.test.ts @@ -1,4 +1,4 @@ -import { FieldType, MutableDataFrame } from '@grafana/data'; +import { DataFrameType, Field, FieldType, LogRowModel, MutableDataFrame } from '@grafana/data'; import { ExploreFieldLinkModel } from 'app/features/explore/utils/links'; import { createLogRow } from './__mocks__/logRow'; @@ -199,6 +199,211 @@ describe('logParser', () => { expect(fields.length).toBe(1); expect(fields.find((field) => field.keys[0] === testStringField.name)).not.toBe(undefined); }); + + describe('dataplane frames', () => { + const makeLogRow = (fields: Field[], entryFieldIndex: number): LogRowModel => + createLogRow({ + entryFieldIndex, + rowIndex: 0, + dataFrame: { + refId: 'A', + fields, + length: fields[0]?.values.length, + meta: { + type: DataFrameType.LogLines, + }, + }, + }); + + const expectHasField = (defs: FieldDef[], name: string): void => { + expect(defs.find((field) => field.keys[0] === name)).not.toBe(undefined); + }; + + it('should filter out fields with data links that have a nullish value', () => { + const createScenario = (value: unknown) => + makeLogRow( + [ + testTimeField, + testLineField, + { + name: 'link', + type: FieldType.string, + config: { + links: [ + { + title: 'link1', + url: 'https://example.com', + }, + ], + }, + values: [value], + }, + ], + 1 + ); + + expect(getAllFields(createScenario(null))).toHaveLength(0); + expect(getAllFields(createScenario(undefined))).toHaveLength(0); + expect(getAllFields(createScenario(''))).toHaveLength(1); + expect(getAllFields(createScenario('test'))).toHaveLength(1); + // technically this is a field-type-string, but i will add more + // falsy-values, just to be sure + expect(getAllFields(createScenario(false))).toHaveLength(1); + expect(getAllFields(createScenario(NaN))).toHaveLength(1); + expect(getAllFields(createScenario(0))).toHaveLength(1); + expect(getAllFields(createScenario(-0))).toHaveLength(1); + }); + + it('should filter out system-fields without data-links, but should keep severity', () => { + const row = makeLogRow( + [ + testTimeField, + testLineField, + { + config: {}, + name: 'id', + type: FieldType.string, + values: ['id1'], + }, + { + config: {}, + name: 'attributes', + type: FieldType.other, + values: [{ a: 1, b: 2 }], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['info'], + }, + testStringField, + ], + 1 + ); + + const output = getAllFields(row); + + expect(output).toHaveLength(2); + expectHasField(output, 'test_field_string'); + expectHasField(output, 'severity'); + }); + + it('should keep system fields with data-links', () => { + const links = [ + { + title: 'link1', + url: 'https://example.com', + }, + ]; + + const row = makeLogRow( + [ + { + ...testTimeField, + config: { links }, + }, + { + ...testLineField, + config: { links }, + }, + { + config: { links }, + name: 'id', + type: FieldType.string, + values: ['id1'], + }, + { + config: { links }, + name: 'attributes', + type: FieldType.other, + values: [{ a: 1, b: 2 }], + }, + { + config: { links }, + name: 'severity', + type: FieldType.string, + values: ['info'], + }, + ], + 1 + ); + + const output = getAllFields(row); + + expect(output).toHaveLength(5); + expectHasField(output, 'timestamp'); + expectHasField(output, 'body'); + expectHasField(output, 'id'); + expectHasField(output, 'attributes'); + expectHasField(output, 'severity'); + }); + + it('should filter out config-hidden fields', () => { + const row = makeLogRow( + [ + testTimeField, + testLineField, + { + ...testStringField, + config: { + custom: { + hidden: true, + }, + }, + }, + ], + 1 + ); + + const output = getAllFields(row); + + expect(output).toHaveLength(0); + }); + + it('should filter out fields with null values', () => { + const row = makeLogRow( + [ + testTimeField, + testLineField, + { + // null-value + config: {}, + type: FieldType.string, + name: 'test1', + values: [null], + }, + { + // null-value and data-link + config: { + links: [ + { + title: 'link1', + url: 'https://example.com', + }, + ], + }, + type: FieldType.string, + name: 'test2', + values: [null], + }, + { + // normal value + config: {}, + type: FieldType.string, + name: 'test3', + values: ['testvalue'], + }, + ], + 1 + ); + + const output = getAllFields(row); + + expect(output).toHaveLength(1); + expectHasField(output, 'test3'); + }); + }); }); describe('createLogLineLinks', () => { diff --git a/public/app/features/logs/components/logParser.ts b/public/app/features/logs/components/logParser.ts index 63344030c3c..7e541ca3e53 100644 --- a/public/app/features/logs/components/logParser.ts +++ b/public/app/features/logs/components/logParser.ts @@ -1,9 +1,12 @@ +import { partition } from 'lodash'; import memoizeOne from 'memoize-one'; -import { DataFrame, Field, FieldType, LinkModel, LogRowModel } from '@grafana/data'; +import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel } from '@grafana/data'; import { safeStringifyValue } from 'app/core/utils/explore'; import { ExploreFieldLinkModel } from 'app/features/explore/utils/links'; +import { parseLogsFrame } from '../logsFrame'; + export type FieldDef = { keys: string[]; values: string[]; @@ -65,76 +68,83 @@ export const getDataframeFields = memoizeOne( row: LogRowModel, getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> ): FieldDef[] => { - return row.dataFrame.fields - .map((field, index) => ({ ...field, index })) - .filter((field, index) => !shouldRemoveField(field, index, row)) - .map((field) => { - const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; - const fieldVal = field.values[row.rowIndex]; - const outputVal = - typeof fieldVal === 'string' || typeof fieldVal === 'number' - ? fieldVal.toString() - : safeStringifyValue(fieldVal); - return { - keys: [field.name], - values: [outputVal], - links: links, - fieldIndex: field.index, - }; - }); + const visibleFields = separateVisibleFields(row.dataFrame).visible; + const nonEmptyVisibleFields = visibleFields.filter((f) => f.values[row.rowIndex] != null); + return nonEmptyVisibleFields.map((field) => { + const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; + const fieldVal = field.values[row.rowIndex]; + const outputVal = + typeof fieldVal === 'string' || typeof fieldVal === 'number' + ? fieldVal.toString() + : safeStringifyValue(fieldVal); + return { + keys: [field.name], + values: [outputVal], + links: links, + fieldIndex: field.index, + }; + }); } ); -export function shouldRemoveField( - field: Field, - index: number, - row: LogRowModel, - shouldRemoveLine = true, - shouldRemoveTime = true -) { - // field that has empty value (we want to keep 0 or empty string) - if (field.values[row.rowIndex] == null) { - return true; +type VisOptions = { + keepTimestamp?: boolean; + keepBody?: boolean; +}; + +// return the fields (their indices to be exact) that should be visible +// based on the logs dataframe structure +function getVisibleFieldIndices(frame: DataFrame, opts: VisOptions): Set { + const logsFrame = parseLogsFrame(frame); + if (logsFrame === null) { + // should not really happen + return new Set(); } - // hidden field, remove - if (field.config.custom?.hidden) { - return true; + // we want to show every "extra" field + const visibleFieldIndices = new Set(logsFrame.extraFields.map((f) => f.index)); + + // we always show the severity field + if (logsFrame.severityField !== null) { + visibleFieldIndices.add(logsFrame.severityField.index); } - // field with data-links, keep - if ((field.config.links ?? []).length > 0) { - return false; - } - // the remaining checks use knowledge of how we parse logs-dataframes - - // Remove field if it is: - // "labels" field that is in Loki used to store all labels - if (field.name === 'labels' && field.type === FieldType.other) { - return true; - } - // id and tsNs are arbitrary added fields in the backend and should be hidden in the UI - if (field.name === 'id' || field.name === 'tsNs') { - return true; - } - if (shouldRemoveTime) { - const firstTimeField = row.dataFrame.fields.find((f) => f.type === FieldType.time); - if ( - field.name === firstTimeField?.name && - field.type === FieldType.time && - field.values[0] === firstTimeField.values[0] - ) { - return true; - } + if (opts.keepBody) { + visibleFieldIndices.add(logsFrame.bodyField.index); } - if (shouldRemoveLine) { - // first string-field is the log-line - const firstStringFieldIndex = row.dataFrame.fields.findIndex((f) => f.type === FieldType.string); - if (firstStringFieldIndex === index) { - return true; - } + if (opts.keepTimestamp) { + visibleFieldIndices.add(logsFrame.timeField.index); } - return false; + return visibleFieldIndices; +} + +// split the dataframe's fields into visible and hidden arrays. +// note: does not do any row-level checks, +// for example does not check if the field's values are nullish +// or not at a givn row. +export function separateVisibleFields( + frame: DataFrame, + opts?: VisOptions +): { visible: FieldWithIndex[]; hidden: FieldWithIndex[] } { + const fieldsWithIndex: FieldWithIndex[] = frame.fields.map((field, index) => ({ ...field, index })); + + const visibleFieldIndices = getVisibleFieldIndices(frame, opts ?? {}); + + const [visible, hidden] = partition(fieldsWithIndex, (f) => { + // hidden fields are always hidden + if (f.config.custom?.hidden) { + return false; + } + + // fields with data-links are visible + if ((f.config.links ?? []).length > 0) { + return true; + } + + return visibleFieldIndices.has(f.index); + }); + + return { visible, hidden }; } diff --git a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx index 1e735327472..e76d552b614 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { selectors } from '@grafana/e2e-selectors'; -import { DataSourcePicker } from '@grafana/runtime'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { Button, @@ -15,6 +14,7 @@ import { Legend, } from '@grafana/ui'; import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DashboardInput, @@ -118,6 +118,7 @@ export const ImportDashboardForm = ({ return ( { it('Should send data source uid', async () => { const form: ImportDashboardDTO = { @@ -107,3 +117,643 @@ describe('validateDashboardJson', () => { expect(validateDashboardJsonNotValid).toBe('Not valid JSON'); }); }); + +describe('processDashboard', () => { + const panel = new PanelModel({ + datasource: { + type: 'mysql', + uid: '${DS_GDEV-MYSQL}', + }, + }); + + const panelWithLibPanel = { + gridPos: { + h: 8, + w: 12, + x: 0, + y: 8, + }, + id: 3, + libraryPanel: { + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + name: 'another prom lib panel', + }, + }; + const libPanel = { + 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90': { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + kind: 1, + model: { + datasource: { + type: 'prometheus', + uid: '${DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL}', + }, + description: '', + fieldConfig: { + defaults: { + color: { + mode: 'palette-classic', + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 0, + gradientMode: 'none', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + libraryPanel: { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'single', + sort: 'none', + }, + }, + targets: [ + { + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + editorMode: 'builder', + expr: 'access_evaluation_duration_bucket', + instant: false, + range: true, + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'timeseries', + }, + }, + }; + + const panelWithSecondLibPanel = { + gridPos: { + h: 8, + w: 12, + x: 0, + y: 16, + }, + id: 1, + libraryPanel: { + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + name: 'Testing lib panel', + }, + }; + const secondLibPanel = { + 'c46a6b49-de40-43b3-982c-1b5e1ec084a4': { + name: 'Testing lib panel', + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + kind: 1, + model: { + datasource: { + type: 'prometheus', + uid: '${DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL}', + }, + description: '', + fieldConfig: { + defaults: { + color: { + mode: 'palette-classic', + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 0, + gradientMode: 'none', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + libraryPanel: { + name: 'Testing lib panel', + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'single', + sort: 'none', + }, + }, + targets: [ + { + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + editorMode: 'builder', + expr: 'access_evaluation_duration_count', + instant: false, + range: true, + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'timeseries', + }, + }, + }; + + const importedJson: DashboardJson = { + ...defaultDashboard, + __inputs: [ + { + name: 'DS_GDEV-MYSQL', + label: 'gdev-mysql', + description: '', + type: 'datasource', + value: '', + }, + { + name: 'DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL', + label: 'gdev-prometheus', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + }, + ], + }, + }, + ], + __elements: { + ...libPanel, + }, + __requires: [ + { + type: 'grafana', + id: 'grafana', + name: 'Grafana', + version: '10.1.0-pre', + }, + { + type: 'datasource', + id: 'mysql', + name: 'MySQL', + version: '1.0.0', + }, + { + type: 'datasource', + id: 'prometheus', + name: 'Prometheus', + version: '1.0.0', + }, + { + type: 'panel', + id: 'table', + name: 'Table', + version: '', + }, + ], + panels: [], + }; + + it("Should return 2 inputs, 1 for library panel because it's used for 2 panels", async () => { + mocks.getLibraryPanel.mockImplementation(() => { + throw { status: 404 }; + }); + const importDashboardState = initialImportDashboardState; + const dashboardJson: DashboardJson = { + ...importedJson, + panels: [panel, panelWithLibPanel, panelWithLibPanel], + }; + const libPanelInputs = await getLibraryPanelInputs(dashboardJson); + const newDashboardState = { + ...importDashboardState, + inputs: { + ...importDashboardState.inputs, + libraryPanels: libPanelInputs!, + }, + }; + + const processedDashboard = processDashboard(dashboardJson, newDashboardState); + const dsInputsForLibPanels = processedDashboard.__inputs!.filter((input) => !!input.usage?.libraryPanels); + expect(processedDashboard.__inputs).toHaveLength(2); + expect(dsInputsForLibPanels).toHaveLength(1); + }); + it('Should return 3 inputs, 2 for library panels', async () => { + mocks.getLibraryPanel.mockImplementation(() => { + throw { status: 404 }; + }); + const importDashboardState = initialImportDashboardState; + const dashboardJson: DashboardJson = { + ...importedJson, + __inputs: [ + { + name: 'DS_GDEV-MYSQL', + label: 'gdev-mysql', + description: '', + type: 'datasource', + value: '', + }, + { + name: 'DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL', + label: 'gdev-prometheus', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + }, + ], + }, + }, + { + name: 'DS_GDEV-MYSQL-FOR-LIBRARY-PANEL', + label: 'gdev-mysql-2', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + name: 'Testing lib panel', + }, + ], + }, + }, + ], + __elements: { + ...libPanel, + ...secondLibPanel, + }, + panels: [panel, panelWithLibPanel, panelWithSecondLibPanel], + }; + const libPanelInputs = await getLibraryPanelInputs(dashboardJson); + const newDashboardState = { + ...importDashboardState, + inputs: { + ...importDashboardState.inputs, + libraryPanels: libPanelInputs!, + }, + }; + + const processedDashboard = processDashboard(dashboardJson, newDashboardState); + const dsInputsForLibPanels = processedDashboard.__inputs!.filter((input) => !!input.usage?.libraryPanels); + expect(processedDashboard.__inputs).toHaveLength(3); + expect(dsInputsForLibPanels).toHaveLength(2); + }); + + it('Should return 1 input, since library panels already exist in the instance', async () => { + const getLibPanelFirstRS: LibraryElementDTO = { + folderUid: '', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + name: 'another prom lib panel', + type: 'timeseries', + description: '', + model: { + transparent: false, + transformations: [], + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + description: '', + fieldConfig: { + defaults: { + color: { + mode: FieldColorModeId.PaletteClassic, + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 0, + gradientMode: 'none', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'single', + sort: 'none', + }, + }, + targets: [ + { + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + editorMode: 'builder', + expr: 'access_evaluation_duration_bucket', + instant: false, + range: true, + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'timeseries', + }, + version: 1, + }; + + const getLibPanelSecondRS: LibraryElementDTO = { + folderUid: '', + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + name: 'Testing lib panel', + type: 'timeseries', + description: '', + model: { + transparent: false, + transformations: [], + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + description: '', + fieldConfig: { + defaults: { + color: { + mode: FieldColorModeId.PaletteClassic, + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 0, + gradientMode: 'none', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'single', + sort: 'none', + }, + }, + targets: [ + { + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + editorMode: 'builder', + expr: 'access_evaluation_duration_count', + instant: false, + range: true, + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'timeseries', + }, + version: 1, + }; + mocks.getLibraryPanel + .mockReturnValueOnce(Promise.resolve(getLibPanelFirstRS)) + .mockReturnValueOnce(Promise.resolve(getLibPanelSecondRS)); + + const importDashboardState = initialImportDashboardState; + const dashboardJson: DashboardJson = { + ...importedJson, + __inputs: [ + { + name: 'DS_GDEV-MYSQL', + label: 'gdev-mysql', + description: '', + type: 'datasource', + value: '', + }, + { + name: 'DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL', + label: 'gdev-prometheus', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + }, + ], + }, + }, + { + name: 'DS_GDEV-MYSQL-FOR-LIBRARY-PANEL', + label: 'gdev-mysql-2', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + name: 'Testing lib panel', + }, + ], + }, + }, + ], + __elements: { + ...libPanel, + ...secondLibPanel, + }, + panels: [panel, panelWithLibPanel, panelWithSecondLibPanel], + }; + const libPanelInputs = await getLibraryPanelInputs(dashboardJson); + const newDashboardState = { + ...importDashboardState, + inputs: { + ...importDashboardState.inputs, + libraryPanels: libPanelInputs!, + }, + }; + + const processedDashboard = processDashboard(dashboardJson, newDashboardState); + const dsInputsForLibPanels = processedDashboard.__inputs!.filter((input) => !!input.usage?.libraryPanels); + expect(processedDashboard.__inputs).toHaveLength(1); + expect(dsInputsForLibPanels).toHaveLength(0); + }); +}); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index c244a5193b8..5474afacf21 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,22 +1,28 @@ import { DataSourceInstanceSettings, locationUtil } from '@grafana/data'; -import { getDataSourceSrv, locationService, getBackendSrv, isFetchError } from '@grafana/runtime'; +import { getBackendSrv, getDataSourceSrv, isFetchError, locationService } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DashboardDTO, FolderInfo, PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types'; -import { LibraryElementExport } from '../../dashboard/components/DashExportModal/DashboardExporter'; +import { + Input, + InputUsage, + LibraryElementExport, + LibraryPanel, +} from '../../dashboard/components/DashExportModal/DashboardExporter'; import { getLibraryPanel } from '../../library-panels/state/api'; import { LibraryElementDTO, LibraryElementKind } from '../../library-panels/types'; import { DashboardSearchHit } from '../../search/types'; -import { DeleteDashboardResponse } from '../types'; +import { DashboardJson, DeleteDashboardResponse } from '../types'; import { clearDashboard, fetchDashboard, fetchFailed, ImportDashboardDTO, + ImportDashboardState, InputType, LibraryPanelInput, LibraryPanelInputState, @@ -31,9 +37,9 @@ export function fetchGcomDashboard(id: string): ThunkResult { try { dispatch(fetchDashboard()); const dashboard = await getBackendSrv().get(`/api/gnet/dashboards/${id}`); - dispatch(setGcomDashboard(dashboard)); - dispatch(processInputs(dashboard.json)); - dispatch(processElements(dashboard.json)); + await dispatch(processElements(dashboard.json)); + await dispatch(processGcomDashboard(dashboard)); + dispatch(processInputs()); } catch (error) { dispatch(fetchFailed()); if (isFetchError(error)) { @@ -45,17 +51,66 @@ export function fetchGcomDashboard(id: string): ThunkResult { export function importDashboardJson(dashboard: any): ThunkResult { return async (dispatch) => { - dispatch(setJsonDashboard(dashboard)); - dispatch(processInputs(dashboard)); - dispatch(processElements(dashboard)); + await dispatch(processElements(dashboard)); + await dispatch(processJsonDashboard(dashboard)); + dispatch(processInputs()); }; } -function processInputs(dashboardJson: any): ThunkResult { - return (dispatch) => { - if (dashboardJson && dashboardJson.__inputs) { +const getNewLibraryPanelsByInput = (input: Input, state: ImportDashboardState): LibraryPanel[] | undefined => { + return input?.usage?.libraryPanels?.filter((usageLibPanel) => + state.inputs.libraryPanels.some( + (libPanel) => libPanel.state !== LibraryPanelInputState.Exists && libPanel.model.uid === usageLibPanel.uid + ) + ); +}; + +export function processDashboard(dashboardJson: DashboardJson, state: ImportDashboardState): DashboardJson { + let inputs = dashboardJson.__inputs; + if (!!state.inputs.libraryPanels?.length) { + const filteredUsedInputs: Input[] = []; + dashboardJson.__inputs?.forEach((input: Input) => { + if (!input?.usage?.libraryPanels) { + filteredUsedInputs.push(input); + return; + } + + const newLibraryPanels = getNewLibraryPanelsByInput(input, state); + input.usage = { libraryPanels: newLibraryPanels }; + + const isInputBeingUsedByANewLibraryPanel = !!newLibraryPanels?.length; + if (isInputBeingUsedByANewLibraryPanel) { + filteredUsedInputs.push(input); + } + }); + inputs = filteredUsedInputs; + } + + return { ...dashboardJson, __inputs: inputs }; +} + +function processGcomDashboard(dashboard: { json: DashboardJson }): ThunkResult { + return (dispatch, getState) => { + const state = getState().importDashboard; + const dashboardJson = processDashboard(dashboard.json, state); + dispatch(setGcomDashboard({ ...dashboard, json: dashboardJson })); + }; +} + +function processJsonDashboard(dashboardJson: DashboardJson): ThunkResult { + return (dispatch, getState) => { + const state = getState().importDashboard; + const dashboard = processDashboard(dashboardJson, state); + dispatch(setJsonDashboard(dashboard)); + }; +} + +function processInputs(): ThunkResult { + return (dispatch, getState) => { + const dashboard = getState().importDashboard.dashboard; + if (dashboard && dashboard.__inputs) { const inputs: any[] = []; - dashboardJson.__inputs.forEach((input: any) => { + dashboard.__inputs.forEach((input: any) => { const inputModel: any = { name: input.name, label: input.label, @@ -66,6 +121,8 @@ function processInputs(dashboardJson: any): ThunkResult { options: [], }; + inputModel.description = getDataSourceDescription(input); + if (input.type === InputType.DataSource) { getDataSourceOptions(input, inputModel); } else if (!inputModel.info) { @@ -81,50 +138,57 @@ function processInputs(dashboardJson: any): ThunkResult { function processElements(dashboardJson?: { __elements?: Record }): ThunkResult { return async function (dispatch) { - if (!dashboardJson || !dashboardJson.__elements) { - return; - } - - const libraryPanelInputs: LibraryPanelInput[] = []; - - for (const element of Object.values(dashboardJson.__elements)) { - if (element.kind !== LibraryElementKind.Panel) { - continue; - } - - const model = element.model; - const { type, description } = model; - const { uid, name } = element; - const input: LibraryPanelInput = { - model: { - model, - uid, - name, - version: 0, - type, - kind: LibraryElementKind.Panel, - description, - } as LibraryElementDTO, - state: LibraryPanelInputState.New, - }; - - try { - const panelInDb = await getLibraryPanel(uid, true); - input.state = LibraryPanelInputState.Exists; - input.model = panelInDb; - } catch (e: any) { - if (e.status !== 404) { - throw e; - } - } - - libraryPanelInputs.push(input); - } - + const libraryPanelInputs = await getLibraryPanelInputs(dashboardJson); dispatch(setLibraryPanelInputs(libraryPanelInputs)); }; } +export async function getLibraryPanelInputs(dashboardJson?: { + __elements?: Record; +}): Promise { + if (!dashboardJson || !dashboardJson.__elements) { + return []; + } + + const libraryPanelInputs: LibraryPanelInput[] = []; + + for (const element of Object.values(dashboardJson.__elements)) { + if (element.kind !== LibraryElementKind.Panel) { + continue; + } + + const model = element.model; + const { type, description } = model; + const { uid, name } = element; + const input: LibraryPanelInput = { + model: { + model, + uid, + name, + version: 0, + type, + kind: LibraryElementKind.Panel, + description, + } as LibraryElementDTO, + state: LibraryPanelInputState.New, + }; + + try { + const panelInDb = await getLibraryPanel(uid, true); + input.state = LibraryPanelInputState.Exists; + input.model = panelInDb; + } catch (e: any) { + if (e.status !== 404) { + throw e; + } + } + + libraryPanelInputs.push(input); + } + + return libraryPanelInputs; +} + export function clearLoadedDashboard(): ThunkResult { return (dispatch) => { dispatch(clearDashboard()); @@ -182,6 +246,22 @@ const getDataSourceOptions = (input: { pluginId: string; pluginName: string }, i } }; +const getDataSourceDescription = (input: { usage?: InputUsage }): string | undefined => { + if (!input.usage) { + return undefined; + } + + if (input.usage.libraryPanels) { + const libPanelNames = input.usage.libraryPanels.reduce( + (acc: string, libPanel, index) => (index === 0 ? libPanel.name : `${acc}, ${libPanel.name}`), + '' + ); + return `List of affected library panels: ${libPanelNames}`; + } + + return undefined; +}; + export async function moveFolders(folderUIDs: string[], toFolder: FolderInfo) { const result = { totalCount: folderUIDs.length, diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index 97f05697fa4..bea14221a51 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -34,6 +34,7 @@ export enum LibraryPanelInputState { export interface DashboardInput { name: string; label: string; + description?: string; info: string; value: string; type: InputType; @@ -100,7 +101,7 @@ const importDashboardSlice = createSlice({ state.inputs = { dataSources: action.payload.filter((p) => p.type === InputType.DataSource), constants: action.payload.filter((p) => p.type === InputType.Constant), - libraryPanels: [], + libraryPanels: state.inputs.libraryPanels || [], }; }, setLibraryPanelInputs: (state: Draft, action: PayloadAction) => { diff --git a/public/app/features/manage-dashboards/types.ts b/public/app/features/manage-dashboards/types.ts index 0b24c223403..48bd056afb7 100644 --- a/public/app/features/manage-dashboards/types.ts +++ b/public/app/features/manage-dashboards/types.ts @@ -1,3 +1,7 @@ +import { Dashboard } from '@grafana/schema/src/veneer/dashboard.types'; + +import { ExternalDashboard } from '../dashboard/components/DashExportModal/DashboardExporter'; + export interface Snapshot { created: string; expires: string; @@ -36,3 +40,5 @@ export interface PublicDashboardListResponse { export interface PublicDashboardListWithPagination extends PublicDashboardListWithPaginationResponse { totalPages: number; } + +export type DashboardJson = ExternalDashboard & Omit; diff --git a/public/app/features/plugins/extensions/getPluginExtensions.test.ts b/public/app/features/plugins/extensions/getPluginExtensions.test.ts index 0ad86c5f080..ce106bce5a5 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.test.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.test.ts @@ -134,18 +134,30 @@ describe('getPluginExtensions()', () => { expect(extension.category).toBe('Machine Learning'); }); - test('should hide the extension if it tries to override not-allowed properties with the configure() function', () => { + test('should ignore restricted properties passed via the configure() function', () => { link2.configure = jest.fn().mockImplementation(() => ({ // The following props are not allowed to override type: 'unknown-type', pluginId: 'another-plugin', + + // Unknown properties + testing: false, + + // The following props are allowed to override + title: 'test', })); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); + const [extension] = extensions; expect(link2.configure).toHaveBeenCalledTimes(1); - expect(extensions).toHaveLength(0); + expect(extensions).toHaveLength(1); + expect(extension.title).toBe('test'); + expect(extension.type).toBe('link'); + expect(extension.pluginId).toBe('grafana-basic-app'); + //@ts-ignore + expect(extension.testing).toBeUndefined(); }); test('should pass a read only context to the configure() function', () => { const context = { title: 'New title from the context!' }; diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index e878989ebf1..71acd6a365b 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -139,10 +139,10 @@ function getLinkExtensionOverrides(pluginId: string, config: PluginExtensionLink assertStringProps({ title, description }, ['title', 'description']); if (Object.keys(rest).length > 0) { - throw new Error( - `Invalid extension "${config.title}". Trying to override not-allowed properties: ${Object.keys(rest).join( + logWarning( + `Extension "${config.title}", is trying to override restricted properties: ${Object.keys(rest).join( ', ' - )}` + )} which will be ignored.` ); } diff --git a/public/app/features/query/components/QueryEditorRowHeader.test.tsx b/public/app/features/query/components/QueryEditorRowHeader.test.tsx index c94fb627b03..a3fdae682bf 100644 --- a/public/app/features/query/components/QueryEditorRowHeader.test.tsx +++ b/public/app/features/query/components/QueryEditorRowHeader.test.tsx @@ -83,7 +83,7 @@ describe('QueryEditorRowHeader', () => { it('should render variables in the data source picker', async () => { renderScenario({ onChangeDataSource: () => {} }); - const dsSelect = screen.getByLabelText(selectors.components.DataSourcePicker.inputV2); + const dsSelect = screen.getByTestId(selectors.components.DataSourcePicker.container).querySelector('input')!; openMenu(dsSelect); expect(await screen.findByText('${dsVariable}')).toBeInTheDocument(); }); diff --git a/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx new file mode 100644 index 00000000000..4a39104b6fd --- /dev/null +++ b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx @@ -0,0 +1,100 @@ +import React, { useCallback, ChangeEvent } from 'react'; + +import { + DataTransformerID, + SelectableValue, + standardTransformers, + TransformerRegistryItem, + TransformerUIProps, + getFieldDisplayName, + PluginState, +} from '@grafana/data'; +import { FormatTimeTransformerOptions } from '@grafana/data/src/transformations/transformers/formatTime'; +import { Select, InlineFieldRow, InlineField, Input, InlineSwitch } from '@grafana/ui'; + +export function FormatTimeTransfomerEditor({ + input, + options, + onChange, +}: TransformerUIProps) { + const timeFields: Array> = []; + + // Get time fields + for (const frame of input) { + for (const field of frame.fields) { + if (field.type === 'time') { + const name = getFieldDisplayName(field, frame, input); + timeFields.push({ label: name, value: name }); + } + } + } + + const onSelectField = useCallback( + (value: SelectableValue) => { + const val = value?.value !== undefined ? value.value : ''; + onChange({ + ...options, + timeField: val, + }); + }, + [onChange, options] + ); + + const onFormatChange = useCallback( + (e: ChangeEvent) => { + const val = e.target.value; + onChange({ + ...options, + outputFormat: val, + }); + }, + [onChange, options] + ); + + const onUseTzChange = useCallback(() => { + onChange({ + ...options, + useTimezone: !options.useTimezone, + }); + }, [onChange, options]); + + return ( + <> + + + + + + + + + + ); +} + +export const formatTimeTransformerRegistryItem: TransformerRegistryItem = { + id: DataTransformerID.formatTime, + editor: FormatTimeTransfomerEditor, + transformation: standardTransformers.formatTimeTransformer, + name: standardTransformers.formatTimeTransformer.name, + state: PluginState.alpha, + description: standardTransformers.formatTimeTransformer.description, +}; diff --git a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx index 8f6bdd434a6..1853af3e315 100644 --- a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx @@ -14,8 +14,19 @@ import { Select, InlineFieldRow, InlineField } from '@grafana/ui'; import { useAllFieldNamesFromDataFrames } from '../utils'; const modes = [ - { value: JoinMode.outer, label: 'OUTER', description: 'Keep all rows from any table with a value' }, - { value: JoinMode.inner, label: 'INNER', description: 'Drop rows that do not match a value in all tables' }, + { + value: JoinMode.outer, + label: 'OUTER (TIME SERIES)', + description: + 'Keep all rows from any table with a value. Join on distinct field values. Performant and best used for time series.', + }, + { + value: JoinMode.outerTabular, + label: 'OUTER (TABULAR)', + description: + 'Join on a field value with dupicated values. Non performant outer join best used for tabular(SQL like) data.', + }, + { value: JoinMode.inner, label: 'INNER', description: 'Drop rows that do not match a value in all tables.' }, ]; export function SeriesToFieldsTransformerEditor({ input, options, onChange }: TransformerUIProps) { diff --git a/public/app/features/transformers/standardTransformers.ts b/public/app/features/transformers/standardTransformers.ts index 8716a48a20a..5a188e091e0 100644 --- a/public/app/features/transformers/standardTransformers.ts +++ b/public/app/features/transformers/standardTransformers.ts @@ -9,6 +9,7 @@ import { concatenateTransformRegistryItem } from './editors/ConcatenateTransform import { convertFieldTypeTransformRegistryItem } from './editors/ConvertFieldTypeTransformerEditor'; import { filterFieldsByNameTransformRegistryItem } from './editors/FilterByNameTransformerEditor'; import { filterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefIdTransformerEditor'; +import { formatTimeTransformerRegistryItem } from './editors/FormatTimeTransformerEditor'; import { groupByTransformRegistryItem } from './editors/GroupByTransformerEditor'; import { groupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor'; import { histogramTransformRegistryItem } from './editors/HistogramTransformerEditor'; @@ -59,6 +60,7 @@ export const getStandardTransformers = (): Array> = limitTransformRegistryItem, joinByLabelsTransformRegistryItem, partitionByValuesTransformRegistryItem, + formatTimeTransformerRegistryItem, ...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []), ]; }; diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx index 66caf9b4cbc..cc4ae6e0bfb 100644 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx +++ b/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx @@ -60,12 +60,14 @@ describe('AdHocVariableEditor', () => { it('has a datasource select menu', async () => { render(); - expect(await screen.findByLabelText(selectors.components.DataSourcePicker.inputV2)).toBeInTheDocument(); + expect(await screen.getByTestId(selectors.components.DataSourcePicker.container)).toBeInTheDocument(); }); it('calls the callback when changing the datasource', async () => { render(); - const selectEl = screen.getByLabelText(selectors.components.DataSourcePicker.inputV2); + const selectEl = screen + .getByTestId(selectors.components.DataSourcePicker.container) + .getElementsByTagName('input')[0]; await selectOptionInTest(selectEl, 'Loki'); expect(props.changeVariableDatasource).toBeCalledWith( diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx index c5f02f0d712..659fd7869c5 100644 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx +++ b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx @@ -2,8 +2,8 @@ import React, { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; import { Alert, Field } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { StoreState } from 'app/types'; import { VariableLegend } from '../editor/VariableLegend'; diff --git a/public/app/features/variables/query/QueryVariableEditor.tsx b/public/app/features/variables/query/QueryVariableEditor.tsx index 50bd589a9e5..1077ceb1eb8 100644 --- a/public/app/features/variables/query/QueryVariableEditor.tsx +++ b/public/app/features/variables/query/QueryVariableEditor.tsx @@ -3,8 +3,9 @@ import { connect, ConnectedProps } from 'react-redux'; import { DataSourceInstanceSettings, getDataSourceRef, LoadingState, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { DataSourcePicker, getTemplateSrv } from '@grafana/runtime'; +import { getTemplateSrv } from '@grafana/runtime'; import { Field } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { StoreState } from '../../../types'; import { getTimeSrv } from '../../dashboard/services/TimeSrv'; diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts index b8ff117bc1d..293b4850d3d 100644 --- a/public/app/plugins/datasource/alertmanager/types.ts +++ b/public/app/plugins/datasource/alertmanager/types.ts @@ -79,20 +79,22 @@ export type GrafanaManagedReceiverConfig = { provenance?: string; }; -export type Receiver = { +export interface GrafanaManagedContactPoint { + name: string; + grafana_managed_receiver_configs?: GrafanaManagedReceiverConfig[]; +} + +export interface AlertmanagerReceiver { name: string; email_configs?: EmailConfig[]; - pagerduty_configs?: any[]; - pushover_configs?: any[]; - slack_configs?: any[]; - opsgenie_configs?: any[]; webhook_configs?: WebhookConfig[]; - victorops_configs?: any[]; - wechat_configs?: any[]; - grafana_managed_receiver_configs?: GrafanaManagedReceiverConfig[]; - [key: string]: any; -}; + + // this is supposedly to support any *_configs + [key: `${string}_configs`]: any[] | undefined; +} + +export type Receiver = GrafanaManagedContactPoint | AlertmanagerReceiver; export type ObjectMatcher = [name: string, operator: MatcherOperator, value: string]; diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx index 0547882c7d2..de3b4cfd9db 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx @@ -258,10 +258,12 @@ describe('VariableEditor:', () => { await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); await selectAndRerender('select query type', 'Resource Names', onChange, rerender); await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + await selectAndRerender('select region', 'North Europe', onChange, rerender); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ queryType: AzureQueryType.ResourceNamesQuery, subscription: 'sub', + region: 'northeurope', refId: 'A', }) ); @@ -323,5 +325,21 @@ describe('VariableEditor:', () => { }) ); }); + + it('should run the query if requesting regions', async () => { + const onChange = jest.fn(); + const { rerender } = render(); + // wait for initial load + await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); + await selectAndRerender('select query type', 'Regions', onChange, rerender); + await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + queryType: AzureQueryType.LocationsQuery, + subscription: 'sub', + refId: 'A', + }) + ); + }); }); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx index f9dce486d07..321cc7a887f 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx @@ -30,6 +30,7 @@ const VariableEditor = (props: Props) => { { label: 'Subscriptions', value: AzureQueryType.SubscriptionsQuery }, { label: 'Resource Groups', value: AzureQueryType.ResourceGroupsQuery }, { label: 'Namespaces', value: AzureQueryType.NamespacesQuery }, + { label: 'Regions', value: AzureQueryType.LocationsQuery }, { label: 'Resource Names', value: AzureQueryType.ResourceNamesQuery }, { label: 'Metric Names', value: AzureQueryType.MetricNamesQuery }, { label: 'Workspaces', value: AzureQueryType.WorkspacesQuery }, @@ -50,6 +51,7 @@ const VariableEditor = (props: Props) => { const [requireSubscription, setRequireSubscription] = useState(false); const [hasResourceGroup, setHasResourceGroup] = useState(false); const [hasNamespace, setHasNamespace] = useState(false); + const [hasRegion, setHasRegion] = useState(false); const [requireResourceGroup, setRequireResourceGroup] = useState(false); const [requireNamespace, setRequireNamespace] = useState(false); const [requireResource, setRequireResource] = useState(false); @@ -57,6 +59,7 @@ const VariableEditor = (props: Props) => { const [resourceGroups, setResourceGroups] = useState([]); const [namespaces, setNamespaces] = useState([]); const [resources, setResources] = useState([]); + const [regions, setRegions] = useState([]); const [errorMessage, setError] = useLastError(); const queryType = typeof query === 'string' ? '' : query.queryType; @@ -88,6 +91,7 @@ const VariableEditor = (props: Props) => { setRequireSubscription(true); setHasResourceGroup(true); setHasNamespace(true); + setHasRegion(true); break; case AzureQueryType.MetricNamesQuery: setRequireSubscription(true); @@ -95,6 +99,9 @@ const VariableEditor = (props: Props) => { setRequireNamespace(true); setRequireResource(true); break; + case AzureQueryType.LocationsQuery: + setRequireSubscription(true); + break; } }, [queryType]); @@ -135,6 +142,16 @@ const VariableEditor = (props: Props) => { } }, [datasource, subscription, resourceGroup]); + useEffect(() => { + if (subscription) { + datasource.azureMonitorDatasource.getLocations([subscription]).then((rgs) => { + const regions: SelectableValue[] = []; + rgs.forEach((r) => regions.push({ label: r.displayName, value: r.name })); + setRegions(regions); + }); + } + }, [datasource, subscription, resourceGroup]); + const namespace = (typeof query === 'object' && query.namespace) || ''; useEffect(() => { if (subscription) { @@ -191,6 +208,13 @@ const VariableEditor = (props: Props) => { }); }; + const onChangeRegion = (selectableValue: SelectableValue) => { + onChange({ + ...query, + region: selectableValue.value, + }); + }; + const onChangeResource = (selectableValue: SelectableValue) => { onChange({ ...query, @@ -296,6 +320,22 @@ const VariableEditor = (props: Props) => { /> )} + {hasRegion && ( + + diff --git a/public/app/plugins/datasource/prometheus/configuration/AzureCredentialsForm.tsx b/public/app/plugins/datasource/prometheus/configuration/AzureCredentialsForm.tsx index daa1fb3cf31..18154fdbebc 100644 --- a/public/app/plugins/datasource/prometheus/configuration/AzureCredentialsForm.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/AzureCredentialsForm.tsx @@ -1,6 +1,8 @@ +import { cx } from '@emotion/css'; import React, { ChangeEvent, useEffect, useReducer, useState } from 'react'; import { SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { InlineFormLabel, Button } from '@grafana/ui/src/components'; import { Input } from '@grafana/ui/src/components/Forms/Legacy/Input/Input'; import { Select } from '@grafana/ui/src/components/Forms/Legacy/Select/Select'; @@ -148,6 +150,7 @@ export const AzureCredentialsForm = (props: Props) => { onCredentialsChange(updated); } }; + const prometheusConfigOverhaulAuth = config.featureToggles.prometheusConfigOverhaulAuth; return (
@@ -190,7 +193,7 @@ export const AzureCredentialsForm = (props: Props) => { Directory (tenant) ID
{ Application (client) ID
{ Client Secret - +
{!disabled && (
-
+
@@ -237,7 +249,7 @@ export const AzureCredentialsForm = (props: Props) => { Client Secret
{
Default Subscription -
+
updateValidDuration({ ...validDuration, timeInterval: e.currentTarget.value })} + /> + {validateInput(validDuration.timeInterval, DURATION_REGEX, durationError)} - } - interactive={true} - disabled={options.readOnly} - > - <> - updateValidDuration({ ...validDuration, timeInterval: e.currentTarget.value })} - /> - {validateInput(validDuration.timeInterval, DURATION_REGEX, durationError)} - - + +
+
+ {/* Query Timeout */} +
+
+ Set the Prometheus query timeout. {docsTip()}} + interactive={true} + disabled={options.readOnly} + > + <> + updateValidDuration({ ...validDuration, queryTimeout: e.currentTarget.value })} + /> + {validateInput(validDuration.queryTimeout, DURATION_REGEX, durationError)} + + +
- {/* Query Timeout */} -
+ + + +
Set the Prometheus query timeout. {docsTip()}} - interactive={true} - disabled={options.readOnly} - > - <> - updateValidDuration({ ...validDuration, queryTimeout: e.currentTarget.value })} - /> - {validateInput(validDuration.queryTimeout, DURATION_REGEX, durationError)} - - -
-
-
- -

Query editor

-
-
- Set default editor option for all users of this data source. {docsTip()}} - interactive={true} - disabled={options.readOnly} - > - o.value === options.jsonData.prometheusType)} - onChange={onChangeHandler( - 'prometheusType', - { - ...options, - jsonData: { ...options.jsonData, prometheusVersion: undefined }, - }, - (options) => { - // Check buildinfo api and set default version if we can - setPrometheusVersion(options, onOptionsChange, onUpdate); - return onOptionsChange({ - ...options, - jsonData: { ...options.jsonData, prometheusVersion: undefined }, - }); - } - )} + aria-label={`Default Editor (Code or Builder)`} + options={editorOptions} + value={ + editorOptions.find((o) => o.value === options.jsonData.defaultEditor) ?? + editorOptions.find((o) => o.value === QueryEditorMode.Builder) + } + onChange={onChangeHandler('defaultEditor', options, onOptionsChange)} width={40} />
+
+ + Checking this option will disable the metrics chooser and metric/label support in the query + field's autocomplete. This helps if you have performance issues with bigger Prometheus instances.{' '} + {docsTip()} + + } + interactive={true} + disabled={options.readOnly} + className={styles.switchField} + > + + +
-
- {options.jsonData.prometheusType && ( + + + + {!options.jsonData.prometheusType && !options.jsonData.prometheusVersion && options.readOnly && ( +
+ For more information on configuring prometheus type and version in data sources, see the{' '} + + provisioning documentation + + . +
+ )} +
+
- Use this to set the version of your {options.jsonData.prometheusType} instance if it is not - automatically configured. {docsTip()} + Set this to the type of your prometheus database, e.g. Prometheus, Cortex, Mimir or Thanos. Changing + this field will save your current settings, and attempt to detect the version. Certain types of + Prometheus support or do not support various APIs. For example, some types support regex matching + for label queries to improve performance. Some types have an API for metadata. If you set this + incorrectly you may experience odd behavior when querying metrics and labels. Please check your + Prometheus documentation to ensure you enter the correct type. {docsTip()} } interactive={true} disabled={options.readOnly} > o.value === options.jsonData.prometheusVersion + )} + onChange={onChangeHandler('prometheusVersion', options, onOptionsChange)} + width={40} + /> + +
+ )} +
+ {config.featureToggles.prometheusResourceBrowserCache && ( +
+
+ + Sets the browser caching level for editor queries. Higher cache settings are recommended for high + cardinality data sources. + + } + interactive={true} + disabled={options.readOnly} + > + o.value === options.jsonData.cacheLevel) ?? PrometheusCacheLevel.Low - } + <> + + updateValidDuration({ ...validDuration, incrementalQueryOverlapWindow: e.currentTarget.value }) + } + className="width-20" + value={options.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow} + onChange={onChangeHandler('incrementalQueryOverlapWindow', options, onOptionsChange)} + spellCheck={false} + /> + {validateInput(validDuration.incrementalQueryOverlapWindow, MULTIPLE_DURATION_REGEX, durationError)} + + + )} +
+ +
+
+ This feature will disable recording rules Turn this on to improve dashboard performance} + interactive={true} + className={styles.switchField} + disabled={options.readOnly} + > +
- )} - -
-
- - This feature will change the default behavior of relative queries to always request fresh data from - the prometheus instance, instead query results will be cached, and only new records are requested. - Turn this on to decrease database and network load. - - } - interactive={true} - className={styles.switchField} - disabled={options.readOnly} - > - - -
+ -
- {options.jsonData.incrementalQuerying && ( - - Set a duration like 10m or 120s or 0s. Default of 10 minutes. This duration will be added to the - duration of each incremental request. - - } - interactive={true} - disabled={options.readOnly} - > - <> + +
+
+
+ + Add custom parameters to the Prometheus query URL. For example timeout, partial_response, dedup, or + max_source_resolution. Multiple parameters should be concatenated together with an ‘&’. {docsTip()} + + } + interactive={true} + disabled={options.readOnly} + > - updateValidDuration({ ...validDuration, incrementalQueryOverlapWindow: e.currentTarget.value }) - } - className="width-25" - value={options.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow} - onChange={onChangeHandler('incrementalQueryOverlapWindow', options, onOptionsChange)} + className="width-20" + value={options.jsonData.customQueryParameters} + onChange={onChangeHandler('customQueryParameters', options, onOptionsChange)} spellCheck={false} + placeholder="Example: max_source_resolution=5m&timeout=10" /> - {validateInput(validDuration.incrementalQueryOverlapWindow, MULTIPLE_DURATION_REGEX, durationError)} - - - )} + +
+
+
+ {/* HTTP Method */} +
+ + You can use either POST or GET HTTP method to query your Prometheus data source. POST is the + recommended method as it allows bigger queries. Change this to GET if you have a Prometheus version + older than 2.1 or if POST requests are restricted in your network. {docsTip()} + + } + interactive={true} + label="HTTP method" + disabled={options.readOnly} + > + - -
-
-
- {/* HTTP Method */} -
- - You can use either POST or GET HTTP method to query your Prometheus data source. POST is the - recommended method as it allows bigger queries. Change this to GET if you have a Prometheus version - older than 2.1 or if POST requests are restricted in your network. {docsTip()} - - } - interactive={true} - label="HTTP method" - disabled={options.readOnly} - > - + + + + ); +}; diff --git a/public/app/plugins/datasource/prometheus/configuration/overhaul/ConnectionSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/overhaul/ConnectionSettings.tsx new file mode 100644 index 00000000000..8167b97ae3f --- /dev/null +++ b/public/app/plugins/datasource/prometheus/configuration/overhaul/ConnectionSettings.tsx @@ -0,0 +1,90 @@ +import { css, cx } from '@emotion/css'; +import React, { ReactNode } from 'react'; + +import { DataSourceJsonData, DataSourceSettings } from '@grafana/data'; +import { ConfigSection } from '@grafana/experimental'; +import { InlineField, Input, PopoverContent } from '@grafana/ui'; + +import { PromOptions } from '../../types'; +// THIS FILE IS COPIED FROM GRAFANA/EXPERIMENTAL +// BECAUSE IT CONTAINS TYPES THAT ARE REQUIRED IN THE ADVANCEDHTTPSETTINGS COMPONENT +// THE TYPES ARE WRITTEN IN EXPERIMENTAL WHERE THEY ARE NOT AS STRICT +// @ts-ignore +export type Config = DataSourceSettings< + // @ts-ignore + JSONData, + // @ts-ignore + SecureJSONData +>; +// @ts-ignore +export type OnChangeHandler = (options: DataSourceSettings) => void; +// @ts-ignore +export type Props = { + config: C; + onChange: OnChangeHandler; + description?: ReactNode; + urlPlaceholder?: string; + urlTooltip?: PopoverContent; + urlLabel?: string; + className?: string; +}; +// @ts-ignore +export const ConnectionSettings: (props: Props) => JSX.Element = ({ + config, + onChange, + description, + urlPlaceholder, + urlTooltip, + urlLabel, + className, +}) => { + const isValidUrl = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/.test( + config.url + ); + + const styles = { + container: css({ + maxWidth: 578, + }), + }; + + return ( + <> + + + Specify a complete HTTP URL +
+ (for example https://example.com:8080) + + ) + } + grow + disabled={config.readOnly} + required + invalid={!isValidUrl && !config.readOnly} + error={isValidUrl ? '' : 'Please enter a valid URL'} + interactive + > + + onChange({ + ...config, + url: event.currentTarget.value, + }) + } + value={config.url || ''} + placeholder={urlPlaceholder || 'URL'} + /> +
+
+ + ); +}; diff --git a/public/app/plugins/datasource/prometheus/configuration/overhaul/types.ts b/public/app/plugins/datasource/prometheus/configuration/overhaul/types.ts new file mode 100644 index 00000000000..5edb4da623c --- /dev/null +++ b/public/app/plugins/datasource/prometheus/configuration/overhaul/types.ts @@ -0,0 +1,11 @@ +import { ReactElement } from 'react'; + +// these are not available yet in grafana +export type CustomMethodId = `custom-${string}`; + +export type CustomMethod = { + id: CustomMethodId; + label: string; + description: string; + component: ReactElement; +}; diff --git a/public/app/plugins/datasource/prometheus/promql.ts b/public/app/plugins/datasource/prometheus/promql.ts index 8b4f032989a..c37f52796ba 100644 --- a/public/app/plugins/datasource/prometheus/promql.ts +++ b/public/app/plugins/datasource/prometheus/promql.ts @@ -238,6 +238,13 @@ export const FUNCTIONS = [ documentation: 'Returns the day of the week for each of the given times in UTC. Returned values are from 0 to 6, where 0 means Sunday etc.', }, + { + insertText: 'day_of_year', + label: 'day_of_year', + detail: 'day_of_year(v=vector(time()) instant-vector)', + documentation: + 'Returns the day of the year for each of the given times in UTC. Returned values are from 1 to 365 for non-leap years, and 1 to 366 in leap years.', + }, { insertText: 'days_in_month', label: 'days_in_month', @@ -521,6 +528,12 @@ export const FUNCTIONS = [ detail: 'last_over_time(range-vector)', documentation: 'The most recent point value in specified interval.', }, + { + insertText: 'present_over_time', + label: 'present_over_time', + detail: 'present_over_time(range-vector)', + documentation: 'The value 1 for any series in the specified interval.', + }, ]; export const PROM_KEYWORDS = FUNCTIONS.map((keyword) => keyword.label); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/operations.ts b/public/app/plugins/datasource/prometheus/querybuilder/operations.ts index 788689ee7da..2c091045bb2 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/operations.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/operations.ts @@ -164,6 +164,10 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { id: PromOperationId.DayOfWeek, category: PromVisualQueryOperationCategory.Time, }), + createFunction({ + id: PromOperationId.DayOfYear, + category: PromVisualQueryOperationCategory.Time, + }), createFunction({ id: PromOperationId.DaysInMonth, category: PromVisualQueryOperationCategory.Time, diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx index d17cebd4105..bd552920d0c 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx @@ -254,8 +254,6 @@ const getStyles = (theme: GrafanaTheme2, isConflicting: boolean) => { card: css({ background: theme.colors.background.primary, border: `1px solid ${theme.colors.border.medium}`, - display: 'flex', - flexDirection: 'column', cursor: 'grab', borderRadius: theme.shape.borderRadius(1), position: 'relative', diff --git a/public/app/plugins/datasource/prometheus/querybuilder/types.ts b/public/app/plugins/datasource/prometheus/querybuilder/types.ts index 49cef909698..24750f21a96 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/types.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/types.ts @@ -53,6 +53,7 @@ export enum PromOperationId { CountValues = 'count_values', DayOfMonth = 'day_of_month', DayOfWeek = 'day_of_week', + DayOfYear = 'day_of_year', DaysInMonth = 'days_in_month', Deg = 'deg', Delta = 'delta', diff --git a/public/app/plugins/datasource/prometheus/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/result_transformer.test.ts index f44856d27af..76240c37803 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.test.ts @@ -360,7 +360,8 @@ describe('Prometheus Result Transformer', () => { expect(series.data[1].meta?.preferredVisualisationType).toEqual('rawPrometheus' as PreferredVisualisationType); }); - it('results with deprecated heatmap format should be correctly transformed', () => { + // Heatmap frames can either have a name of the metric, or if there is no metric, a name of "Value" + it('results with heatmap format (no metric name) should be correctly transformed', () => { const options = { targets: [ { @@ -420,7 +421,7 @@ describe('Prometheus Result Transformer', () => { expect(series.data[0].fields[2].name).toEqual('2'); expect(series.data[0].fields[3].name).toEqual('+Inf'); }); - it('results with heatmap format should be correctly transformed', () => { + it('results with heatmap format (with metric name) should be correctly transformed', () => { const options = { targets: [ { @@ -437,9 +438,10 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'metric_name', type: FieldType.number, values: [10, 10, 0], - labels: { le: '1' }, + labels: { le: '1', __name__: 'metric_name' }, }, ], }), @@ -448,9 +450,10 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'metric_name', type: FieldType.number, values: [30, 10, 40], - labels: { le: '+Inf' }, + labels: { le: '+Inf', __name__: 'metric_name' }, }, ], }), @@ -459,9 +462,10 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'metric_name', type: FieldType.number, values: [20, 10, 30], - labels: { le: '2' }, + labels: { le: '2', __name__: 'metric_name' }, }, ], }), @@ -478,7 +482,7 @@ describe('Prometheus Result Transformer', () => { expect(series.data[0].fields[3].name).toEqual('+Inf'); }); - it('results with heatmap format from multiple queries should be correctly transformed', () => { + it('results with heatmap format (no metric name) from multiple queries should be correctly transformed', () => { const options = { targets: [ { @@ -499,6 +503,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1' }, @@ -510,6 +515,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [20, 10, 30], labels: { le: '2' }, @@ -521,6 +527,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40], labels: { le: '+Inf' }, @@ -532,6 +539,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1' }, @@ -543,6 +551,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [20, 10, 30], labels: { le: '2' }, @@ -554,6 +563,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40], labels: { le: '+Inf' }, @@ -569,6 +579,103 @@ describe('Prometheus Result Transformer', () => { expect(series.data[0].fields[2].values).toEqual([10, 0, 30]); expect(series.data[0].fields[3].values).toEqual([10, 0, 10]); }); + it('results with heatmap format (with metric name) from multiple queries should be correctly transformed', () => { + const options = { + targets: [ + { + format: 'heatmap', + refId: 'A', + }, + { + format: 'heatmap', + refId: 'B', + }, + ], + } as unknown as DataQueryRequest; + const response = { + state: 'Done', + data: [ + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [10, 10, 0], + labels: { le: '1', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [20, 10, 30], + labels: { le: '2', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [30, 10, 40], + labels: { le: '+Inf', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'B', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [10, 10, 0], + labels: { le: '1', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'B', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [20, 10, 30], + labels: { le: '2', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'B', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [30, 10, 40], + labels: { le: '+Inf', __name__: 'metric_name' }, + }, + ], + }), + ], + } as unknown as DataQueryResponse; + + const series = transformV2(response, options, {}); + expect(series.data[0].fields.length).toEqual(4); + expect(series.data[0].fields[1].values).toEqual([10, 10, 0]); + expect(series.data[0].fields[2].values).toEqual([10, 0, 30]); + expect(series.data[0].fields[3].values).toEqual([10, 0, 10]); + }); it('results with heatmap format and multiple histograms should be grouped and de-accumulated by non-le labels', () => { const options = { @@ -588,6 +695,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1', additionalProperty: '10' }, @@ -599,6 +707,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [20, 10, 30], labels: { le: '2', additionalProperty: '10' }, @@ -610,6 +719,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40], labels: { le: '+Inf', additionalProperty: '10' }, @@ -622,6 +732,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [0, 10, 10], labels: { le: '1', additionalProperty: '20' }, @@ -633,6 +744,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [20, 10, 40], labels: { le: '2', additionalProperty: '20' }, @@ -644,6 +756,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 60], labels: { le: '+Inf', additionalProperty: '20' }, @@ -656,6 +769,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 30, 60], labels: { le: '1', additionalProperty: '30' }, @@ -667,6 +781,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 40, 60], labels: { le: '2', additionalProperty: '30' }, @@ -678,6 +793,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [40, 40, 60], labels: { le: '+Inf', additionalProperty: '30' }, @@ -719,6 +835,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1' }, @@ -736,6 +853,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4, 3, 2, 1] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40, 90, 14, 21], labels: { le: '6' }, @@ -767,6 +885,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1' }, @@ -784,6 +903,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4, 3, 2, 1] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40, 90, 14, 21], labels: { le: '6' }, diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index df895ce838f..f74d9a6a7bf 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -45,6 +45,7 @@ export interface PromOptions extends DataSourceJsonData { incrementalQuerying?: boolean; incrementalQueryOverlapWindow?: string; disableRecordingRules?: boolean; + sigV4Auth?: boolean; } export type ExemplarTraceIdDestination = { diff --git a/public/app/plugins/datasource/tempo/configuration/LokiSearchSettings.tsx b/public/app/plugins/datasource/tempo/configuration/LokiSearchSettings.tsx index 8a5d9d87b53..7ae8e368ee2 100644 --- a/public/app/plugins/datasource/tempo/configuration/LokiSearchSettings.tsx +++ b/public/app/plugins/datasource/tempo/configuration/LokiSearchSettings.tsx @@ -1,8 +1,12 @@ import React from 'react'; -import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { + DataSourceInstanceSettings, + DataSourcePluginOptionsEditorProps, + updateDatasourcePluginJsonDataOption, +} from '@grafana/data'; import { Button, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { TempoJsonData } from '../types'; @@ -33,7 +37,7 @@ export function LokiSearchSettings({ options, onOptionsChange }: Props) { current={options.jsonData.lokiSearch?.datasourceUid} noDefault={true} width={40} - onChange={(ds) => + onChange={(ds: DataSourceInstanceSettings) => updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'lokiSearch', { datasourceUid: ds.uid, }) diff --git a/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx b/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx index 46ecf18670c..646d004f888 100644 --- a/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx +++ b/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx @@ -1,8 +1,12 @@ import React from 'react'; -import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { + DataSourceInstanceSettings, + DataSourcePluginOptionsEditorProps, + updateDatasourcePluginJsonDataOption, +} from '@grafana/data'; import { Button, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { TempoJsonData } from '../types'; @@ -27,7 +31,7 @@ export function ServiceGraphSettings({ options, onOptionsChange }: Props) { current={options.jsonData.serviceMap?.datasourceUid} noDefault={true} width={40} - onChange={(ds) => + onChange={(ds: DataSourceInstanceSettings) => updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'serviceMap', { datasourceUid: ds.uid, }) diff --git a/public/app/plugins/datasource/tempo/dataquery.cue b/public/app/plugins/datasource/tempo/dataquery.cue index 82016c131d3..a54bb10e116 100644 --- a/public/app/plugins/datasource/tempo/dataquery.cue +++ b/public/app/plugins/datasource/tempo/dataquery.cue @@ -44,8 +44,6 @@ composableKinds: DataQuery: { serviceMapIncludeNamespace?: bool // Defines the maximum number of traces that are returned from Tempo limit?: int64 - // Use the streaming API to get partial results as they are available - streaming?: bool filters: [...#TraceqlFilter] } @cuetsy(kind="interface") @grafana(TSVeneer="type") diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts index 0ddba987cfd..7bfac5a1758 100644 --- a/public/app/plugins/datasource/tempo/dataquery.gen.ts +++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts @@ -48,10 +48,6 @@ export interface TempoQuery extends common.DataQuery { * @deprecated Query traces by span name */ spanName?: string; - /** - * Use the streaming API to get partial results as they are available - */ - streaming?: boolean; } export const defaultTempoQuery: Partial = { diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index bc72734c7ea..bc58422a6fb 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -223,10 +223,10 @@ export class TempoDatasource extends DataSourceWithBackend(({ onChange, query }) query.limit = DEFAULT_LIMIT; } - if (!query.hasOwnProperty('streaming')) { - query.streaming = !config.featureToggles.disableTraceQLStreaming; - } - const onLimitChange = (e: React.FormEvent) => { onChange({ ...query, limit: parseInt(e.currentTarget.value, 10) }); }; - const onStreamingChange = (e: React.FormEvent) => { - onChange({ ...query, streaming: e.currentTarget.checked }); - }; - const collapsedInfoList = [`Limit: ${query.limit || DEFAULT_LIMIT}`]; - if (!config.featureToggles.disableTraceQLStreaming) { - collapsedInfoList.push(`Streaming: ${query.streaming ? 'Yes' : 'No'}`); - } return ( <> @@ -50,11 +38,6 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query }) value={query.limit} /> - {!config.featureToggles.disableTraceQLStreaming && ( - - - - )} diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index 5f8c608b484..a29404959f7 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -135,18 +135,53 @@ export function UnifiedAlertList(props: PanelProps) { [parsedOptions.alertInstanceLabelFilter] ); + // If the datasource is not defined we should NOT skip the query + // Undefined dataSourceName means that there is no datasource filter applied and we should fetch all the rules + const shouldFetchGrafanaRules = !dataSourceName || dataSourceName === GRAFANA_RULES_SOURCE_NAME; + + //For grafana managed rules, get the result using RTK Query to avoid the need of using the redux store + //See https://github.com/grafana/grafana/pull/70482 + const { + currentData: grafanaPromRules = [], + isLoading: grafanaRulesLoading, + refetch: refetchGrafanaPromRules, + } = usePrometheusRulesByNamespaceQuery( + { + limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined, + matcher: matcherList, + state: stateList, + }, + { skip: !shouldFetchGrafanaRules } + ); + useEffect(() => { //we need promRules and rulerRules for getting the uid when creating the alert link in panel in case of being a rulerRule. if (!promRulesRequests.loading) { fetchPromAndRuler({ dispatch, limitInstances, matcherList, dataSourceName, stateList }); } - const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => - fetchPromAndRuler({ dispatch, limitInstances, matcherList, dataSourceName, stateList }) - ); + const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => { + if (shouldFetchGrafanaRules) { + refetchGrafanaPromRules(); + } + + if (!dataSourceName || dataSourceName !== GRAFANA_RULES_SOURCE_NAME) { + fetchPromAndRuler({ dispatch, limitInstances, matcherList, dataSourceName, stateList }); + } + }); return () => { sub?.unsubscribe(); }; - }, [dispatch, dashboard, matcherList, stateList, limitInstances, dataSourceName, promRulesRequests.loading]); + }, [ + dispatch, + dashboard, + matcherList, + stateList, + limitInstances, + dataSourceName, + refetchGrafanaPromRules, + shouldFetchGrafanaRules, + promRulesRequests.loading, + ]); const handleInstancesLimit = (limit: boolean) => { if (limit) { @@ -158,18 +193,7 @@ export function UnifiedAlertList(props: PanelProps) { } }; - //For grafana managed rules, get the result using RTK Query to avoid the need of using the redux store - //See https://github.com/grafana/grafana/pull/70482 - const { currentData: promRules = [], isLoading: grafanaRulesLoading } = usePrometheusRulesByNamespaceQuery( - { - limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined, - matcher: matcherList, - state: stateList, - }, - { skip: dataSourceName !== GRAFANA_RULES_SOURCE_NAME } - ); - - const combinedRules = useCombinedRuleNamespaces(undefined, promRules); + const combinedRules = useCombinedRuleNamespaces(undefined, grafanaPromRules); const someRulerRulesDispatched = isAsyncRequestMapSlicePartiallyDispatched(rulerRulesRequests); const haveResults = isAsyncRequestMapSlicePartiallyFulfilled(promRulesRequests); diff --git a/public/app/plugins/panel/alertlist/module.tsx b/public/app/plugins/panel/alertlist/module.tsx index 481d3ec14b0..33f3126f923 100644 --- a/public/app/plugins/panel/alertlist/module.tsx +++ b/public/app/plugins/panel/alertlist/module.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { PanelPlugin } from '@grafana/data'; -import { config, DataSourcePicker } from '@grafana/runtime'; +import { DataSourceInstanceSettings, PanelPlugin } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { TagsInput } from '@grafana/ui'; import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; import { @@ -9,6 +9,7 @@ import { GENERAL_FOLDER, ReadonlyFolderPicker, } from 'app/core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { PermissionLevelString } from 'app/types'; import { GRAFANA_DATASOURCE_NAME } from '../../../features/alerting/unified/utils/datasource'; @@ -259,7 +260,7 @@ const unifiedAlertList = new PanelPlugin(UnifiedAlertLi type={['prometheus', 'loki', 'grafana']} noDefault current={props.value} - onChange={(ds) => props.onChange(ds.name)} + onChange={(ds: DataSourceInstanceSettings) => props.onChange(ds.name)} onClear={() => props.onChange(null)} /> ); diff --git a/public/app/plugins/panel/geomap/editor/StyleEditor.tsx b/public/app/plugins/panel/geomap/editor/StyleEditor.tsx index ee6e369ef79..56aba8718aa 100644 --- a/public/app/plugins/panel/geomap/editor/StyleEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/StyleEditor.tsx @@ -1,9 +1,9 @@ import { capitalize } from 'lodash'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useObservable } from 'react-use'; import { Observable, of } from 'rxjs'; -import { FieldConfigPropertyItem, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data'; +import { FieldConfigPropertyItem, StandardEditorProps, StandardEditorsRegistryItem, FrameMatcher } from '@grafana/data'; import { ScaleDimensionConfig, ResourceDimensionConfig, @@ -39,11 +39,22 @@ export interface StyleEditorOptions { layerInfo?: Observable; simpleFixedValues?: boolean; displayRotation?: boolean; + hideSymbol?: boolean; + frameMatcher?: FrameMatcher; } type Props = StandardEditorProps; -export const StyleEditor = ({ value, context, onChange, item }: Props) => { +export const StyleEditor = (props: Props) => { + const { value, onChange, item } = props; + const context = useMemo(() => { + if (!item.settings?.frameMatcher) { + return props.context; + } + + return { ...props.context, data: props.context.data.filter(item.settings.frameMatcher) }; + }, [props.context, item.settings]); + const settings = item.settings; const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => { @@ -188,24 +199,26 @@ export const StyleEditor = ({ value, context, onChange, item }: Props) => { } /> - - - + {!settings?.hideSymbol && ( + + + + )} = { + type: NETWORK_LAYER_ID, + name: '', // will get replaced + config: defaultOptions, + location: { + mode: FrameGeometrySourceMode.Auto, + }, +}; + +/** + * Map layer configuration for network overlay + */ +export const networkLayer: MapLayerRegistryItem = { + id: NETWORK_LAYER_ID, + name: 'Network', + description: 'Render a node graph as a map layer', + isBaseMap: false, + showLocation: true, + hideOpacity: true, + state: PluginState.beta, + + /** + * Function that configures transformation and returns a transformer + * @param map + * @param options + * @param eventBus + * @param theme + */ + create: async (map: Map, options: MapLayerOptions, eventBus: EventBus, theme: GrafanaTheme2) => { + // Assert default values + const config = { + ...defaultOptions, + ...options?.config, + }; + + const style = await getStyleConfigState(config.style); + const edgeStyle = await getStyleConfigState(config.edgeStyle); + const location = await getLocationMatchers(options.location); + const source = new FrameVectorSource(location); + + const vectorLayer = new VectorLayer({ + source, + }); + const hasArrows = config.arrow === 1 || config.arrow === -1 || config.arrow === 2; + + // TODO update legend to display edges as well + const legendProps = new ReplaySubject(1); + let legend: ReactNode = null; + if (config.showLegend) { + legend = ; + } + + vectorLayer.setStyle((feature: FeatureLike) => { + const geom = feature.getGeometry(); + const idx = feature.get('rowIndex'); + const dims = style.dims; + + if (!style.fields && !edgeStyle.fields && !hasArrows && geom?.getType() !== 'LineString') { + // Set a global style + return style.maker(style.base); + } + + // For edges + if (geom?.getType() === 'LineString' && geom instanceof SimpleGeometry) { + const edgeDims = edgeStyle.dims; + const edgeTextConfig = edgeStyle.config.textConfig; + const edgeId = Number(feature.getId()); + const coordinates = geom.getCoordinates(); + const opacity = edgeStyle.config.opacity ?? 1; + if (coordinates && edgeDims) { + const segmentStartCoords = coordinates[0]; + const segmentEndCoords = coordinates[1]; + const color1 = tinycolor( + theme.visualization.getColorByName((edgeDims.color && edgeDims.color.get(edgeId)) ?? edgeStyle.base.color) + ) + .setAlpha(opacity) + .toString(); + const color2 = tinycolor( + theme.visualization.getColorByName((edgeDims.color && edgeDims.color.get(edgeId)) ?? edgeStyle.base.color) + ) + .setAlpha(opacity) + .toString(); + const arrowSize1 = (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size; + const arrowSize2 = (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size; + const styles = []; + + const flowStyle = new FlowLine({ + visible: true, + lineCap: config.arrow === 0 ? 'round' : 'square', + color: color1, + color2: color2, + width: (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size, + width2: (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size, + }); + + if (config.arrow) { + flowStyle.setArrow(config.arrow); + if (config.arrow > 0) { + flowStyle.setArrowColor(color2); + flowStyle.setArrowSize((arrowSize2 ?? 0) * 2); + } else { + flowStyle.setArrowColor(color1); + flowStyle.setArrowSize((arrowSize1 ?? 0) * 2); + } + } + const LS = new LineString([segmentStartCoords, segmentEndCoords]); + flowStyle.setGeometry(LS); + + const fontFamily = theme.typography.fontFamily; + if (edgeDims.text) { + const labelStyle = new Style({ + zIndex: 10, + text: new Text({ + text: edgeDims.text.get(edgeId), + font: `normal ${edgeTextConfig?.fontSize}px ${fontFamily}`, + fill: new Fill({ color: color1 ?? defaultStyleConfig.color.fixed }), + stroke: new Stroke({ + color: tinycolor(theme.visualization.getColorByName('text')).setAlpha(opacity).toString(), + width: Math.max(edgeTextConfig?.fontSize! / 10, 1), + }), + ...edgeTextConfig, + }), + }); + labelStyle.setGeometry(LS); + styles.push(labelStyle); + } + styles.push(flowStyle); + return styles; + } + } + if (!dims || !isNumber(idx)) { + return style.maker(style.base); + } + + const values = { ...style.base }; + + if (dims.color) { + values.color = dims.color.get(idx); + } + if (dims.size) { + values.size = dims.size.get(idx); + } + if (dims.text) { + values.text = dims.text.get(idx); + } + if (dims.rotation) { + values.rotation = dims.rotation.get(idx); + } + return style.maker(values); + }); + + return { + init: () => vectorLayer, + legend: legend, + update: (data: PanelData) => { + if (!data.series?.length) { + source.clear(); + return; // ignore empty + } + + // Post updates to the legend component + if (legend) { + legendProps.next({ + styleConfig: style, + size: style.dims?.size, + layerName: options.name, + layer: vectorLayer, + }); + } + const graphFrames = getGraphFrame(data.series); + + for (const frame of data.series) { + if (frame === graphFrames.edges[0]) { + edgeStyle.dims = getStyleDimension(frame, edgeStyle, theme); + } else { + style.dims = getStyleDimension(frame, style, theme); + } + + updateEdge(source, graphFrames); + } + }, + + // Marker overlay options + registerOptionsUI: (builder, context) => { + const networkFrames = getGraphFrame(context.data); + const frameNodes = networkFrames.nodes[0]; + const frameEdges = networkFrames.edges[0]; + + builder + .addCustomEditor({ + id: 'config.style', + category: ['Node Styles'], + path: 'config.style', + name: 'Node Styles', + editor: StyleEditor, + settings: { + displayRotation: true, + frameMatcher: (frame: DataFrame) => frame === frameNodes, + }, + defaultValue: defaultOptions.style, + }) + .addCustomEditor({ + id: 'config.edgeStyle', + category: ['Edge Styles'], + path: 'config.edgeStyle', + name: 'Edge Styles', + editor: StyleEditor, + settings: { + hideSymbol: true, + frameMatcher: (frame: DataFrame) => frame === frameEdges, + }, + defaultValue: defaultOptions.style, + }) + .addRadio({ + path: 'config.arrow', + name: 'Arrow', + settings: { + options: [ + { label: 'None', value: 0 }, + { label: 'Forward', value: 1 }, + { label: 'Reverse', value: -1 }, + { label: 'Both', value: 2 }, + ], + }, + defaultValue: defaultOptions.arrow, + }) + .addBooleanSwitch({ + path: 'config.showLegend', + name: 'Show legend', + description: 'Show map legend', + defaultValue: defaultOptions.showLegend, + }); + }, + }; + }, + + // fill in the default values + defaultOptions, +}; + +function updateEdge(source: FrameVectorSource, graphFrames: GraphFrame) { + source.clear(true); + + const frameNodes = graphFrames.nodes[0]; + const frameEdges = graphFrames.edges[0]; + + if (!frameNodes || !frameEdges) { + // TODO: provide helpful error message / link to docs for how to format data + return; + } + + const info = getGeometryField(frameNodes, source.location); + if (!info.field) { + source.changed(); + return; + } + + // TODO: Fix this + // eslint-disable-next-line + const field = info.field as unknown as Field; + + // TODO for nodes, don't hard code id field name + const nodeIdIndex = frameNodes.fields.findIndex((f: Field) => f.name === 'id'); + const nodeIdValues = frameNodes.fields[nodeIdIndex].values; + + // Edges + // TODO for edges, don't hard code source and target fields + const sourceIndex = frameEdges.fields.findIndex((f: Field) => f.name === 'source'); + const targetIndex = frameEdges.fields.findIndex((f: Field) => f.name === 'target'); + + const sources = frameEdges.fields[sourceIndex].values; + const targets = frameEdges.fields[targetIndex].values; + + // Loop through edges, referencing node locations + for (let i = 0; i < sources.length; i++) { + // Create linestring for each edge + const sourceId = sources[i]; + const targetId = targets[i]; + + const sourceNodeIndex = nodeIdValues.findIndex((value: string) => value === sourceId); + const targetNodeIndex = nodeIdValues.findIndex((value: string) => value === targetId); + + if (!field.values[sourceNodeIndex] || !field.values[targetNodeIndex]) { + continue; + } + + const geometryEdge: Geometry = new LineString([ + field.values[sourceNodeIndex].getCoordinates(), + field.values[targetNodeIndex].getCoordinates(), + ]); + + const edgeFeature = new Feature({ + geometry: geometryEdge, + }); + edgeFeature.setId(i); + source['addFeatureInternal'](edgeFeature); // @TODO revisit? + } + + // Nodes + for (let i = 0; i < frameNodes.length; i++) { + source['addFeatureInternal']( + new Feature({ + frameNodes, + rowIndex: i, + geometry: info.field.values[i], + }) + ); + } + + // only call source at the end + source.changed(); +} diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx index 40fe2fe4897..5a05b0ba09e 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx @@ -8,6 +8,9 @@ import { getFieldDisplayName, LinkModel, TimeRange, + getLinksSupplier, + InterpolateFunction, + ScopedVars, } from '@grafana/data'; import { HeatmapCellLayout } from '@grafana/schema'; import { LinkButton, VerticalGroup } from '@grafana/ui'; @@ -23,6 +26,8 @@ type Props = { hover: HeatmapHoverEvent; showHistogram?: boolean; timeRange: TimeRange; + replaceVars: InterpolateFunction; + scopedVars: ScopedVars[]; }; export const HeatmapHoverView = (props: Props) => { @@ -32,7 +37,7 @@ export const HeatmapHoverView = (props: Props) => { return ; }; -const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => { +const HeatmapHoverCell = ({ data, hover, showHistogram, scopedVars, replaceVars }: Props) => { const index = hover.dataIdx; const xField = data.heatmap?.fields[0]; const yField = data.heatmap?.fields[1]; @@ -119,12 +124,22 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => { const linkLookup = new Set(); for (const field of visibleFields ?? []) { - // TODO: Currently always undefined? (getLinks) - if (field.getLinks) { - const v = field.values[index]; - const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v }; + const hasLinks = field.config.links && field.config.links.length > 0; - field.getLinks({ calculatedValue: disp, valueRowIndex: index }).forEach((link) => { + if (hasLinks && data.heatmap) { + const appropriateScopedVars = scopedVars.find( + (scopedVar) => + scopedVar && scopedVar.__dataContext && scopedVar.__dataContext.value.field.name === nonNumericOrdinalDisplay + ); + + field.getLinks = getLinksSupplier(data.heatmap, field, appropriateScopedVars || {}, replaceVars); + } + + if (field.getLinks) { + const value = field.values[index]; + const display = field.display ? field.display(value) : { text: `${value}`, numeric: +value }; + + field.getLinks({ calculatedValue: display, valueRowIndex: index }).forEach((link) => { const key = `${link.title}/${link.href}`; if (!linkLookup.has(key)) { links.push(link); diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index a84729f0d83..dac87caa2f8 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -43,6 +43,16 @@ export const HeatmapPanel = ({ const styles = useStyles2(getStyles); const { sync } = usePanelContext(); + // necessary for enabling datalinks in hover view + let scopedVarsFromRawData = []; + for (const series of data.series) { + for (const field of series.fields) { + if (field.state?.scopedVars) { + scopedVarsFromRawData.push(field.state.scopedVars); + } + } + } + // ugh let timeRangeRef = useRef(timeRange); timeRangeRef.current = timeRange; @@ -210,6 +220,8 @@ export const HeatmapPanel = ({ data={info} hover={hover} showHistogram={options.tooltip.yHistogram} + replaceVars={replaceVariables} + scopedVars={scopedVarsFromRawData} /> )} diff --git a/public/app/plugins/panel/nodeGraph/types.ts b/public/app/plugins/panel/nodeGraph/types.ts index 8e2a1636c86..848437d26cf 100644 --- a/public/app/plugins/panel/nodeGraph/types.ts +++ b/public/app/plugins/panel/nodeGraph/types.ts @@ -1,6 +1,6 @@ import { SimulationNodeDatum, SimulationLinkDatum } from 'd3-force'; -import { Field, IconName } from '@grafana/data'; +import { DataFrame, Field, IconName } from '@grafana/data'; export { Options as NodeGraphOptions, ArcOption } from './panelcfg.gen'; @@ -43,3 +43,8 @@ export type NodesMarker = { node: NodeDatum; count: number; }; + +export type GraphFrame = { + nodes: DataFrame[]; + edges: DataFrame[]; +}; diff --git a/public/app/plugins/panel/nodeGraph/useCategorizeFrames.ts b/public/app/plugins/panel/nodeGraph/useCategorizeFrames.ts index a23162c313a..7b815a1d698 100644 --- a/public/app/plugins/panel/nodeGraph/useCategorizeFrames.ts +++ b/public/app/plugins/panel/nodeGraph/useCategorizeFrames.ts @@ -2,6 +2,8 @@ import { useMemo } from 'react'; import { DataFrame } from '@grafana/data'; +import { getGraphFrame } from './utils'; + /** * As we need 2 dataframes for the service map, one with nodes and one with edges we have to figure out which is which. * Right now we do not have any metadata for it so we just check preferredVisualisationType and then column names. @@ -9,20 +11,6 @@ import { DataFrame } from '@grafana/data'; */ export function useCategorizeFrames(series: DataFrame[]) { return useMemo(() => { - return series.reduce<{ - nodes: DataFrame[]; - edges: DataFrame[]; - }>( - (acc, frame) => { - const sourceField = frame.fields.filter((f) => f.name === 'source'); - if (sourceField.length) { - acc.edges.push(frame); - } else { - acc.nodes.push(frame); - } - return acc; - }, - { edges: [], nodes: [] } - ); + return getGraphFrame(series); }, [series]); } diff --git a/public/app/plugins/panel/nodeGraph/utils.ts b/public/app/plugins/panel/nodeGraph/utils.ts index 4fb8e5095a2..a0d533b72f7 100644 --- a/public/app/plugins/panel/nodeGraph/utils.ts +++ b/public/app/plugins/panel/nodeGraph/utils.ts @@ -9,7 +9,7 @@ import { NodeGraphDataFrameFieldNames, } from '@grafana/data'; -import { EdgeDatum, NodeDatum, NodeDatumFromEdge, NodeGraphOptions } from './types'; +import { EdgeDatum, GraphFrame, NodeDatum, NodeDatumFromEdge, NodeGraphOptions } from './types'; type Line = { x1: number; y1: number; x2: number; y2: number }; @@ -593,3 +593,18 @@ export const findConnectedNodesForNode = (nodes: NodeDatum[], edges: EdgeDatum[] } return []; }; + +export const getGraphFrame = (frames: DataFrame[]) => { + return frames.reduce( + (acc, frame) => { + const sourceField = frame.fields.filter((f) => f.name === 'source'); + if (sourceField.length) { + acc.edges.push(frame); + } else { + acc.nodes.push(frame); + } + return acc; + }, + { edges: [], nodes: [] } + ); +}; diff --git a/public/app/plugins/panel/xychart/TooltipView.tsx b/public/app/plugins/panel/xychart/TooltipView.tsx index bf8256bf309..5bfe57ff54c 100644 --- a/public/app/plugins/panel/xychart/TooltipView.tsx +++ b/public/app/plugins/panel/xychart/TooltipView.tsx @@ -72,8 +72,8 @@ export const TooltipView = ({ let yValue: YValue | null = null; let extraFacets: ExtraFacets | null = null; if (seriesMapping === SeriesMapping.Manual && manualSeriesConfigs) { - const colorFacetFieldName = manualSeriesConfigs[hoveredPointIndex].pointColor?.field ?? ''; - const sizeFacetFieldName = manualSeriesConfigs[hoveredPointIndex].pointSize?.field ?? ''; + const colorFacetFieldName = manualSeriesConfigs[hoveredPointIndex]?.pointColor?.field ?? ''; + const sizeFacetFieldName = manualSeriesConfigs[hoveredPointIndex]?.pointSize?.field ?? ''; const colorFacet = colorFacetFieldName ? findField(frame, colorFacetFieldName) : undefined; const sizeFacet = sizeFacetFieldName ? findField(frame, sizeFacetFieldName) : undefined; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 5f727daa716..16f202022ea 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Redirect } from 'react-router-dom'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; import { isTruthy } from '@grafana/data'; import { LoginPage } from 'app/core/components/Login/LoginPage'; @@ -10,6 +10,7 @@ import { contextSrv } from 'app/core/services/context_srv'; import UserAdminPage from 'app/features/admin/UserAdminPage'; import LdapPage from 'app/features/admin/ldap/LdapPage'; import { getAlertingRoutes } from 'app/features/alerting/routes'; +import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/routes'; import { DATASOURCES_ROUTES } from 'app/features/datasources/constants'; import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes'; @@ -105,30 +106,23 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: DATASOURCES_ROUTES.List, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "DataSourcesListPage"*/ 'app/features/datasources/pages/DataSourcesListPage') - ), + component: () => , }, { path: DATASOURCES_ROUTES.Edit, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "EditDataSourcePage"*/ '../features/datasources/pages/EditDataSourcePage') + component: (props: RouteComponentProps<{ uid: string }>) => ( + ), }, { path: DATASOURCES_ROUTES.Dashboards, - component: SafeDynamicImport( - () => - import( - /* webpackChunkName: "DataSourceDashboards"*/ 'app/features/datasources/pages/DataSourceDashboardsPage' - ) + component: (props: RouteComponentProps<{ uid: string }>) => ( + ), }, { path: DATASOURCES_ROUTES.New, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NewDataSourcePage"*/ '../features/datasources/pages/NewDataSourcePage') - ), + component: () => , }, { path: '/datasources/correlations', diff --git a/public/app/types/configAuth.ts b/public/app/types/configAuth.ts index a50ad7f48e7..c72e05a038d 100644 --- a/public/app/types/configAuth.ts +++ b/public/app/types/configAuth.ts @@ -11,7 +11,6 @@ export interface AuthConfigState { export interface AuthProviderStatus { enabled: boolean; configured: boolean; - configFoundInIniFile?: boolean; hide?: boolean; } diff --git a/scripts/drone/pipelines/trigger_downstream.star b/scripts/drone/pipelines/trigger_downstream.star index d35e2ce090a..9004bf6e712 100644 --- a/scripts/drone/pipelines/trigger_downstream.star +++ b/scripts/drone/pipelines/trigger_downstream.star @@ -23,6 +23,9 @@ trigger = { "latest.json", ], }, + "repo": [ + "grafana/grafana", + ], } def enterprise_downstream_pipeline(): diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 42d89768fed..60d56cafc58 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1222,7 +1222,7 @@ def publish_grafanacom_step(ver_mode): ], "environment": { "GRAFANA_COM_API_KEY": from_secret("grafana_api_key"), - "GCP_KEY": from_secret(gcp_grafanauploads), + "GCP_KEY": from_secret(gcp_grafanauploads_base64), }, "commands": [ cmd, @@ -1245,7 +1245,7 @@ def publish_linux_packages_step(package_manager = "deb"): "gpg_passphrase": from_secret("packages_gpg_passphrase"), "gpg_public_key": from_secret("packages_gpg_public_key"), "gpg_private_key": from_secret("packages_gpg_private_key"), - "package_path": "gs://grafana-prerelease/artifacts/downloads/*$${{DRONE_TAG}}/oss/**.{}".format( + "package_path": "gs://grafana-prerelease/artifacts/downloads/*${{DRONE_TAG}}/oss/**.{}".format( package_manager, ), }, @@ -1362,7 +1362,7 @@ def get_windows_steps(ver_mode, bucket = "%PRERELEASE_BUCKET%"): "windows-init", ], "environment": { - "GCP_KEY": from_secret("gcp_key"), + "GCP_KEY": from_secret(gcp_grafanauploads_base64), "PRERELEASE_BUCKET": from_secret(prerelease_bucket), "GITHUB_TOKEN": from_secret("github_token"), }, diff --git a/yarn.lock b/yarn.lock index 6e080abb8bc..568961be878 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3678,9 +3678,9 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource" dependencies: - "@grafana/data": 10.1.0-pre + "@grafana/data": 10.2.0-pre "@grafana/tsconfig": ^1.2.0-rc1 - "@grafana/ui": 10.1.0-pre + "@grafana/ui": 10.2.0-pre "@types/jest": 26.0.15 "@types/react": 18.0.28 copy-webpack-plugin: 11.0.0 @@ -3716,12 +3716,12 @@ __metadata: languageName: node linkType: hard -"@grafana/data@10.1.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": +"@grafana/data@10.2.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": version: 0.0.0-use.local resolution: "@grafana/data@workspace:packages/grafana-data" dependencies: "@braintree/sanitize-url": 6.0.2 - "@grafana/schema": 10.1.0-pre + "@grafana/schema": 10.2.0-pre "@grafana/tsconfig": ^1.2.0-rc1 "@rollup/plugin-commonjs": 25.0.2 "@rollup/plugin-json": 6.0.0 @@ -3783,7 +3783,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@10.1.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": +"@grafana/e2e-selectors@10.2.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": version: 0.0.0-use.local resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors" dependencies: @@ -3820,7 +3820,7 @@ __metadata: "@babel/core": 7.22.1 "@babel/preset-env": 7.22.4 "@cypress/webpack-preprocessor": 5.17.0 - "@grafana/e2e-selectors": 10.1.0-pre + "@grafana/e2e-selectors": 10.2.0-pre "@grafana/tsconfig": ^1.2.0-rc1 "@mochajs/json-file-reporter": ^1.2.0 "@rollup/plugin-node-resolve": 15.1.0 @@ -3983,11 +3983,11 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/runtime@workspace:packages/grafana-runtime" dependencies: - "@grafana/data": 10.1.0-pre - "@grafana/e2e-selectors": 10.1.0-pre + "@grafana/data": 10.2.0-pre + "@grafana/e2e-selectors": 10.2.0-pre "@grafana/faro-web-sdk": 1.1.0 "@grafana/tsconfig": ^1.2.0-rc1 - "@grafana/ui": 10.1.0-pre + "@grafana/ui": 10.2.0-pre "@rollup/plugin-commonjs": 25.0.2 "@rollup/plugin-node-resolve": 15.1.0 "@testing-library/dom": 9.3.0 @@ -4040,7 +4040,7 @@ __metadata: languageName: node linkType: hard -"@grafana/schema@10.1.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": +"@grafana/schema@10.2.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": version: 0.0.0-use.local resolution: "@grafana/schema@workspace:packages/grafana-schema" dependencies: @@ -4090,17 +4090,17 @@ __metadata: languageName: node linkType: hard -"@grafana/ui@10.1.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": +"@grafana/ui@10.2.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: "@babel/core": 7.22.1 "@emotion/css": 11.11.2 "@emotion/react": 11.11.1 - "@grafana/data": 10.1.0-pre - "@grafana/e2e-selectors": 10.1.0-pre + "@grafana/data": 10.2.0-pre + "@grafana/e2e-selectors": 10.2.0-pre "@grafana/faro-web-sdk": 1.1.0 - "@grafana/schema": 10.1.0-pre + "@grafana/schema": 10.2.0-pre "@grafana/tsconfig": ^1.2.0-rc1 "@leeoniya/ufuzzy": 1.0.8 "@mdx-js/react": 1.6.22