diff --git a/.betterer.results b/.betterer.results index f83e79923d8..31d1f23e1c8 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1692,10 +1692,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "11"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] ], "public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] @@ -2639,6 +2636,11 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], + "public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + ], "public/app/features/dashboard-scene/embedding/EmbeddedDashboardTestPage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -4003,11 +4005,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "public/app/features/explore/extensions/AddToDashboard/AddToDashboardForm.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] - ], "public/app/features/explore/extensions/ConfirmNavigationModal.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -6712,10 +6709,6 @@ exports[`better eslint`] = { "public/app/plugins/panel/timeseries/SpanNullsEditor.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], - "public/app/plugins/panel/timeseries/TimezonesEditor.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/plugins/panel/timeseries/migrations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aea94e92eeb..d9c88232b09 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -731,7 +731,6 @@ embed.go @grafana/grafana-as-code /.github/workflows/remove-milestone.yml @grafana/grafana-release-guild /.github/workflows/sbom-report.yml @grafana/security-team /.github/workflows/scripts/json-file-to-job-output.js @grafana/plugins-platform-frontend -/.github/workflows/scripts/pr-get-job-link.js @grafana/plugins-platform-frontend /.github/workflows/stale.yml @grafana/grafana-release-guild /.github/workflows/update-changelog.yml @grafana/grafana-release-guild /.github/workflows/update-make-docs.yml @grafana/docs-tooling diff --git a/.github/commands.json b/.github/commands.json index af047be4341..cbbdea3813b 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -59,14 +59,6 @@ "url": "https://github.com/orgs/grafana/projects/76" } }, - { - "type": "label", - "name": "type/docs", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/69" - } - }, { "type": "label", "name": "datasource/Azure", diff --git a/.github/workflows/detect-breaking-changes-levitate.yml b/.github/workflows/detect-breaking-changes-levitate.yml index 8e802abd639..bb71dc4cfe8 100644 --- a/.github/workflows/detect-breaking-changes-levitate.yml +++ b/.github/workflows/detect-breaking-changes-levitate.yml @@ -6,6 +6,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + id-token: write + on: pull_request: paths: @@ -154,26 +158,16 @@ jobs: project_id: 'grafanalabs-global' install_components: 'bq' - - name: Get link for the Github Action job - id: job - uses: actions/github-script@v6 - with: - script: | - const name = 'Detect breaking changes'; - const script = require('./.github/workflows/scripts/pr-get-job-link.js') - await script({name, github, context, core}) - - name: Detect breaking changes id: breaking-changes run: ./scripts/check-breaking-changes.sh env: FORCE_COLOR: 3 - GITHUB_JOB_LINK: ${{ steps.job.outputs.link }} - name: Persisting the check output run: | mkdir -p ./levitate - echo "{ \"exit_code\": ${{ steps.breaking-changes.outputs.is_breaking }}, \"message\": \"${{ steps.breaking-changes.outputs.message }}\", \"job_link\": \"${{ steps.job.outputs.link }}#step:${GITHUB_STEP_NUMBER}:1\", \"pr_number\": \"${{ github.event.pull_request.number }}\" }" > ./levitate/result.json + echo "{ \"exit_code\": ${{ steps.breaking-changes.outputs.is_breaking }}, \"message\": \"${{ steps.breaking-changes.outputs.message }}\", \"pr_number\": \"${{ github.event.pull_request.number }}\" }" > ./levitate/result.json - name: Upload check output as artifact uses: actions/upload-artifact@v4 @@ -219,15 +213,12 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} with: script: | - const { data } = await github.rest.issues.listLabelsOnIssue({ - issue_number: process.env.PR_NUMBER, + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, }); - const labels = data.map(({ name }) => name); - const doesExist = labels.includes('levitate breaking change'); - - return doesExist ? 1 : 0; + return labels.some(label => label.name === 'levitate breaking change') ? 1 : 0 # put the markdown into a variable - name: Levitate Markdown @@ -271,22 +262,41 @@ jobs: delete: true GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} - # Posts a notification to Slack if a PR has a breaking change and it did not have a breaking change before - - name: Post to Slack + - name: Send Slack Message via Payload id: slack - if: steps.levitate-run.outputs.exit_code == 1 && steps.does-label-exist.outputs.result == 0 && env.HAS_SECRETS - uses: slackapi/slack-github-action@v1.26.0 + if: steps.levitate-run.outputs.exit_code == 1 && steps.does-label-exist.outputs.result == 0 && github.repository == 'grafana/grafana' + uses: grafana/shared-workflows/actions/send-slack-message@main with: - payload: | + channel-id: "C031SLFH6G0" + payload: | { - "pr_link": "https://github.com/grafana/grafana/pull/${{ steps.levitate-run.outputs.pr_number }}", - "pr_number": "${{ steps.levitate-run.outputs.pr_number }}", - "job_link": "${{ steps.levitate-run.outputs.job_link }}", - "message": "${{ steps.levitate-run.outputs.message }}" + "channel": "C031SLFH6G0", + "text": ":warning: Possible breaking changes detected in *PR:* <${{ github.event.pull_request.html_url }}|#${{ github.event.pull_request.number }} :warning:", + "icon_emoji": ":grot:", + "username": "Levitate Bot", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*grafana/grafana* repository has possible breaking changes" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*PR:* <${{ github.event.pull_request.html_url }}|#${{ github.event.pull_request.number }}>" + }, + { + "type": "mrkdwn", + "text": "*Job:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Job>" + } + ] + } + ] } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_LEVITATE_WEBHOOK_URL }} - HAS_SECRETS: ${{ (github.repository == 'grafana/grafana' || secrets.SLACK_LEVITATE_WEBHOOK_URL != '') || '' }} # Add the label - name: Add "levitate breaking change" label diff --git a/.github/workflows/doc-validator.yml b/.github/workflows/doc-validator.yml index 75b721904cc..0e6f613da9b 100644 --- a/.github/workflows/doc-validator.yml +++ b/.github/workflows/doc-validator.yml @@ -1,13 +1,18 @@ name: "doc-validator" on: - pull_request: - paths: ["docs/sources/**"] workflow_dispatch: + inputs: + include: + description: | + Regular expression that matches paths to include in linting. + + For example: docs/sources/(?:alerting|fundamentals)/.+\.md + required: true jobs: doc-validator: runs-on: "ubuntu-latest" container: - image: "grafana/doc-validator:v5.0.0" + image: "grafana/doc-validator:v5.2.0" steps: - name: "Checkout code" uses: "actions/checkout@v4" @@ -15,15 +20,7 @@ jobs: # Only run doc-validator on specific directories. run: > doc-validator - '--include=^docs/sources/(?:alerting|fundamentals|getting-started|introduction|setup-grafana|upgrade-guide|whatsnew/whats-new-in-v(?:9|10))/.+\.md$' + '--include=${{ inputs.include }}' '--skip-checks=^(?:image.+|canonical-does-not-match-pretty-URL)$' ./docs/sources /docs/grafana/latest - | reviewdog - -f=rdjsonl - --fail-on-error - --filter-mode=nofilter - --name=doc-validator - --reporter=github-pr-review - env: - REVIEWDOG_GITHUB_API_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/scripts/pr-get-job-link.js b/.github/workflows/scripts/pr-get-job-link.js deleted file mode 100644 index b1b49c89941..00000000000 --- a/.github/workflows/scripts/pr-get-job-link.js +++ /dev/null @@ -1,9 +0,0 @@ - -module.exports = async ({ name, github, context, core }) => { - const { owner, repo } = context.repo; - const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs/${context.runId}/jobs` - const result = await github.request(url); - const job = result.data.jobs.find(j => j.name === name); - - core.setOutput('link', `${job.html_url}?check_suite_focus=true`); -} diff --git a/Makefile b/Makefile index 9346e4b9b10..9c8f178a3d9 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ GO_RACE_FLAG := $(if $(GO_RACE),-race) GO_BUILD_FLAGS += $(if $(GO_BUILD_DEV),-dev) GO_BUILD_FLAGS += $(if $(GO_BUILD_TAGS),-build-tags=$(GO_BUILD_TAGS)) GO_BUILD_FLAGS += $(GO_RACE_FLAG) +GIT_BASE = remotes/origin/main # GNU xargs has flag -r, and BSD xargs (e.g. MacOS) has that behaviour by default XARGSR = $(shell xargs --version 2>&1 | grep -q GNU && echo xargs -r || echo xargs) @@ -308,7 +309,7 @@ lint-go: golangci-lint ## Run all code checks for backend. You can use GO_LINT_F .PHONY: lint-go-diff lint-go-diff: $(GOLANGCI_LINT) - git diff --name-only remotes/origin/main | \ + git diff --name-only $(GIT_BASE) | \ grep '\.go$$' | \ $(XARGSR) dirname | \ sort -u | \ diff --git a/docs/sources/dashboards/create-reports/index.md b/docs/sources/dashboards/create-reports/index.md index 29beabb4794..272b948d796 100644 --- a/docs/sources/dashboards/create-reports/index.md +++ b/docs/sources/dashboards/create-reports/index.md @@ -39,6 +39,11 @@ refs: destination: /docs/grafana//administration/roles-and-permissions/access-control/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//administration/roles-and-permissions/access-control/ + permission: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/roles-and-permissions/ role-based-access-control: - pattern: /docs/grafana/ destination: /docs/grafana//administration/roles-and-permissions/access-control/ @@ -109,7 +114,7 @@ For information about recent improvements to the reporting UI, refer to [Grafana ## Access control -When [RBAC](ref:rbac) is enabled, you need to have the relevant [Permissions][] to create and manage reports. +When [RBAC](ref:rbac) is enabled, you need to have the relevant [Permissions](ref:permission) to create and manage reports. ## Create or update a report diff --git a/docs/sources/datasources/tempo/configure-tempo-data-source.md b/docs/sources/datasources/tempo/configure-tempo-data-source.md index 1a85b2357af..9505f33bd41 100644 --- a/docs/sources/datasources/tempo/configure-tempo-data-source.md +++ b/docs/sources/datasources/tempo/configure-tempo-data-source.md @@ -123,30 +123,25 @@ For additional information on setting up TLS encryption with Tempo, refer to [Co ## Streaming - - Streaming enables TraceQL query results to be displayed as they become available. Without streaming, no results are displayed until all results have returned. -{{< docs/public-preview product="TraceQL streaming results" >}} - To use streaming, you need to: -- Run Tempo version 2.2 or newer, or Grafana Enterprise Traces (GET) version 2.2 or newer, or use Grafana Cloud Traces. -- For self-managed Tempo or GET instances: If your Tempo or GET instance is behind a load balancer or proxy that doesn't supporting gRPC or HTTP2, streaming may not work and should be disabled. +- Run Tempo version 2.2 or later, or Grafana Enterprise Traces (GET) version 2.2 or later, or use Grafana Cloud Traces. +- Tempo must have `stream_over_http_enabled: true` for streaming to work. + + For more information, refer to [Tempo GRPC API](https://grafana.com/docs/tempo//api_docs/#tempo-grpc-api). + +- For self-managed Tempo or GET instances: If your Tempo or GET instance is behind a load balancer or proxy that doesn't supporting gRPC or HTTP2, streaming may not work and should be deactivated. ### Activate streaming -You can activate streaming by either setting the `traceQLStreaming` [feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/) to true or by activating the **Streaming** toggle in the Tempo data source. +Streaming is available in Grafana and Grafana Cloud. +You can activate streaming by turning the **Streaming** toggle to on in the Tempo data source. ![Streaming section in Tempo data source](/media/docs/grafana/data-sources/tempo-data-source-streaming-v11.2.png) -If you are using Grafana Cloud, the `traceQLStreaming` feature toggle is already set to `true` by default. - -If the Tempo data source is set to allow streaming but the `traceQLStreaming` feature toggle is set to `false` in Grafana, streaming occurs. - -If the data source has streaming disabled and `traceQLStreaming` is set to `true`, streaming happens for that data source. - When streaming is active, it's shows as **Enabled** in **Explore**. To check the status, select Explore in the menu, select your Tempo data source, and expand the **Options** section. diff --git a/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md b/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md index dd297b5503e..f4059970634 100644 --- a/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md +++ b/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md @@ -39,6 +39,12 @@ You can use a bar gauge visualization when you need to show: - Attendance - Process completion rates +## Configure a bar gauge visualization + +The following video shows you how to create and configure a bar gauge visualization: + +{{< youtube id="7PhDysObEXA" >}} + {{< docs/play title="Bar Gauge" url="https://play.grafana.org/d/vmie2cmWz/" >}} ## Supported data formats 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 ef8bc6a3396..46c13858bb3 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -191,11 +191,10 @@ Experimental features might be changed or removed without prior notice. | `queryLibrary` | Enables Query Library feature in Explore | | `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | | `alertingListViewV2` | Enables the new alert list view design | -| `dashboardRestore` | Enables deleted dashboard restore feature (backend only) | +| `dashboardRestore` | Enables deleted dashboard restore feature | | `alertingCentralAlertHistory` | Enables the new central alert history. | | `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | | `alertingApiServer` | Register Alerting APIs with the K8s API server | -| `dashboardRestoreUI` | Enables the frontend to be able to restore a recently deleted dashboard | | `dataplaneAggregator` | Enable grafana dataplane aggregator | | `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | | `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | diff --git a/docs/sources/setup-grafana/configure-security/audit-grafana.md b/docs/sources/setup-grafana/configure-security/audit-grafana.md index 981eddbbd08..9f421becc5f 100644 --- a/docs/sources/setup-grafana/configure-security/audit-grafana.md +++ b/docs/sources/setup-grafana/configure-security/audit-grafana.md @@ -48,7 +48,7 @@ Audit logs contain the following fields. The fields followed by **\*** are alway | `user.orgId`\* | number | Current organization of the user that made the request. | | `user.orgRole` | string | Current role of the user that made the request. | | `user.name` | string | Name of the Grafana user that made the request. | -| `user.tokenId` | number | ID of the user authentication token. | +| `user.authTokenId` | number | ID of the user authentication token. | | `user.apiKeyId` | number | ID of the Grafana API key used to make the request. | | `user.isAnonymous`\* | boolean | If an anonymous user made the request, `true`. Otherwise, `false`. | | `action`\* | string | The request action. For example, `create`, `update`, or `manage-permissions`. | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md index 29ef9f04975..11b405c1959 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md @@ -231,21 +231,34 @@ This app registration will be used as a Service Account to retrieve more informa 1. Go to the [Azure portal](https://portal.azure.com/#home) and sign in with your Azure AD account. 1. In the left-hand navigation pane, select the Azure Active Directory service, and then select **App registrations**. -1. Select **New registration**. +1. Click the **New registration** button. 1. In the **Register an application** pane, enter a name for the application. 1. In the **Supported account types** section, select the account types that can use the application. 1. In the **Redirect URI** section, select Web and enter `https://localhost/login/azuread`. -1. Select **Register**. +1. Click the **Register** button. #### Set up permissions for the application 1. In the overview pane, look for **API permissions** section and select **Add a permission**. 1. In the **Request API permissions** pane, select **Microsoft Graph**, and click **Application permissions**. 1. In the **Select permissions** pane, under the **GroupMember** section, select **GroupMember.Read.All**. +1. In the **Select permissions** pane, under the **User** section, select **User.Read.All**. +1. Click the **Add permissions** button at the bottom of the page. +1. In the **Request API permissions** pane, select **Microsoft Graph**, and click **Delegated permissions**. 1. In the **Select permissions** pane, under the **User** section, select **User.Read**. -1. Select **Add permissions** at the bottom of the page. +1. Click the **Add permissions** button at the bottom of the page. 1. In the **API permissions** section, select **Grant admin consent for **. +The following table shows what the permissions look like from the Azure AD portal: + +| Permissions name | Type | Admin consent required | Status | +| ---------------- | ----------- | ---------------------- | ------- | +| `Group.Read.All` | Application | Yes | Granted | +| `User.Read` | Delegated | No | Granted | +| `User.Read.All` | Application | Yes | Granted | + +{{< figure src="/media/docs/grafana/saml/graph-api-app-permissions.png" caption="Screen shot of the permissions listed in Azure AD for the App registration" >}} + #### Generate a client secret 1. In the **Overview** pane, select **Certificates & secrets**. diff --git a/docs/sources/setup-grafana/installation/helm/index.md b/docs/sources/setup-grafana/installation/helm/index.md index 566f73d5610..d34e5c35f0e 100644 --- a/docs/sources/setup-grafana/installation/helm/index.md +++ b/docs/sources/setup-grafana/installation/helm/index.md @@ -278,6 +278,67 @@ To install plugins in the Grafana Helm Charts, complete the following steps: 1. Search for the above plugins and they should be marked as installed. +### Configure a Private CA (Certificate Authority) + +In many enterprise networks, TLS certificates are issued by a private certificate authority and are not trusted by default (using the provided OS trust chain). + +If your Grafana instance needs to interact with services exposing certificates issued by these private CAs, then you need to ensure Grafana trusts the root certificate. + +You might need to configure this if you: + +- have plugins that require connectivity to other self hosted systems. For example, if you've installed the Grafana Enterprise Metrics, Logs, or Traces (GEM, GEL, GET) plugins, and your GEM (or GEL/GET) cluster is using a private certificate. +- want to connect to data sources which are listening on HTTPS with a private certificate. +- are using a backend database for persistence, or caching service that uses private certificates for encryption in transit. + +In some cases you can specify a self-signed certificate within Grafana (such as in some data sources), or choose to skip TLS certificate validation (this is not recommended unless absolutely necessary). + +A simple solution which should work across your entire instance (plugins, data sources, and backend connections) is to add your self-signed CA certificate to your Kubernetes deployment. + +1. Create a ConfigMap containing the certificate, and deploy it to your Kubernetes cluster + + ```yaml + # grafana-ca-configmap.yaml + --- + apiVersion: v1 + kind: ConfigMap + metadata: + name: grafana-ca-cert + data: + ca.pem: | + -----BEGIN CERTIFICATE----- + (rest of the CA cert) + -----END CERTIFICATE----- + ``` + + ```bash + kubectl apply --filename grafana-ca-configmap.yaml --namespace monitoring + ``` + +1. Open the Helm `values.yaml` file in your favorite editor. + +1. Find the line that says `extraConfigmapMounts:` and under that section, specify the additional ConfigMap that you want to mount. + + ```yaml + ....... + ............ + ...... + extraConfigmapMounts: + - name: ca-certs-configmap + mountPath: /etc/ssl/certs/ca.pem + subPath: ca.pem + configMap: grafana-ca-cert + readOnly: true + ....... + ............ + ...... + ``` + +1. Save the changes and use the `helm upgrade` command to update your Grafana deployment and mount the new ConfigMap: + + ```bash + helm upgrade my-grafana grafana/grafana --values values.yaml --namespace monitoring + ``` + ## Troubleshooting This section includes troubleshooting tips you might find helpful when deploying Grafana on Kubernetes via Helm. diff --git a/docs/sources/shared/datasources/tempo-editor-traceql.md b/docs/sources/shared/datasources/tempo-editor-traceql.md index 8b47093f616..4a63ecb651b 100644 --- a/docs/sources/shared/datasources/tempo-editor-traceql.md +++ b/docs/sources/shared/datasources/tempo-editor-traceql.md @@ -129,10 +129,8 @@ Using the **Spans** option makes it easier access the spans to apply transformat The Tempo data source supports streaming responses to TraceQL queries so you can see partial query results as they come in without waiting for the whole query to finish. -{{% admonition type="note" %}} -To use this feature in Grafana OSS v10.1 and later, enable the `traceQLStreaming` feature toggle. This capability is enabled by default in Grafana Cloud. -{{% /admonition %}} - Streaming is available for both the **Search** and **TraceQL** query types, and you'll get immediate visibility of incoming traces on the results table. +To learn how to activate streaming, refer to [Streaming](https://grafana.com/docs/grafana//datasources/tempo/configure-tempo-data-source/#streaming) in the Tempo data source documentation. + {{< video-embed src="/media/docs/grafana/data-sources/tempo-streaming-v2.mp4" >}} diff --git a/docs/sources/shared/datasources/tempo-search-traceql.md b/docs/sources/shared/datasources/tempo-search-traceql.md index 5f5f19b034e..e51a25860f5 100644 --- a/docs/sources/shared/datasources/tempo-search-traceql.md +++ b/docs/sources/shared/datasources/tempo-search-traceql.md @@ -167,12 +167,9 @@ Selecting a Trace ID (2 in the screenshot) displays more detailed information (3 The Tempo data source supports streaming responses to TraceQL queries so you can see partial query results as they come in without waiting for the whole query to finish. -{{% admonition type="note" %}} -To use this public preview feature, enable the `traceQLStreaming` feature toggle. -When active, all configured Tempo data sources will attempt to use streaming. -You can control which Tempo data sources do and don't attempt to stream results at the per-data source level using the **Streaming** section of the Tempo data source configuration. - -{{% /admonition %}} +When active, all configured Tempo data sources attempt to use streaming. +You can activate and control which Tempo data sources do and don't attempt to stream results at the per-data source level using the **Streaming** section of the Tempo data source configuration. +For more information, refer to the [Tempo data source](https://grafana.com/docs/grafana//datasources/tempo/configure-tempo-data-source/#streaming) documentation. Streaming is available for both the **Search** and **TraceQL** query types. You'll get immediate visibility of incoming traces on the results table. diff --git a/docs/sources/upgrade-guide/_index.md b/docs/sources/upgrade-guide/_index.md index 390c81e965c..874374740f4 100644 --- a/docs/sources/upgrade-guide/_index.md +++ b/docs/sources/upgrade-guide/_index.md @@ -22,7 +22,7 @@ We recommend that you upgrade Grafana often to stay current with the latest fixe Because Grafana upgrades are backward compatible, the upgrade process is straightforward, and dashboards and graphs will not change. -To learn what's available in a Grafana release, refer to [What's New ]({{< relref "../whatsnew" >}}). +To learn what's available in a Grafana release, refer to [What's New]({{< relref "../whatsnew" >}}). Refer to any of the following upgrade guides: diff --git a/docs/sources/upgrade-guide/when-to-upgrade/index.md b/docs/sources/upgrade-guide/when-to-upgrade/index.md new file mode 100644 index 00000000000..33fe3211ed6 --- /dev/null +++ b/docs/sources/upgrade-guide/when-to-upgrade/index.md @@ -0,0 +1,115 @@ +--- +description: Strategies for upgrading your self-managed Grafana instance +keywords: + - grafana + - configuration + - documentation + - upgrade +title: Strategies for upgrading your self-managed Grafana instance +menuTitle: Upgrade strategies +weight: 1000 +--- + +# Strategies for upgrading your self-managed Grafana instance + +At Grafana Labs, we believe in shipping features early and often, and in recent years we’ve increased our commitment to that philosophy. + +We no longer wait for the yearly major release to give you access to the next big improvement. Instead, we regularly make new features, bug fixes, and security patches available to our self-managing users ([Grafana OSS](https://grafana.com/oss/grafana/) and [Grafana Enterprise](https://grafana.com/products/enterprise/)) throughout the year. + +Having a dependable release process provides users like you with the best Grafana experience possible, and it provides the flexibility to upgrade in a manner that works best for you and your organization. + +## What to expect from each release type + +We split Grafana OSS and Grafana Enterprise releases into three main categories: + +- **Minor release (every other month)**: These releases can include new features, deprecation notices, notices about upcoming breaking changes, previously announced breaking changes, bug fixes, and security vulnerability patches. +- **Major release (once a year, in April/May)**: These are like a minor release, but accompanied by [GrafanaCON](https://grafana.com/events/grafanacon/) and a comprehensive upgrade guide for users who like to upgrade only once a year. +- **Patching release (every month)**: These include bug fixes for currently supported versions, as well as any security vulnerability patches. + +You can choose your cadence: For frequent self-managed updates, you should follow the minor release (for example, upgrade 10.1 to 10.2), which also gives you access to the latest features. If you need a longer period to review our new releases, you should follow the major releases. Both strategies get patching releases with security fixes (high severity security fixes also result in ad-hoc patch releases). We’ll get into additional guidance on upgrade cadences later in this guide. + +## How to find the specifics for a release + +We love sharing all our great features with you so you can leverage Grafana to its fullest. We also understand that great release documentation allows you to upgrade with confidence. +Whether it’s knowing that a bug has been fixed, seeing that a security vulnerability is patched, or understanding how to mitigate the impact of breaking changes, proper documentation allows you to make informed decisions about when to upgrade your local Grafana instances. + +We provide release documentation in multiple places to address different needs: + +- [**What’s new**](https://grafana.com/docs/grafana/latest/whatsnew/?pg=blog&plcmt=body-txt) outlines new features debuting in each major and minor release. +- [**Breaking changes**](https://grafana.com/docs/grafana/latest/breaking-changes/?pg=blog&plcmt=body-txt) notify you of updates included in major releases that could impact you and provide mitigation recommendations when needed. +- [**Upgrade guides**](https://grafana.com/docs/grafana/latest/upgrade-guide/?pg=blog&plcmt=body-txt) instruct you on how to upgrade to a newer minor or major version. +- And finally, a [**changelog**](https://github.com/grafana/grafana/blob/main/CHANGELOG.md) is generated for every release (major, minor, patching, security) and outlines all changes included in that release. + +## When to expect releases + +Currently, Grafana is on a monthly release cycle. Here’s a look at scheduled releases for 2024: + +| **Anticipated release date** | **Grafana versions** | **Release type** | +| ---------------------------- | -------------------- | ------------------ | +| May 14, 2024 | 11 | Major and patching | +| June 25, 2024 | 11.1 | Minor and patching | +| July 23, 2024 | 11.1.x | Patching | +| Aug. 27, 2024 | 11.2 | Minor and patching | +| Sept. 24, 2024 | 11.2.x | Patching | +| Oct. 22, 2024 | 11.3 | Minor and patching | +| November/December 2024 | To be determined | To be determined | + +### A few important notes + +- The schedule above outlines how we plan release dates. However, unforeseen events and circumstances may cause dates to change. +- High severity security and feature degradation incidents will result in ad-hoc releases that are not scheduled ahead of time. +- Patching releases are for the current (last released) minor version of Grafana. Additional older versions of Grafana may be included if there is a critical bug or security vulnerability that needs to be patched. +- A Grafana release freeze occurs for a week in November and again during the end of December. This does not apply to changes that may be required during the course of an operational or security incident. + +## What to know about version support + +Self-managed Grafana users have control over when they upgrade to a new version of Grafana. To help you make an informed decision about whether it's time to upgrade, it’s important that you understand the level of support provided for your current version. + +For self-managed Grafana (both Enterprise and OSS), the support for versions is as follows: + +- Support for each minor release extends to nine months after the release date. +- Support for the last minor release of a major version is extended an additional six months, for a total of 15 months of support after the release date. + +Here is an overview of projected version support through 2024: + +| **Version** | **Release date** | **Support end of life (EOL)** | +| ------------------------- | ----------------------- | ----------------------------- | +| 10.3 | January 2024 | October 2024 | +| 10.4 (Last minor of 10.0) | March 2024 | June 2025 (extended support) | +| 11.0 | May 2024 | February 2025 | +| 11.1 | June 2024 | March 2025 | +| 11.2 | August 2024 (tentative) | May 2025 (tentative) | + +{{< admonition type="note" >}} +Grafana 9.5.x was the last supported minor for the 9.0 major release and is no longer supported as of July 2024. +{{< /admonition >}} + +## How are these versions supported? + +The level of support changes as new versions of Grafana are released. Here are a few details to keep in mind: + +- The current (most recently released) version of Grafana gets the highest level of support. Releases for this version include all the new features along with all bug fixes. +- All supported versions receive security patches for vulnerabilities impacting that version. +- All supported versions receive patches for bugs that cause critical feature degradation incidents. + +Keeping all this in mind, users that want to receive the most recent features and all bug fixes should be on the current (most recently released) version of Grafana. + +### What is a critical feature degradation? + +A critical feature degradation usually meets one of the following criteria: + +- Major functionality is universally unavailable (for example, cannot create dashboards, unable to authenticate). +- Major (critical) impact to a significant amount of customers. +- Major escalated incident for one or many customers. + +## Self-managing upgrade strategies + +Based on your needs, you can choose your ideal upgrade strategy. Here’s what that might look like in practice: + +| **Strategy/cadence** | **Advantages/disadvantages** | **Example upgrade procedure** | +| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Minor / bi-monthly (11.1 to 11.2)** | Our recommended strategy. It combines up-to-date, secure releases with access to latest features as soon as they're released.
  • Small changelog to review
  • Highest compatibility with actively maintained plugins
  • Easy migration to [Grafana Cloud](https://grafana.com/products/cloud)
|
  • **June 2024**: You review the 11.1 changelog and deploy the release to testing
  • **July 2024**: You deploy 11.1 to production
  • **August 2024**: 11.2 is released
| +| **Major / yearly (10.0 to 11.0)** | Yearly upgrade path that still gives access to up-to-date features presented at GrafanaCON.
  • Big changelog to review
  • High compatibility with plugins
  • Relatively easy migration to [Grafana Cloud](https://grafana.com/products/cloud)
|
  • **April 2024**: 11.0 is released, you start a big changelog review
  • **May 2024**: You deploy 11.0 to testing
  • **June 2024**: You deploy 11.0 to production
  • **April 2025**: 12.0 is released
| +| **Previous major / yearly (9.5 to 10.4)** | Release with extended support timeline
  • Limited compatibility with actively developed plugins
  • Big changelog to review
  • Migrations to Grafana Cloud might require professional support
|
  • **April 2024**: 11.0 is released, marking the previous minor (10.4.x) with extended support, you start a big changelog review (9.5.x to 10.4.x)
  • **May 2024**: You deploy 10.4.x to testing
  • **June 2024**: You deploy 10.4.x to production
| + +For each strategy, you should stay informed about patch releases that fix security vulnerabilities (released monthly, plus ad-hoc releases). Follow the “minor” strategy for the most flexibility, as you can also occasionally lengthen the cadence to a full quarter and still rely on your currently deployed minor release being supported with security fixes. diff --git a/e2e/dashboards-suite/dashboard-share-externally-create.spec.ts b/e2e/dashboards-suite/dashboard-share-externally-create.spec.ts index 39e01c4e60f..6548198013d 100644 --- a/e2e/dashboards-suite/dashboard-share-externally-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-share-externally-create.spec.ts @@ -22,7 +22,9 @@ describe('Shared dashboards', () => { e2e.pages.ShareDashboardDrawer.ShareExternally.container().should('not.exist'); }); - it('Create a shared dashboard and check API', () => { + // Skipping due to being a flaky test + // https://drone.grafana.net/grafana/grafana/201217/6/14 + it.skip('Create a shared dashboard and check API', () => { openDashboard(); // Open share externally drawer @@ -111,7 +113,7 @@ describe('Shared dashboards', () => { }); }); - it('Disable a shared dashboard', () => { + it.skip('Disable a shared dashboard', () => { openDashboard(); //TODO Failing in CI/CD. Fix it diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e/test-plugins/grafana-extensionstest-app/package.json index f7577e54dd5..fc38d4f3e24 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e/test-plugins/grafana-extensionstest-app/package.json @@ -1,6 +1,6 @@ { "name": "@test-plugins/extensions-test-app", - "version": "1.0.0", + "version": "11.4.0-pre", "private": true, "scripts": { "build": "webpack -c ./webpack.config.ts --env production", @@ -12,7 +12,7 @@ "license": "Apache-2.0", "devDependencies": { "@grafana/eslint-config": "7.0.0", - "@grafana/plugin-configs": "11.3.0-pre", + "@grafana/plugin-configs": "11.4.0-pre", "@types/lodash": "4.17.7", "@types/node": "20.14.14", "@types/prismjs": "1.26.4", diff --git a/go.mod b/go.mod index 7d9993fcdc4..5ab6b92c69d 100644 --- a/go.mod +++ b/go.mod @@ -27,11 +27,10 @@ require ( github.com/Masterminds/semver v1.5.0 // @grafana/grafana-backend-group github.com/Masterminds/semver/v3 v3.2.0 // @grafana/grafana-release-guild github.com/Masterminds/sprig/v3 v3.2.3 // @grafana/grafana-backend-group - github.com/Masterminds/squirrel v1.5.4 // @grafana/identity-access-team github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // @grafana/plugins-platform-backend github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // @grafana/grafana-backend-group github.com/alicebob/miniredis/v2 v2.33.0 // @grafana/alerting-backend - github.com/andybalholm/brotli v1.0.6 // @grafana/partner-datasources + github.com/andybalholm/brotli v1.1.0 // @grafana/partner-datasources github.com/apache/arrow/go/v15 v15.0.2 // @grafana/observability-metrics github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad github.com/aws/aws-sdk-go v1.55.5 // @grafana/aws-datasources @@ -73,7 +72,7 @@ require ( github.com/googleapis/gax-go/v2 v2.13.0 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240930154843-22cee00b280e // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20241010165806-807ddf183724 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20240919120951-58259833c564 // @grafana/identity-access-team github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad @@ -123,21 +122,20 @@ require ( github.com/mattn/go-isatty v0.0.20 // @grafana/grafana-backend-group github.com/mattn/go-sqlite3 v1.14.22 // @grafana/grafana-backend-group github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-backend - github.com/microsoft/go-mssqldb v1.7.0 // @grafana/grafana-bi-squad + github.com/microsoft/go-mssqldb v1.7.2 // @grafana/grafana-bi-squad github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c //@grafana/identity-access-team github.com/mocktools/go-smtp-mock/v2 v2.3.1 // @grafana/grafana-backend-group github.com/modern-go/reflect2 v1.0.2 // @grafana/alerting-backend github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // @grafana/alerting-backend github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // @grafana/grafana-operator-experience-squad github.com/oapi-codegen/oapi-codegen/v2 v2.3.0 // @grafana/grafana-as-code - github.com/oklog/ulid/v2 v2.1.0 // @grafana/identity-access-team github.com/olekukonko/tablewriter v0.0.5 // @grafana/grafana-backend-group - github.com/openfga/api/proto v0.0.0-20240529184453-5b0b4941f3e0 // @grafana/identity-access-team - github.com/openfga/language/pkg/go v0.0.0-20240409225820-a53ea2892d6d // @grafana/identity-access-team - github.com/openfga/openfga v1.5.4 // @grafana/identity-access-team + github.com/openfga/api/proto v0.0.0-20240906203051-102620ef2a66 // @grafana/identity-access-team + github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20240926131254-992b301a003f // @grafana/identity-access-team + github.com/openfga/openfga v1.6.2 // @grafana/identity-access-team github.com/patrickmn/go-cache v2.1.0+incompatible // @grafana/alerting-backend github.com/prometheus/alertmanager v0.27.0 // @grafana/alerting-backend - github.com/prometheus/client_golang v1.20.3 // @grafana/alerting-backend + github.com/prometheus/client_golang v1.20.4 // @grafana/alerting-backend github.com/prometheus/client_model v0.6.1 // @grafana/grafana-backend-group github.com/prometheus/common v0.55.0 // @grafana/alerting-backend github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 // @grafana/alerting-backend @@ -158,21 +156,21 @@ require ( github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // @grafana/grafana-operator-experience-squad github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group go.opentelemetry.io/collector/pdata v1.6.0 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // @grafana/plugins-platform-backend + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // @grafana/plugins-platform-backend go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 // @grafana/grafana-operator-experience-squad go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel v1.30.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/sdk v1.29.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/sdk v1.30.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/trace v1.30.0 // @grafana/grafana-backend-group go.uber.org/atomic v1.11.0 // @grafana/alerting-backend go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage gocloud.dev v0.39.0 // @grafana/grafana-app-platform-squad golang.org/x/crypto v0.27.0 // @grafana/grafana-backend-group - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // @grafana/alerting-backend + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // @grafana/alerting-backend golang.org/x/mod v0.20.0 // indirect; @grafana/grafana-backend-group golang.org/x/net v0.29.0 // @grafana/oss-big-tent @grafana/partner-datasources golang.org/x/oauth2 v0.23.0 // @grafana/identity-access-team @@ -180,9 +178,9 @@ require ( golang.org/x/text v0.18.0 // @grafana/grafana-backend-group golang.org/x/time v0.6.0 // @grafana/grafana-backend-group golang.org/x/tools v0.24.0 // @grafana/grafana-as-code - gonum.org/v1/gonum v0.14.0 // @grafana/observability-metrics + gonum.org/v1/gonum v0.15.1 // @grafana/observability-metrics google.golang.org/api v0.191.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.66.0 // @grafana/plugins-platform-backend + google.golang.org/grpc v1.67.0 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.34.2 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group @@ -224,12 +222,13 @@ require ( github.com/FZambia/eagle v0.1.0 // indirect github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/RoaringBitmap/roaring v1.9.3 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/thrift v0.20.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect @@ -269,7 +268,7 @@ require ( github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/proto v1.10.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -293,7 +292,7 @@ require ( github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/cel-go v0.20.1 // indirect + github.com/google/cel-go v0.21.0 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -306,10 +305,8 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect; @grafana/plugins-platform-backend github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // @grafana/identity-access-team github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect - github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/go-sockaddr v1.0.6 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect @@ -321,9 +318,9 @@ require ( github.com/invopop/jsonschema v0.12.0 // indirect github.com/invopop/yaml v0.3.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect @@ -377,15 +374,16 @@ require ( github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/pressly/goose/v3 v3.20.0 // indirect + github.com/pressly/goose/v3 v3.22.1 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.11.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -401,14 +399,14 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/encoding v0.4.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/sethvargo/go-retry v0.2.4 // indirect - github.com/shopspring/decimal v1.3.1 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/viper v1.18.2 // indirect + github.com/spf13/viper v1.19.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect @@ -440,8 +438,8 @@ require ( golang.org/x/term v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect; @grafana/grafana-backend-group - google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -449,10 +447,10 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/kms v0.31.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect - modernc.org/libc v1.41.0 // indirect + modernc.org/libc v1.55.3 // indirect modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.7.2 // indirect - modernc.org/sqlite v1.29.6 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.33.1 // indirect modernc.org/strutil v1.2.0 // indirect modernc.org/token v1.1.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect @@ -484,6 +482,7 @@ require ( cloud.google.com/go/longrunning v0.5.12 // indirect github.com/at-wat/mqtt-go v0.19.4 // indirect github.com/dolthub/maphash v0.1.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/gammazero/deque v0.2.1 // indirect github.com/grafana/grafana-app-sdk v0.19.0 // indirect github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 // indirect diff --git a/go.sum b/go.sum index 58deaa35524..f707fdc6534 100644 --- a/go.sum +++ b/go.sum @@ -1334,8 +1334,6 @@ cloud.google.com/go/workflows v1.12.1/go.mod h1:5A95OhD/edtOhQd/O741NSfIMezNTbCw cloud.google.com/go/workflows v1.12.2/go.mod h1:+OmBIgNqYJPVggnMo9nqmizW0qEXHhmnAzK/CnBqsHc= cloud.google.com/go/workflows v1.12.3/go.mod h1:fmOUeeqEwPzIU81foMjTRQIdwQHADi/vEr1cx9R1m5g= cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9pAMX51vpwB/w= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg= filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE= @@ -1389,8 +1387,6 @@ github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7Og github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= @@ -1453,8 +1449,6 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4 github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= -github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= @@ -1502,11 +1496,11 @@ github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUi github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= -github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= @@ -1723,8 +1717,8 @@ github.com/cncf/xds/go v0.0.0-20230428030218-4003588d1b74/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= -github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= -github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= @@ -1734,9 +1728,6 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE github.com/containerd/cgroups/v3 v3.0.1/go.mod h1:/vtwk1VXrtoa5AaZLkypuOJgA/6DyPMZHJPGQNtlHnw= github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= -github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -1749,8 +1740,6 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= @@ -1805,8 +1794,8 @@ github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc github.com/docker/docker v0.7.3-0.20190103212154-2b7e084dc98b/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v0.7.3-0.20190817195342-4760db040282/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v26.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v26.0.2+incompatible h1:yGVmKUFGgcxA6PXWAokO0sQL22BrQ67cgVjko8tGdXE= -github.com/docker/docker v26.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= @@ -1839,6 +1828,8 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw= github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -1854,8 +1845,8 @@ github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCw github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= -github.com/envoyproxy/go-control-plane v0.12.1-0.20240621013728-1eb8caab5155 h1:IgJPqnrlY2Mr4pYB6oaMKvFvwJ9H+X6CCY5x1vCTcpc= -github.com/envoyproxy/go-control-plane v0.12.1-0.20240621013728-1eb8caab5155/go.mod h1:5Wkq+JduFtdAXihLmeTJf+tRYIT4KBc2vPXDhwVo1pA= +github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= @@ -1863,8 +1854,9 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= -github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= @@ -1953,8 +1945,6 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/analysis v0.21.5/go.mod h1:25YcZosX9Lwz2wBsrFrrsL8bmjjXdlyP6zsr2AMy29M= github.com/go-openapi/analysis v0.22.0/go.mod h1:acDnkkCI2QxIo8sSIPgmp1wUlRohV7vfGtAIVae73b0= github.com/go-openapi/analysis v0.22.2/go.mod h1:pDF4UbZsQTo/oNuRfAWWd4dAh4yuYf//LYorPTjrpvo= @@ -2124,8 +2114,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= -github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= +github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= @@ -2257,8 +2247,8 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240930154843-22cee00b280e h1:RttFYx5+RTNuMPlaftx8i9f91kwUi9LdxsoPLHnticU= -github.com/grafana/alerting v0.0.0-20240930154843-22cee00b280e/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20241010165806-807ddf183724 h1:u+ZM5TLkdeEoSWXgYWxc4XRfPHhXpR63MyHXJxbBLrc= +github.com/grafana/alerting v0.0.0-20241010165806-807ddf183724/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20240919120951-58259833c564 h1:zYF/RBulpvMqPYR3gbzJZ8t/j/Eymn5FNidSYkueNCA= github.com/grafana/authlib v0.0.0-20240919120951-58259833c564/go.mod h1:PFzXbCrn0GIpN4KwT6NP1l5Z1CPLfmKHnYx8rZzQcyY= github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= @@ -2396,8 +2386,8 @@ github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1 github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= -github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= -github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= @@ -2490,13 +2480,13 @@ github.com/ionos-cloud/sdk-go/v6 v6.1.11/go.mod h1:EzEgRIDxBELvfoa/uBN0kOQaqovLj github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= @@ -2621,8 +2611,6 @@ github.com/linkedin/goavro/v2 v2.10.0 h1:eTBIRoInBM88gITGXYtUSqqxLTFXfOsJBiX8ZMW github.com/linkedin/goavro/v2 v2.10.0/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= github.com/linode/linodego v1.32.0 h1:OmZzB3iON6uu84VtLFf64uKmAQqJJarvmsVguroioPI= github.com/linode/linodego v1.32.0/go.mod h1:y8GDP9uLVH4jTB9qyrgw79qfKdYJmNCGUOJmfuiOcmI= -github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY= -github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= @@ -2690,8 +2678,8 @@ github.com/maypok86/otter v1.2.2 h1:jJi0y8ruR/ZcKmJ4FbQj3QQTqKwV+LNrSOo2S1zbF5M= github.com/maypok86/otter v1.2.2/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs= -github.com/microsoft/go-mssqldb v1.7.0/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -2750,18 +2738,10 @@ github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QR github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= -github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/mocktools/go-smtp-mock/v2 v2.3.1 h1:wq75NDSsOy5oHo/gEQQT0fRRaYKRqr1IdkjhIPXxagM= github.com/mocktools/go-smtp-mock/v2 v2.3.1/go.mod h1:h9AOf/IXLSU2m/1u4zsjtOM/WddPwdOUBz56dV9f81M= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -2778,7 +2758,6 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJ github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= @@ -2885,12 +2864,12 @@ github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/openfga/api/proto v0.0.0-20240529184453-5b0b4941f3e0 h1:tSJl/atdqsDACjRQQPCbXe2GfEvcOkhdrUNmDToAjTA= -github.com/openfga/api/proto v0.0.0-20240529184453-5b0b4941f3e0/go.mod h1:XnvYrdU//9i70Aou6n4H5DJ0bdRPB3IlmE/Vx6qhnm8= -github.com/openfga/language/pkg/go v0.0.0-20240409225820-a53ea2892d6d h1:n44DfITs+CLCYJIgsryJkG2ElwOZJ3huekPZKydPi7U= -github.com/openfga/language/pkg/go v0.0.0-20240409225820-a53ea2892d6d/go.mod h1:wkI4GcY3yNNuFMU2ncHPWqBaF7XylQTkJYfBi2pIpK8= -github.com/openfga/openfga v1.5.4 h1:mVrp0uB9jNWX/5+OtZLM6YOx5Y9Y4r/D/O+LNBF/FGQ= -github.com/openfga/openfga v1.5.4/go.mod h1:+PoZg9BJeq+h3L0eR52tqNTwghSapCFtmaVKHsUK7QM= +github.com/openfga/api/proto v0.0.0-20240906203051-102620ef2a66 h1:pAYrdyIKxPsMs/nRcTEnS9za2io11g7Rt7ng7HK82hk= +github.com/openfga/api/proto v0.0.0-20240906203051-102620ef2a66/go.mod h1:gil5LBD8tSdFQbUkCQdnXsoeU9kDJdJgbGdHkgJfcd0= +github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20240926131254-992b301a003f h1:ZMZ7ntMnaHIPZxvVQv/aqC4ctzLqH+9Fqn4uw35kQpk= +github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20240926131254-992b301a003f/go.mod h1:ll/hN6kS4EE6B/7J/PbZqac9Nuv7ZHpI+Jfh36JLrbs= +github.com/openfga/openfga v1.6.2 h1:tHBAgiCPomCZb3IH0CFqOpTDVDA/Xb8kG87oX4JCbXc= +github.com/openfga/openfga v1.6.2/go.mod h1:jzbEpheazf6MFjtanQt1rpxexSRzfa9057F7JlkMv2I= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w= github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= @@ -2919,8 +2898,8 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= @@ -2956,11 +2935,9 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= -github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/pressly/goose/v3 v3.20.0 h1:uPJdOxF/Ipj7ABVNOAMJXSxwFXZGwMGHNqjC8e61VA0= -github.com/pressly/goose/v3 v3.20.0/go.mod h1:BRfF2GcG4FTG12QfdBVy3q1yveaf4ckL9vWwEcIO3lA= +github.com/pressly/goose/v3 v3.22.1 h1:2zICEfr1O3yTP9BRZMGPj7qFxQ+ik6yeo+z1LMuioLc= +github.com/pressly/goose/v3 v3.22.1/go.mod h1:xtMpbstWyCpyH+0cxLTMCENWBG+0CSxvTsXhW95d5eo= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -2977,8 +2954,8 @@ github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= -github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= -github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -3093,19 +3070,15 @@ github.com/segmentio/encoding v0.4.0 h1:MEBYvRqiUB2nfR2criEXWqwdY6HJOUrCn5hboVOV github.com/segmentio/encoding v0.4.0/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec= -github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b h1:SCYeryKXBVdW38167VyumGakH+7E4Wxe6b/zxmQxwyM= github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b/go.mod h1:daNLfX/GJKuZyN4HkMf0h8dVmTmgRbBSkd9bFQyGNIo= -github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y= -github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v1.7.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= @@ -3159,8 +3132,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0= github.com/spyzhov/ajson v0.9.0/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= @@ -3200,19 +3173,9 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= -github.com/testcontainers/testcontainers-go v0.30.0 h1:jmn/XS22q4YRrcMwWg0pAwlClzs/abopbsBzrepyc4E= -github.com/testcontainers/testcontainers-go v0.30.0/go.mod h1:K+kHNGiM5zjklKjgTtcrEetF3uhWbMUyqAQoyoh8Pf0= -github.com/testcontainers/testcontainers-go/modules/mysql v0.30.0 h1:wrePvxfU/2HFALKyBqpNs6VoPPvThzHy9aN+PCxse9g= -github.com/testcontainers/testcontainers-go/modules/mysql v0.30.0/go.mod h1:Srnlf7wwA7s6K4sKKhjAoBHJcKorRINR/i5dCA4ZyGk= -github.com/testcontainers/testcontainers-go/modules/postgres v0.30.0 h1:D3HFqpZS90iRGAN7M85DFiuhPfvYvFNnx8urQ6mPAvo= -github.com/testcontainers/testcontainers-go/modules/postgres v0.30.0/go.mod h1:e1sKxwUOkqzvaqdHl/oV9mUtFmkDPTfBGp0po2tnWQU= github.com/thanos-io/objstore v0.0.0-20220809103346-8ef1f215e2bf h1:onQsPyHlq2yIWU+Nfl6yStuqnZuVQQN8FZ8sBb2wqtw= github.com/thanos-io/objstore v0.0.0-20220809103346-8ef1f215e2bf/go.mod h1:v0NhuxxxUFUPatQcVNSCUkBEVezXzl7LSdaBOZygq98= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= @@ -3282,8 +3245,6 @@ 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.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -3343,8 +3304,8 @@ go.opentelemetry.io/collector/semconv v0.98.0/go.mod h1:8ElcRZ8Cdw5JnvhTOQOdYizk go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= @@ -3369,11 +3330,11 @@ go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDI go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0/go.mod h1:8GlBGcDk8KKi7n+2S4BT/CPZQYH3erLu0/k64r1MYgo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 h1:m0yTiGDLUvVYaTFbAvCkVYIYcvwKt3G7OLoN77NUs/8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0/go.mod h1:wBQbT4UekBfegL2nx0Xk1vBcnzyBPsIVm9hRG4fYcr4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= @@ -3388,8 +3349,8 @@ go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZF go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= @@ -3509,8 +3470,8 @@ golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -4048,8 +4009,8 @@ 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/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= -gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0= -gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= @@ -4347,8 +4308,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9/go. google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw= @@ -4393,8 +4354,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415141817-7cd4c1c1f9ec/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -4460,8 +4421,8 @@ google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJai google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -4591,6 +4552,8 @@ modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= 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.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI= @@ -4601,9 +4564,13 @@ modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aw modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= @@ -4622,8 +4589,8 @@ modernc.org/libc v1.21.2/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= -modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= 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= @@ -4635,15 +4602,18 @@ modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/sqlite v1.18.2/go.mod h1:kvrTLEWgxUcHa2GfHBQtanR1H9ht3hTJNtKpzH9k1u0= modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0= -modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= -modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= diff --git a/go.work.sum b/go.work.sum index 8fbaee04958..eb78119b883 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2,6 +2,8 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-2023080216373 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230802163732-1c33ebd9ecfa.1/go.mod h1:xafc+XIsTxTy76GJQ1TKgvJWsSugFBqMaN27WhUblew= cel.dev/expr v0.15.0 h1:O1jzfJCQBfL5BFoYktaxwIhuttaQPsVWerH9/EEKx0w= cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= +cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cloud.google.com/go/accessapproval v1.7.11 h1:MgtE8CI+YJWPGGHnxQ9z1VQqV87h+vSGy2MeM/m0ggQ= cloud.google.com/go/accessapproval v1.7.11/go.mod h1:KGK3+CLDWm4BvjN0wFtZqdFUGhxlTvTF6PhAwQJGL4M= cloud.google.com/go/accesscontextmanager v1.8.11 h1:IQ3KLJmNKPgstN0ZcRw0niU4KfsiOZmzvcGCF+NT618= @@ -259,7 +261,19 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUu docker.io/go-docker v1.0.0 h1:VdXS/aNYQxyA9wdLD5z8Q8Ro688/hG8HzKxYVEVbE6s= docker.io/go-docker v1.0.0/go.mod h1:7tiAn5a0LFmjbPDbyTPOaTTOuG1ZRNXdPA6RvKY+fpY= gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= +gioui.org v0.2.0 h1:RbzDn1h/pCVf/q44ImQSa/J3MIFpY3OWphzT/Tyei+w= +gioui.org v0.2.0/go.mod h1:1H72sKEk/fNFV+l0JNeM2Dt3co3Y4uaQcD+I+/GQ0e4= +gioui.org/cpu v0.0.0-20220412190645-f1e9e8c3b1f7 h1:tNJdnP5CgM39PRc+KWmBRRYX/zJ+rd5XaYxY5d5veqA= +gioui.org/cpu v0.0.0-20220412190645-f1e9e8c3b1f7/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y= +gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= +gioui.org/x v0.2.0 h1:/MbdjKH19F16auv19UiQxli2n6BYPw7eyh9XBOTgmEw= +gioui.org/x v0.2.0/go.mod h1:rCGN2nZ8ZHqrtseJoQxCMZpt2xrZUrdZ2WuMRLBJmYs= +git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= +git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik= +git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8= +git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo= github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng= github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk= @@ -281,8 +295,12 @@ github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWV github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0= github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw= +github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4= +github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4= github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ= +github.com/ClickHouse/clickhouse-go/v2 v2.28.3 h1:SkFzPULX6nzgfNZd1YD1XTECivjTMrCtD09ZPKcVLFQ= +github.com/ClickHouse/clickhouse-go/v2 v2.28.3/go.mod h1:vzn73hp+3JwxtFU4RjPCQ7r6fP2pMKVwdi8E1/Tkua8= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= @@ -300,6 +318,8 @@ github.com/KimMachineGun/automemlimit v0.6.0 h1:p/BXkH+K40Hax+PuWWPQ478hPjsp9h1C github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= +github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/OneOfOne/xxhash v1.2.6 h1:U68crOE3y3MPttCMQGywZOLrTeF5HHJ3/vDBCJn9/bA= github.com/OneOfOne/xxhash v1.2.6/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= @@ -335,10 +355,10 @@ github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/andybalholm/stroke v0.0.0-20221221101821-bd29b49d73f0 h1:uF5Q/hWnDU1XZeT6CsrRSxHLroUSEYYO3kgES+yd+So= +github.com/andybalholm/stroke v0.0.0-20221221101821-bd29b49d73f0/go.mod h1:ccdDYaY5+gO+cbnQdFxEXqfy0RkoV25H3jLXUDNM3wg= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= @@ -395,6 +415,8 @@ github.com/bufbuild/protovalidate-go v0.2.1/go.mod h1:e7XXDtlxj5vlEyAgsrxpzayp4c github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0= github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= @@ -426,7 +448,11 @@ github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9D github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= +github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0= github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= @@ -492,6 +518,7 @@ github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQ github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/expr-lang/expr v1.16.2 h1:JvMnzUs3LeVHBvGFcXYmXo+Q6DPDmzrlcSBO6Wy3w4s= github.com/expr-lang/expr v1.16.2/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= @@ -518,20 +545,36 @@ github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI= github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= +github.com/go-fonts/dejavu v0.3.2 h1:3XlHi0JBYX+Cp8n98c6qSoHrxPa4AUKDMKdrh/0sUdk= +github.com/go-fonts/dejavu v0.3.2/go.mod h1:m+TzKY7ZEl09/a17t1593E4VYW8L1VaBXHzFZOIjGEY= github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= +github.com/go-fonts/latin-modern v0.3.2 h1:M+Sq24Dp0ZRPf3TctPnG1MZxRblqyWC/cRUL9WmdaFc= +github.com/go-fonts/latin-modern v0.3.2/go.mod h1:9odJt4NbRrbdj4UAMuLVd4zEukf6aAEKnDaQga0whqQ= github.com/go-fonts/liberation v0.3.0 h1:3BI2iaE7R/s6uUUtzNCjo3QijJu3aS4wmrMgfSpYQ+8= github.com/go-fonts/liberation v0.3.0/go.mod h1:jdJ+cqF+F4SUL2V+qxBth8fvBpBDS7yloUL5Fi8GTGY= +github.com/go-fonts/liberation v0.3.2 h1:XuwG0vGHFBPRRI8Qwbi5tIvR3cku9LUfZGq/Ar16wlQ= +github.com/go-fonts/liberation v0.3.2/go.mod h1:N0QsDLVUQPy3UYg9XAc3Uh3UDMp2Z7M1o4+X98dXkmI= github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= +github.com/go-fonts/stix v0.2.2 h1:v9krocr13J1llaOHLEol1eaHsv8S43UuFX/1bFgEJJ4= +github.com/go-fonts/stix v0.2.2/go.mod h1:SUxggC9dxd/Q+rb5PkJuvfvTbOPtNc2Qaua00fIp9iU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs= github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM= +github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea h1:DfZQkvEbdmOe+JK2TMtBM+0I9GSdzE2y/L1/AmD8xKc= +github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea/go.mod h1:Y7Vld91/HRbTBm7JwoI7HejdDB0u+e9AUBO9MB7yuZk= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= +github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= +github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= @@ -539,6 +582,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= +github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -554,6 +599,8 @@ github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfE github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4= github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= @@ -585,8 +632,6 @@ github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18/go.mod h1:GMLi6d0 github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10 h1:oDbLKM34O+JUF9EQFS+9aYhdYoeNfUpXqNjFCLIxwF4= github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45 h1:AJKOtDKAOg8XNFnIZSmqqqutoTSxVlRs6vekL2p2KEY= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26 h1:HX927q4X1n451pnGb8U0wq74i8PCzuxVjzv7TyD10kc= github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26/go.mod h1:Pn9nfzCk7nV0mvNgwusgCjCROZP6nm4GpwTnmEhLT24= @@ -689,8 +734,12 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= +github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY= +github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= @@ -700,6 +749,8 @@ github.com/matryer/moq v0.5.0 h1:h2PJUYjZSiyEahzVogDRmrgL9Bsx9xYAl8l+LPfmwL8= github.com/matryer/moq v0.5.0/go.mod h1:39GTnrD0mVWHPvWdYj5ki/lxfhLQEtHcLh+tWoYF/iE= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/mfridman/xflag v0.0.0-20240825232106-efb77353e578 h1:CRrqlUmLebb/QjzRDWE0E66+YyN/v95+w6WyH9ju8/Y= +github.com/mfridman/xflag v0.0.0-20240825232106-efb77353e578/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= @@ -722,8 +773,12 @@ github.com/nats-io/jwt/v2 v2.0.3 h1:i/O6cmIsjpcQyWDYNcq2JyZ3/VTF8SJ4JWluI5OhpvI= github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= +github.com/nats-io/nats.go v1.34.0 h1:fnxnPCNiwIG5w08rlMcEKTUw4AV/nKyGCOJE8TdhSPk= +github.com/nats-io/nats.go v1.34.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= +github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= @@ -782,6 +837,8 @@ github.com/parquet-go/parquet-go v0.20.2-0.20240416173845-962b3c5827c3 h1:dHzXGq github.com/parquet-go/parquet-go v0.20.2-0.20240416173845-962b3c5827c3/go.mod h1:wMYanjuaE900FTDTNY00JU+67Oqh9uO0pYWRNoPGctQ= github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s= github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= +github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= @@ -800,6 +857,8 @@ github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDj github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= +github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= @@ -823,6 +882,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= github.com/sagikazarmark/crypt v0.17.0 h1:ZA/7pXyjkHoK4bW4mIdnCLvL8hd+Nrbiw7Dqk7D4qUk= github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg= +github.com/sagikazarmark/crypt v0.19.0 h1:WMyLTjHBo64UvNcWqpzY3pbZTYgnemZU8FBZigKc42E= +github.com/sagikazarmark/crypt v0.19.0/go.mod h1:c6vimRziqqERhtSe0MhIvzE1w54FrCHtrXb5NH/ja78= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= @@ -834,6 +895,10 @@ github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZ github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y= +github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v1.7.1 h1:UJcjSAI3aUKx52kfcfhblgyhZceouhvvs3OYdWgn+PY= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= @@ -854,14 +919,22 @@ github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvF github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tursodatabase/libsql-client-go v0.0.0-20240411070317-a1138d155304 h1:Y6cw8yjWCEJDy5Bll7HjTinkgTQU55AXiKSEe29SpgA= github.com/tursodatabase/libsql-client-go v0.0.0-20240411070317-a1138d155304/go.mod h1:2Fu26tjM011BLeR5+jwTfs6DX/fNMEWV/3CBZvggrA4= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -903,12 +976,18 @@ github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf h1:ckwNHVo4bv2tqNkgx3W3HANh3ta1j6TR5qw08J1A7Tw= github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= +github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7 h1:nL8XwD6fSst7xFUirkaWJmE7kM0CdWRYgu6+YQer1d4= +github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1 h1:Ebo6J5AMXgJ3A438ECYotA0aK7ETqjQx9WoZvVxzKBE= github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1/go.mod h1:udNPW8eupyH/EZocecFmaSNJacKKYjzQa7cVgX5U2nc= +github.com/ydb-platform/ydb-go-sdk/v3 v3.80.2 h1:qmZGJQCNx09/r0HDIT2cDDogiOvWikELy13ubM2CFS8= +github.com/ydb-platform/ydb-go-sdk/v3 v3.80.2/go.mod h1:IHwuXyolaAmGK2Dp7+dlhsnXphG1pwCoaP/OITT3+tU= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= @@ -1022,9 +1101,15 @@ golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5D golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp/shiny v0.0.0-20230801115018-d63ba01acd4b h1:sgkbz1SFTsoQIvzTIw45hccUcGocu00QM3qucBYV8b0= +golang.org/x/exp/shiny v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -1049,17 +1134,20 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= +gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE= +gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M= google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= @@ -1075,8 +1163,10 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= @@ -1107,13 +1197,14 @@ k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/s k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0= modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws= modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= diff --git a/lerna.json b/lerna.json index 88526882713..e8129a1ad90 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "npmClient": "yarn", - "version": "11.3.0-pre" + "version": "11.4.0-pre" } diff --git a/package.json b/package.json index e261fc3abcf..7efcf198bc5 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "11.3.0-pre", + "version": "11.4.0-pre", "repository": "github:grafana/grafana", "scripts": { "build": "NODE_ENV=production nx exec --verbose -- webpack --config scripts/webpack/webpack.prod.js", @@ -69,8 +69,8 @@ "releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/" }, "devDependencies": { - "@babel/core": "7.25.7", - "@babel/preset-env": "7.25.7", + "@babel/core": "7.25.8", + "@babel/preset-env": "7.25.8", "@babel/runtime": "7.25.7", "@betterer/betterer": "5.4.0", "@betterer/cli": "5.4.0", @@ -84,17 +84,17 @@ "@manypkg/get-packages": "^2.2.0", "@playwright/test": "1.48.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", - "@react-types/button": "3.9.6", + "@react-types/button": "3.10.0", "@react-types/menu": "3.9.12", "@react-types/overlays": "3.8.10", - "@react-types/shared": "3.24.1", + "@react-types/shared": "3.25.0", "@rtk-query/codegen-openapi": "^1.2.0", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", "@swc/core": "1.4.2", "@swc/helpers": "0.5.13", - "@testing-library/dom": "10.0.0", - "@testing-library/jest-dom": "6.4.2", - "@testing-library/react": "15.0.2", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.5.0", + "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/angular": "1.8.9", "@types/angular-route": "1.7.6", @@ -107,7 +107,7 @@ "@types/d3-scale-chromatic": "3.0.3", "@types/debounce-promise": "3.1.9", "@types/diff": "^5", - "@types/eslint": "8.56.10", + "@types/eslint": "9.6.1", "@types/eslint-scope": "^3.7.7", "@types/file-saver": "2.0.7", "@types/glob": "^8.0.0", @@ -171,7 +171,7 @@ "cypress-recurse": "^1.35.3", "esbuild": "0.24.0", "esbuild-loader": "4.2.2", - "esbuild-plugin-browserslist": "^0.14.0", + "esbuild-plugin-browserslist": "^0.15.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.26.0", @@ -205,7 +205,7 @@ "knip": "^5.10.0", "lerna": "8.1.8", "mini-css-extract-plugin": "2.9.1", - "msw": "2.4.9", + "msw": "2.4.10", "mutationobserver-shim": "0.3.7", "ngtemplate-loader": "2.1.0", "node-notifier": "10.0.1", @@ -222,7 +222,7 @@ "redux-mock-store": "1.5.4", "rimraf": "6.0.1", "rudder-sdk-js": "2.48.19", - "sass": "1.79.3", + "sass": "1.79.5", "sass-loader": "16.0.2", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", @@ -250,13 +250,13 @@ "@emotion/react": "11.13.3", "@fingerprintjs/fingerprintjs": "^3.4.2", "@floating-ui/react": "0.26.24", - "@formatjs/intl-durationformat": "^0.2.4", + "@formatjs/intl-durationformat": "^0.3.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/aws-sdk": "0.5.0", "@grafana/azure-sdk": "0.0.3", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", - "@grafana/experimental": "2.1.1", + "@grafana/experimental": "2.1.2", "@grafana/faro-core": "^1.3.6", "@grafana/faro-web-sdk": "^1.3.6", "@grafana/faro-web-tracing": "^1.8.2", @@ -268,11 +268,12 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "5.18.3", + "@grafana/scenes": "5.20.0", + "@grafana/scenes-react": "5.20.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", - "@hello-pangea/dnd": "16.6.0", + "@hello-pangea/dnd": "17.0.0", "@kusto/monaco-kusto": "^10.0.0", "@leeoniya/ufuzzy": "1.0.14", "@lezer/common": "1.2.2", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index ce45ca9e694..46d2c1d304d 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": "11.3.0-pre", + "version": "11.4.0-pre", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -36,7 +36,7 @@ }, "dependencies": { "@braintree/sanitize-url": "7.0.1", - "@grafana/schema": "11.3.0-pre", + "@grafana/schema": "11.4.0-pre", "@types/d3-interpolate": "^3.0.0", "@types/string-hash": "1.1.3", "d3-interpolate": "3.0.1", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b8124c22120..83738ec5cf1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -198,7 +198,6 @@ export interface FeatureToggles { zanzana?: boolean; passScopeToDashboardApi?: boolean; alertingApiServer?: boolean; - dashboardRestoreUI?: boolean; cloudWatchRoundUpEndTime?: boolean; cloudwatchMetricInsightsCrossAccount?: boolean; prometheusAzureOverrideAudience?: boolean; @@ -212,7 +211,7 @@ export interface FeatureToggles { exploreLogsAggregatedMetrics?: boolean; exploreLogsLimitedTimeRange?: boolean; homeSetupGuide?: boolean; - appPlatformAccessTokens?: boolean; + appPlatformGrpcClientAuth?: boolean; appSidecar?: boolean; groupAttributeSync?: boolean; alertingQueryAndExpressionsStepMode?: boolean; diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 570437c943d..d88e76965a6 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -22,6 +22,7 @@ export const availableIconsIndex = { 'angle-up': true, 'align-left': true, 'align-right': true, + 'api-endpoint': true, 'application-observability': true, apps: true, 'archive-alt': true, @@ -49,6 +50,7 @@ export const availableIconsIndex = { bookmark: true, 'book-open': true, 'brackets-curly': true, + 'browser-alt': true, bug: true, building: true, 'calculator-alt': true, @@ -161,6 +163,7 @@ export const availableIconsIndex = { info: true, 'info-circle': true, k6: true, + 'k6-rounded': true, 'key-skeleton-alt': true, keyboard: true, kubernetes: true, @@ -184,6 +187,7 @@ export const availableIconsIndex = { 'minus-circle': true, 'mobile-android': true, monitor: true, + 'multi-step': true, palette: true, 'panel-add': true, paragraph: true, diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index ace51d6eee0..3fb5804948d 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": "11.3.0-pre", + "version": "11.4.0-pre", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index 5ac20942895..596158c9a68 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": "11.3.0-pre", + "version": "11.4.0-pre", "main": "./index.cjs", "author": "Grafana Labs", "license": "Apache-2.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 3ccd87c0a02..19ad1f8c055 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/flamegraph", - "version": "11.3.0-pre", + "version": "11.4.0-pre", "description": "Grafana flamegraph visualization component", "keywords": [ "grafana", @@ -44,8 +44,8 @@ ], "dependencies": { "@emotion/css": "11.13.4", - "@grafana/data": "11.3.0-pre", - "@grafana/ui": "11.3.0-pre", + "@grafana/data": "11.4.0-pre", + "@grafana/ui": "11.4.0-pre", "@leeoniya/ufuzzy": "1.0.14", "d3": "^7.8.5", "lodash": "4.17.21", @@ -56,14 +56,14 @@ "tslib": "2.7.0" }, "devDependencies": { - "@babel/core": "7.25.7", - "@babel/preset-env": "7.25.7", + "@babel/core": "7.25.8", + "@babel/preset-env": "7.25.8", "@babel/preset-react": "7.25.7", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.3.0", - "@testing-library/dom": "10.0.0", + "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "15.0.2", + "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/d3": "^7", "@types/jest": "^29.5.4", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index 7313c7fe3ed..f72eff65875 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -1,6 +1,6 @@ { "name": "@grafana/saga-icons", - "version": "11.3.0-pre", + "version": "11.4.0-pre", "private": true, "description": "Icons for Grafana", "author": "Grafana Labs", @@ -34,7 +34,7 @@ "build": "yarn generate && rollup -c rollup.config.ts --configPlugin esbuild" }, "devDependencies": { - "@babel/core": "7.25.7", + "@babel/core": "7.25.8", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-typescript": "^12.1.0", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index a1e256f8dd1..c39ead5fc09 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "name": "@grafana/o11y-ds-frontend", "private": true, - "version": "11.3.0-pre", + "version": "11.4.0-pre", "description": "Library to manage traces in Grafana.", "sideEffects": false, "repository": { @@ -18,12 +18,12 @@ }, "dependencies": { "@emotion/css": "11.13.4", - "@grafana/data": "11.3.0-pre", - "@grafana/e2e-selectors": "11.3.0-pre", - "@grafana/experimental": "2.1.1", - "@grafana/runtime": "11.3.0-pre", - "@grafana/schema": "11.3.0-pre", - "@grafana/ui": "11.3.0-pre", + "@grafana/data": "11.4.0-pre", + "@grafana/e2e-selectors": "11.4.0-pre", + "@grafana/experimental": "2.1.2", + "@grafana/runtime": "11.4.0-pre", + "@grafana/schema": "11.4.0-pre", + "@grafana/ui": "11.4.0-pre", "react-select": "5.8.1", "react-use": "17.5.1", "rxjs": "7.8.1", @@ -31,9 +31,9 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@testing-library/dom": "10.0.0", + "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "15.0.2", + "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", "@types/node": "20.16.11", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index fa204ae3c75..186a3896971 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -2,14 +2,14 @@ "name": "@grafana/plugin-configs", "description": "Shared dependencies and files for core plugins", "private": true, - "version": "11.3.0-pre", + "version": "11.4.0-pre", "dependencies": { "tslib": "2.7.0" }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", "@swc/core": "1.4.2", - "@types/eslint": "8.56.10", + "@types/eslint": "9.6.1", "copy-webpack-plugin": "12.0.2", "eslint": "8.57.0", "eslint-webpack-plugin": "4.2.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 5c8924babaf..f0da22c686a 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "AGPL-3.0-only", "name": "@grafana/prometheus", - "version": "11.3.0-pre", + "version": "11.4.0-pre", "description": "Grafana Prometheus Library", "keywords": [ "typescript" @@ -38,13 +38,13 @@ "dependencies": { "@emotion/css": "11.13.4", "@floating-ui/react": "0.26.24", - "@grafana/data": "11.3.0-pre", - "@grafana/experimental": "2.1.1", + "@grafana/data": "11.4.0-pre", + "@grafana/experimental": "2.1.2", "@grafana/faro-web-sdk": "1.10.2", - "@grafana/runtime": "11.3.0-pre", - "@grafana/schema": "11.3.0-pre", - "@grafana/ui": "11.3.0-pre", - "@hello-pangea/dnd": "16.6.0", + "@grafana/runtime": "11.4.0-pre", + "@grafana/schema": "11.4.0-pre", + "@grafana/ui": "11.4.0-pre", + "@hello-pangea/dnd": "17.0.0", "@leeoniya/ufuzzy": "1.0.14", "@lezer/common": "1.2.2", "@lezer/highlight": "1.2.1", @@ -76,19 +76,19 @@ }, "devDependencies": { "@emotion/eslint-plugin": "11.12.0", - "@grafana/e2e-selectors": "11.3.0-pre", + "@grafana/e2e-selectors": "11.4.0-pre", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-image": "3.0.3", "@rollup/plugin-node-resolve": "15.3.0", "@swc/core": "1.4.2", "@swc/helpers": "0.5.13", - "@testing-library/dom": "10.0.0", - "@testing-library/jest-dom": "6.4.2", - "@testing-library/react": "15.0.2", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.5.0", + "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/d3": "7.4.3", "@types/debounce-promise": "3.1.9", - "@types/eslint": "8.56.10", + "@types/eslint": "9.6.1", "@types/jest": "29.5.13", "@types/jquery": "3.5.31", "@types/lodash": "4.17.10", @@ -131,7 +131,7 @@ "rollup-plugin-dts": "^6.1.1", "rollup-plugin-esbuild": "6.1.1", "rollup-plugin-node-externals": "^7.1.3", - "sass": "1.79.3", + "sass": "1.79.5", "sass-loader": "16.0.2", "style-loader": "4.0.0", "testing-library-selector": "0.3.1", diff --git a/packages/grafana-prometheus/src/configuration/PromSettings.test.tsx b/packages/grafana-prometheus/src/configuration/PromSettings.test.tsx index 15af11210cc..45ac59a46d7 100644 --- a/packages/grafana-prometheus/src/configuration/PromSettings.test.tsx +++ b/packages/grafana-prometheus/src/configuration/PromSettings.test.tsx @@ -112,5 +112,12 @@ describe('PromSettings', () => { fireEvent.blur(input); expect(queryByText(countError)).toBeInTheDocument(); }); + + it('should have a series endpoint configuration element', () => { + const options = defaultProps; + + render( {}} options={options} />); + expect(screen.getByText('Use series endpoint')).toBeInTheDocument(); + }); }); }); diff --git a/packages/grafana-prometheus/src/configuration/PromSettings.tsx b/packages/grafana-prometheus/src/configuration/PromSettings.tsx index c08273b4ff7..9291ec95040 100644 --- a/packages/grafana-prometheus/src/configuration/PromSettings.tsx +++ b/packages/grafana-prometheus/src/configuration/PromSettings.tsx @@ -492,6 +492,26 @@ export const PromSettings = (props: Props) => { + + Checking this option will favor the series endpoint with match[] parameter over the label values + endpoint with match[] parameter. While the label values endpoint is considered more performant, some + users may prefer the series because it has a POST method while the label values endpoint only has a GET + method. {docsTip()} + + } + interactive={true} + disabled={options.readOnly} + className={styles.switchField} + > + + diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 29b53f51902..00edfe60784 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -111,6 +111,7 @@ export class PrometheusDatasource cacheLevel: PrometheusCacheLevel; cache: QueryCache; metricNamesAutocompleteSuggestionLimit: number; + seriesEndpoint: boolean; constructor( instanceSettings: DataSourceInstanceSettings, @@ -136,6 +137,7 @@ export class PrometheusDatasource this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters); this.datasourceConfigurationPrometheusFlavor = instanceSettings.jsonData.prometheusType; this.datasourceConfigurationPrometheusVersion = instanceSettings.jsonData.prometheusVersion; + this.seriesEndpoint = instanceSettings.jsonData.seriesEndpoint ?? false; this.defaultEditor = instanceSettings.jsonData.defaultEditor; this.disableRecordingRules = instanceSettings.jsonData.disableRecordingRules ?? false; this.variables = new PrometheusVariableSupport(this, this.templateSrv); @@ -183,6 +185,12 @@ export class PrometheusDatasource } hasLabelsMatchAPISupport(): boolean { + // users may choose the series endpoint as it has a POST method + // while the label values is only GET + if (this.seriesEndpoint) { + return false; + } + return ( // https://github.com/prometheus/prometheus/releases/tag/v2.24.0 this._isDatasourceVersionGreaterOrEqualTo('2.24.0', PromApplication.Prometheus) || diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx index 0942b93ff88..9ad1b409456 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx @@ -7,6 +7,7 @@ import { useStyles2 } from '@grafana/ui'; import { PromQueryField } from '../../components/PromQueryField'; import { PromQueryEditorProps } from '../../components/types'; +import { QueryEditorHints } from '../shared/QueryEditorHints'; import { PromQueryBuilderExplained } from './PromQueryBuilderExplained'; @@ -33,8 +34,8 @@ export function PromQueryCodeEditor(props: PromQueryCodeEditorProps) { data={data} app={app} /> - {showExplain && } + ); } diff --git a/packages/grafana-prometheus/src/querybuilder/shared/QueryEditorHints.tsx b/packages/grafana-prometheus/src/querybuilder/shared/QueryEditorHints.tsx new file mode 100644 index 00000000000..fa69a1c1eaa --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/shared/QueryEditorHints.tsx @@ -0,0 +1,63 @@ +import { css } from '@emotion/css'; +import { useEffect, useState } from 'react'; + +import { GrafanaTheme2, QueryHint } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import { Button, Tooltip, useStyles2 } from '@grafana/ui'; + +import { PromQueryEditorProps } from '../../components/types'; + +export function QueryEditorHints(props: PromQueryEditorProps) { + const [hints, setHints] = useState([]); + const { query, data, datasource } = props; + const styles = useStyles2(getStyles); + + useEffect(() => { + const promQuery = { expr: query.expr, refId: query.refId }; + const hints = datasource.getQueryHints(promQuery, data?.series || []).filter((hint) => hint.fix?.action); + setHints(hints); + }, [datasource, data, query]); + + return ( + <> + {hints.length > 0 && ( +
+ {hints.map((hint) => { + return ( + + + + ); + })} +
+ )} + + ); +} + +function onHintButtonClick(hint: QueryHint, props: PromQueryEditorProps) { + reportInteraction('grafana_query_builder_hints_clicked', { + hint: hint.type, + datasourceType: props.datasource.type, + }); + + if (hint.fix?.action) { + const newQuery = props.datasource.modifyQuery(props.query, hint.fix.action); + return props.onChange(newQuery); + } +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + container: css({ + display: 'flex', + alignItems: 'start', + }), + hint: css({ + marginRight: theme.spacing(1), + padEnd: theme.spacing(2), + }), + }; +}; diff --git a/packages/grafana-prometheus/src/types.ts b/packages/grafana-prometheus/src/types.ts index a8b20b64e87..57903231250 100644 --- a/packages/grafana-prometheus/src/types.ts +++ b/packages/grafana-prometheus/src/types.ts @@ -53,6 +53,7 @@ export interface PromOptions extends DataSourceJsonData { sigV4Auth?: boolean; oauthPassThru?: boolean; codeModeMetricNamesSuggestionLimit?: number; + seriesEndpoint?: boolean; } export type ExemplarTraceIdDestination = { diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 9aefe49a823..b41960b1bfc 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": "11.3.0-pre", + "version": "11.4.0-pre", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -37,11 +37,11 @@ "postpack": "mv package.json.bak package.json" }, "dependencies": { - "@grafana/data": "11.3.0-pre", - "@grafana/e2e-selectors": "11.3.0-pre", + "@grafana/data": "11.4.0-pre", + "@grafana/e2e-selectors": "11.4.0-pre", "@grafana/faro-web-sdk": "^1.3.6", - "@grafana/schema": "11.3.0-pre", - "@grafana/ui": "11.3.0-pre", + "@grafana/schema": "11.4.0-pre", + "@grafana/ui": "11.4.0-pre", "history": "4.10.1", "lodash": "4.17.21", "rxjs": "7.8.1", @@ -51,8 +51,8 @@ "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.3.0", "@rollup/plugin-terser": "0.4.4", - "@testing-library/dom": "10.0.0", - "@testing-library/react": "15.0.2", + "@testing-library/dom": "10.4.0", + "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/angular": "1.8.9", "@types/history": "4.7.11", diff --git a/packages/grafana-runtime/src/components/QueryEditorWithMigration.test.tsx b/packages/grafana-runtime/src/components/QueryEditorWithMigration.test.tsx new file mode 100644 index 00000000000..fb67fcac085 --- /dev/null +++ b/packages/grafana-runtime/src/components/QueryEditorWithMigration.test.tsx @@ -0,0 +1,102 @@ +import { render, screen, waitFor } from '@testing-library/react'; + +import { DataSourceInstanceSettings, QueryEditorProps } from '@grafana/data'; +import { DataQuery, DataSourceJsonData } from '@grafana/schema'; + +import { config } from '../config'; +import { BackendSrv, BackendSrvRequest } from '../services'; +import { DataSourceWithBackend } from '../utils/DataSourceWithBackend'; +import { MigrationHandler } from '../utils/migrationHandler'; + +import { QueryEditorWithMigration } from './QueryEditorWithMigration'; + +const backendSrv = { + post(url: string, data?: unknown, options?: Partial): Promise { + return mockDatasourcePost({ url, data, ...options }); + }, +} as unknown as BackendSrv; + +jest.mock('../services', () => ({ + ...jest.requireActual('../services'), + getBackendSrv: () => backendSrv, +})); + +let mockDatasourcePost = jest.fn(); + +interface MyQuery extends DataQuery {} + +class MyDataSource extends DataSourceWithBackend implements MigrationHandler { + hasBackendMigration: boolean; + + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + this.hasBackendMigration = true; + } + + shouldMigrate(query: DataQuery): boolean { + return true; + } +} + +type Props = QueryEditorProps; + +function QueryEditor(props: Props) { + return
{JSON.stringify(props.query)}
; +} + +function createMockDatasource(otherSettings?: Partial>) { + const settings = { + name: 'test', + id: 1234, + uid: 'abc', + type: 'dummy', + jsonData: {}, + ...otherSettings, + } as DataSourceInstanceSettings; + + return new MyDataSource(settings); +} + +describe('QueryEditorWithMigration', () => { + const originalFeatureToggles = config.featureToggles; + beforeEach(() => { + config.featureToggles = { ...originalFeatureToggles, grafanaAPIServerWithExperimentalAPIs: true }; + }); + afterEach(() => { + config.featureToggles = originalFeatureToggles; + }); + + it('should migrate a query', async () => { + const WithMigration = QueryEditorWithMigration(QueryEditor); + const ds = createMockDatasource(); + const originalQuery = { refId: 'A', datasource: { type: 'dummy' }, foo: 'bar' }; + const migratedQuery = { refId: 'A', datasource: { type: 'dummy' }, foobar: 'barfoo' }; + + mockDatasourcePost = jest.fn().mockImplementation((args: { url: string; data: unknown }) => { + expect(args.url).toBe('/apis/dummy.datasource.grafana.app/v0alpha1/namespaces/default/queryconvert'); + expect(args.data).toMatchObject({ queries: [originalQuery] }); + return Promise.resolve({ queries: [{ JSON: migratedQuery }] }); + }); + + render(); + + await waitFor(() => { + // Check that migratedQuery is rendered + expect(screen.getByText(JSON.stringify(migratedQuery))).toBeInTheDocument(); + }); + }); + + it('should render a Skeleton while migrating', async () => { + const WithMigration = QueryEditorWithMigration(QueryEditor); + const ds = createMockDatasource(); + const originalQuery = { refId: 'A', datasource: { type: 'dummy' }, foo: 'bar' }; + + mockDatasourcePost = jest.fn().mockImplementation(async (args: { url: string; data: unknown }) => { + await waitFor(() => {}, { timeout: 5000 }); + return Promise.resolve({ queries: [{ JSON: originalQuery }] }); + }); + + render(); + expect(screen.getByTestId('react-loading-skeleton-testid')).toBeInTheDocument(); + }); +}); diff --git a/packages/grafana-runtime/src/components/QueryEditorWithMigration.tsx b/packages/grafana-runtime/src/components/QueryEditorWithMigration.tsx new file mode 100644 index 00000000000..ed65a3d133e --- /dev/null +++ b/packages/grafana-runtime/src/components/QueryEditorWithMigration.tsx @@ -0,0 +1,44 @@ +import React, { useEffect, useState } from 'react'; +import Skeleton from 'react-loading-skeleton'; + +import { DataSourceApi, DataSourceOptionsType, DataSourceQueryType, QueryEditorProps } from '@grafana/data'; +import { DataQuery, DataSourceJsonData } from '@grafana/schema'; + +import { isMigrationHandler, migrateQuery } from '../utils/migrationHandler'; + +/** + * @alpha Experimental: QueryEditorWithMigration is a higher order component that wraps the QueryEditor component + * and ensures that the query is migrated before being passed to the QueryEditor. + */ +export function QueryEditorWithMigration< + DSType extends DataSourceApi, + TQuery extends DataQuery = DataSourceQueryType, + TOptions extends DataSourceJsonData = DataSourceOptionsType, +>(QueryEditor: React.ComponentType>) { + const WithExtra = (props: QueryEditorProps) => { + const [migrated, setMigrated] = useState(false); + const [query, setQuery] = useState(props.query); + + useEffect(() => { + if (props.query && isMigrationHandler(props.datasource)) { + migrateQuery(props.datasource, props.query).then((migrated) => { + props.onChange(migrated); + setQuery(migrated); + setMigrated(true); + }); + } else { + setMigrated(true); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + setQuery(props.query); + }, [props.query]); + + if (!migrated) { + return ; + } + return ; + }; + return WithExtra; +} diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index c8b1723ef9d..31286995cc0 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -54,3 +54,5 @@ export { setReturnToPreviousHook, useReturnToPrevious } from './utils/returnToPr export { setChromeHeaderHeightHook, useChromeHeaderHeight } from './utils/chromeHeaderHeight'; export { type EmbeddedDashboardProps, EmbeddedDashboard, setEmbeddedDashboard } from './components/EmbeddedDashboard'; export { hasPermission, hasPermissionInMetadata, hasAllPermissions, hasAnyPermission } from './utils/rbac'; +export { QueryEditorWithMigration } from './components/QueryEditorWithMigration'; +export { type MigrationHandler, isMigrationHandler, migrateQuery, migrateRequest } from './utils/migrationHandler'; diff --git a/packages/grafana-runtime/src/utils/migrationHandler.test.ts b/packages/grafana-runtime/src/utils/migrationHandler.test.ts new file mode 100644 index 00000000000..37fba989266 --- /dev/null +++ b/packages/grafana-runtime/src/utils/migrationHandler.test.ts @@ -0,0 +1,174 @@ +import { BackendSrv, BackendSrvRequest } from 'src/services'; + +import { DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data'; +import { DataQuery, DataSourceJsonData } from '@grafana/schema'; + +import { config } from '../config'; + +import { DataSourceWithBackend } from './DataSourceWithBackend'; +import { isMigrationHandler, migrateQuery, migrateRequest, MigrationHandler } from './migrationHandler'; + +let mockDatasourcePost = jest.fn(); + +interface MyQuery extends DataQuery {} + +class MyDataSourceWithoutMigration extends DataSourceWithBackend { + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + } +} + +class MyDataSource extends DataSourceWithBackend implements MigrationHandler { + hasBackendMigration: boolean; + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + this.hasBackendMigration = true; + } + + shouldMigrate(query: DataQuery): boolean { + return true; + } +} + +const backendSrv = { + post(url: string, data?: unknown, options?: Partial): Promise { + return mockDatasourcePost({ url, data, ...options }); + }, +} as unknown as BackendSrv; + +jest.mock('../services', () => ({ + ...jest.requireActual('../services'), + getBackendSrv: () => backendSrv, +})); + +describe('query migration', () => { + // Configure config.featureToggles.grafanaAPIServerWithExperimentalAPIs + const originalFeatureToggles = config.featureToggles; + beforeEach(() => { + config.featureToggles = { ...originalFeatureToggles, grafanaAPIServerWithExperimentalAPIs: true }; + }); + afterEach(() => { + config.featureToggles = originalFeatureToggles; + }); + + describe('isMigrationHandler', () => { + it('returns true for a datasource with backend migration', () => { + const ds = createMockDatasource(); + expect(isMigrationHandler(ds)).toBe(true); + }); + + it('returns false for a datasource without backend migration', () => { + const ds = new MyDataSourceWithoutMigration({} as DataSourceInstanceSettings); // eslint-disable-line @typescript-eslint/no-explicit-any + expect(isMigrationHandler(ds)).toBe(false); + }); + }); + + describe('migrateQuery', () => { + it('skips migration if the datasource does not support it', async () => { + const ds = createMockDatasource(); + ds.hasBackendMigration = false; + const query = { refId: 'A', datasource: { type: 'dummy' } }; + + const result = await migrateQuery(ds, query); + + expect(query).toEqual(result); + expect(mockDatasourcePost).not.toHaveBeenCalled(); + }); + + it('skips migration if the query should not be migrated', async () => { + const ds = createMockDatasource(); + ds.shouldMigrate = jest.fn().mockReturnValue(false); + const query = { refId: 'A', datasource: { type: 'dummy' } }; + + const result = await migrateQuery(ds, query); + + expect(query).toEqual(result); + expect(mockDatasourcePost).not.toHaveBeenCalled(); + }); + + it('check that migrateQuery works', async () => { + const ds = createMockDatasource(); + + const originalQuery = { refId: 'A', datasource: { type: 'dummy' }, foo: 'bar' }; + const migratedQuery = { refId: 'A', datasource: { type: 'dummy' }, foobar: 'barfoo' }; + mockDatasourcePost = jest.fn().mockImplementation((args: { url: string; data: unknown }) => { + expect(args.url).toBe('/apis/dummy.datasource.grafana.app/v0alpha1/namespaces/default/queryconvert'); + expect(args.data).toMatchObject({ queries: [originalQuery] }); + return Promise.resolve({ queries: [{ JSON: migratedQuery }] }); + }); + + const result = await migrateQuery(ds, originalQuery); + + expect(migratedQuery).toEqual(result); + }); + }); + + describe('migrateRequest', () => { + it('skips migration if the datasource does not support it', async () => { + const ds = createMockDatasource(); + ds.hasBackendMigration = false; + const request = { + targets: [{ refId: 'A', datasource: { type: 'dummy' } }], + } as unknown as DataQueryRequest; // eslint-disable-line @typescript-eslint/no-explicit-any + + const result = await migrateRequest(ds, request); + + expect(request).toEqual(result); + expect(mockDatasourcePost).not.toHaveBeenCalled(); + }); + + it('skips migration if none of the queries should be migrated', async () => { + const ds = createMockDatasource(); + ds.shouldMigrate = jest.fn().mockReturnValue(false); + const request = { + targets: [{ refId: 'A', datasource: { type: 'dummy' } }], + } as unknown as DataQueryRequest; // eslint-disable-line @typescript-eslint/no-explicit-any + + const result = await migrateRequest(ds, request); + + expect(request).toEqual(result); + expect(mockDatasourcePost).not.toHaveBeenCalled(); + }); + + it('check that migrateRequest migrates a request', async () => { + const ds = createMockDatasource(); + + const originalRequest = { + targets: [ + { refId: 'A', datasource: { type: 'dummy' }, foo: 'bar' }, + { refId: 'A', datasource: { type: 'dummy' }, bar: 'foo' }, + ], + } as unknown as DataQueryRequest; // eslint-disable-line @typescript-eslint/no-explicit-any + const migratedRequest = { + targets: [ + { refId: 'A', datasource: { type: 'dummy' }, foobar: 'foobar' }, + { refId: 'A', datasource: { type: 'dummy' }, barfoo: 'barfoo' }, + ], + }; + mockDatasourcePost = jest.fn().mockImplementation((args: { url: string; data: unknown }) => { + expect(args.url).toBe('/apis/dummy.datasource.grafana.app/v0alpha1/namespaces/default/queryconvert'); + expect(args.data).toMatchObject({ queries: originalRequest.targets }); + return Promise.resolve({ queries: migratedRequest.targets.map((query) => ({ JSON: query })) }); + }); + + const result = await migrateRequest(ds, originalRequest); + + expect(migratedRequest).toEqual(result); + }); + }); +}); + +function createMockDatasource(otherSettings?: Partial>) { + const settings = { + name: 'test', + id: 1234, + uid: 'abc', + type: 'dummy', + jsonData: {}, + ...otherSettings, + } as DataSourceInstanceSettings; + + mockDatasourcePost.mockReset(); + + return new MyDataSource(settings); +} diff --git a/packages/grafana-runtime/src/utils/migrationHandler.ts b/packages/grafana-runtime/src/utils/migrationHandler.ts new file mode 100644 index 00000000000..34849c091a3 --- /dev/null +++ b/packages/grafana-runtime/src/utils/migrationHandler.ts @@ -0,0 +1,72 @@ +import { DataQueryRequest } from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; + +import { config } from '../config'; +import { getBackendSrv } from '../services'; + +import { DataSourceWithBackend } from './DataSourceWithBackend'; + +/** + * @alpha Experimental: Plugins implementing MigrationHandler interface will automatically have their queries migrated. + */ +export interface MigrationHandler { + hasBackendMigration: boolean; + shouldMigrate(query: DataQuery): boolean; +} + +export function isMigrationHandler(object: unknown): object is MigrationHandler { + return object instanceof DataSourceWithBackend && 'hasBackendMigration' in object && 'shouldMigrate' in object; +} + +async function postMigrateRequest(queries: TQuery[]): Promise { + if (!(config.featureToggles.grafanaAPIServerWithExperimentalAPIs || config.featureToggles.datasourceAPIServers)) { + console.warn('migrateQuery is only available with the experimental API server'); + return queries; + } + + // Obtaining the GroupName from the plugin ID as done in the backend, this is temporary until we have a better way to obtain it + // https://github.com/grafana/grafana/blob/e013cd427cb0457177e11f19ebd30bc523b36c76/pkg/plugins/apiserver.go#L10 + const dsnameURL = queries[0].datasource?.type?.replace(/^(grafana-)?(.*?)(-datasource)?$/, '$2'); + const groupName = `${dsnameURL}.datasource.grafana.app`; + // Asuming apiVersion is v0alpha1, we'll need to obtain it from a trusted source + const apiVersion = 'v0alpha1'; + const url = `/apis/${groupName}/${apiVersion}/namespaces/${config.namespace}/queryconvert`; + const request = { + queries: queries.map((query) => { + return { + ...query, + JSON: query, // JSON is not part of the type but it should be what holds the query + }; + }), + }; + const res = await getBackendSrv().post(url, request); + return res.queries.map((query: { JSON: TQuery }) => query.JSON); +} + +/** + * @alpha Experimental: Calls migration endpoint with one query. Requires grafanaAPIServerWithExperimentalAPIs or datasourceAPIServers feature toggle. + */ +export async function migrateQuery( + datasource: MigrationHandler, + query: TQuery +): Promise { + if (!datasource.hasBackendMigration || !datasource.shouldMigrate(query)) { + return query; + } + const res = await postMigrateRequest([query]); + return res[0]; +} + +/** + * @alpha Experimental: Calls migration endpoint with multiple queries. Requires grafanaAPIServerWithExperimentalAPIs or datasourceAPIServers feature toggle. + */ +export async function migrateRequest( + datasource: MigrationHandler, + request: DataQueryRequest +): Promise> { + if (!datasource.hasBackendMigration || !request.targets.some((query) => datasource.shouldMigrate(query))) { + return request; + } + const res = await postMigrateRequest(request.targets); + return { ...request, targets: res }; +} diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 6b0ca630ad3..05e4fbb8dd8 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "11.3.0-pre", + "version": "11.4.0-pre", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts index fd19d4267ca..3fb87719ff6 100644 --- a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options { limit: number; diff --git a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts index a6b78e671c4..371c56e5dff 100644 --- a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithTextFormatting { /** diff --git a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts index fbff065bcdd..9c5dcb7854b 100644 --- a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options extends common.OptionsWithLegend, common.SingleStatBaseOptions { displayMode: common.BarGaugeDisplayMode; diff --git a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts index 0dea20acc4b..328c4309cb2 100644 --- a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export enum VizDisplayMode { Candles = 'candles', diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index b40116598e7..3271a491db0 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export enum HorizontalConstraint { Center = 'center', diff --git a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts index f59d93e60d2..691e556e70f 100644 --- a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface MetricStat { /** diff --git a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts index d64925b0958..bccefe7dd16 100644 --- a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts index bdc84b74fc5..8ade131500b 100644 --- a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options { selectedSeries: number; diff --git a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts index d7d41860dad..9c2d289a95c 100644 --- a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export type UpdateConfig = { render: boolean, diff --git a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts index d21b25534ac..74a47041e89 100644 --- a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested); diff --git a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts index b9f809b5ea2..236e7f8be7e 100644 --- a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options extends common.SingleStatBaseOptions { minVizHeight: number; diff --git a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts index 8f3bb011101..20206475413 100644 --- a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options { basemap: ui.MapLayerOptions; diff --git a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts index b0764d00b3b..e27d99fdc8e 100644 --- a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; /** * Controls the color mode of the heatmap diff --git a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts index ff2aea48588..7837b9c9d19 100644 --- a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip { /** diff --git a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts index 5af91fec64c..5e31328c325 100644 --- a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options { dedupStrategy: common.LogsDedupStrategy; diff --git a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts index 6298dd986e2..b8d2b5ffc49 100644 --- a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export enum QueryEditorMode { Builder = 'builder', diff --git a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts index e011dddf763..47c39842d79 100644 --- a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts index 9405bd13003..357969cb116 100644 --- a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface ArcOption { /** diff --git a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts index 48aa1661936..4dda50ccb00 100644 --- a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; /** * Select the pie chart display style. diff --git a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts index c720c206ee0..5917c2b8d39 100644 --- a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options extends common.SingleStatBaseOptions { colorMode: common.BigValueColorMode; diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index c65eb67fd0c..d74a2f04623 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index 77e8c9cd68a..f51dc21c98b 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts index 435c36726ee..4f434b0fc1e 100644 --- a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; export interface Options { /** 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 4e3daebded8..b9620c89905 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 @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.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 f8b95bf3f7d..cf26d19549c 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 @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.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 bd84e783a06..cd1bb27fa3d 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 @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.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 6ca02d98289..8e98dfaa449 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 @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "11.3.0-pre"; +export const pluginVersion = "11.4.0-pre"; /** * Auto is "table" in the UI diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index b291478a8f3..7a6b260f2cc 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "@grafana/sql", - "version": "11.3.0-pre", + "version": "11.4.0-pre", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git", @@ -15,11 +15,11 @@ }, "dependencies": { "@emotion/css": "11.13.4", - "@grafana/data": "11.3.0-pre", - "@grafana/e2e-selectors": "11.3.0-pre", - "@grafana/experimental": "2.1.1", - "@grafana/runtime": "11.3.0-pre", - "@grafana/ui": "11.3.0-pre", + "@grafana/data": "11.4.0-pre", + "@grafana/e2e-selectors": "11.4.0-pre", + "@grafana/experimental": "2.1.2", + "@grafana/runtime": "11.4.0-pre", + "@grafana/ui": "11.4.0-pre", "@react-awesome-query-builder/ui": "6.6.3", "immutable": "4.3.7", "lodash": "4.17.21", @@ -35,9 +35,9 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@testing-library/dom": "10.0.0", + "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "15.0.2", + "@testing-library/react": "16.0.1", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 233095b9f89..15f9e089187 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": "11.3.0-pre", + "version": "11.4.0-pre", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -51,11 +51,11 @@ "@emotion/react": "11.13.3", "@emotion/serialize": "1.3.2", "@floating-ui/react": "0.26.24", - "@grafana/data": "11.3.0-pre", - "@grafana/e2e-selectors": "11.3.0-pre", + "@grafana/data": "11.4.0-pre", + "@grafana/e2e-selectors": "11.4.0-pre", "@grafana/faro-web-sdk": "^1.3.6", - "@grafana/schema": "11.3.0-pre", - "@hello-pangea/dnd": "16.6.0", + "@grafana/schema": "11.4.0-pre", + "@hello-pangea/dnd": "17.0.0", "@leeoniya/ufuzzy": "1.0.14", "@monaco-editor/react": "4.6.0", "@popperjs/core": "2.11.8", @@ -115,7 +115,7 @@ "uuid": "9.0.1" }, "devDependencies": { - "@babel/core": "7.25.7", + "@babel/core": "7.25.8", "@faker-js/faker": "^9.0.0", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.3.0", @@ -135,9 +135,9 @@ "@storybook/react": "^8.1.6", "@storybook/react-webpack5": "^8.1.6", "@storybook/theming": "^8.1.6", - "@testing-library/dom": "10.0.0", - "@testing-library/jest-dom": "6.4.2", - "@testing-library/react": "15.0.2", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.5.0", + "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/chance": "1.1.6", "@types/common-tags": "^1.8.0", diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx index 386e6fe56b9..9f81eb1fd72 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx @@ -3,8 +3,11 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react'; import { Chance } from 'chance'; import React, { ComponentProps, useEffect, useState } from 'react'; +import { useTheme2 } from '../../themes/ThemeContext'; import { Alert } from '../Alert/Alert'; +import { Divider } from '../Divider/Divider'; import { Field } from '../Forms/Field'; +import { Select } from '../Select/Select'; import { Combobox, ComboboxOption } from './Combobox'; @@ -73,7 +76,6 @@ async function generateOptions(amount: number): Promise { return Array.from({ length: amount }, (_, index) => ({ label: chance.sentence({ words: index % 5 }), value: chance.guid(), - //description: chance.sentence(), })); } @@ -88,7 +90,6 @@ const ManyOptionsStory: StoryFn = ({ numberOfOptions, ...arg setIsLoading(false); setOptions(options); setValue(options[5].value); - console.log("I've set stuff"); }); }, 1000); }, [numberOfOptions]); @@ -107,6 +108,123 @@ const ManyOptionsStory: StoryFn = ({ numberOfOptions, ...arg ); }; +const SelectComparisonStory: StoryFn = (args) => { + const [comboboxValue, setComboboxValue] = useState(args.value); + const theme = useTheme2(); + + return ( +
+ + { + setComboboxValue(val?.value || null); + action('onChange')(val); + }} + /> + + + + { + setComboboxValue(val?.value || null); + action('onChange')(val); + }} + /> + + + + + + { + setComboboxValue(val?.value || null); + action('onChange')(val); + }} + /> + + + + { + setComboboxValue(val?.value || null); + action('onChange')(val); + }} + /> + +
+ ); +}; + export const AutoSize: StoryObj = { args: { width: 'auto', @@ -130,6 +248,13 @@ export const CustomValue: StoryObj = { }, }; +export const ComparisonToSelect: StoryObj = { + args: { + numberOfOptions: 100, + }, + render: SelectComparisonStory, +}; + export default meta; function InDevDecorator(Story: React.ElementType) { diff --git a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts index d6de3c76dfd..e967c260f85 100644 --- a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts @@ -4,6 +4,12 @@ import { GrafanaTheme2 } from '@grafana/data'; const MAX_HEIGHT = 400; +// We need a px font size to accurately measure the width of items. +// This should be in sync with the body font size in the theme. +export const MENU_ITEM_FONT_SIZE = 14; +export const MENU_ITEM_FONT_WEIGHT = 500; +export const MENU_ITEM_PADDING_X = 8; + export const getComboboxStyles = (theme: GrafanaTheme2) => { return { menuClosed: css({ @@ -24,7 +30,7 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => { }), option: css({ label: 'grafana-select-option', - padding: '8px', + padding: MENU_ITEM_PADDING_X, position: 'absolute', display: 'flex', alignItems: 'center', @@ -53,6 +59,9 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => { label: 'grafana-select-option-label', textOverflow: 'ellipsis', overflow: 'hidden', + fontSize: MENU_ITEM_FONT_SIZE, + fontWeight: MENU_ITEM_FONT_WEIGHT, + letterSpacing: 0, // pr todo: text in grafana has a slightly different letter spacing, which causes measureText() to be ~5% off }), optionDescription: css({ label: 'grafana-select-option-description', diff --git a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts index bdac3088f96..60916c13ce5 100644 --- a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts +++ b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts @@ -1,12 +1,13 @@ import { autoUpdate, flip, size, useFloating } from '@floating-ui/react'; -import { useEffect, useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; + +import { measureText } from '../../utils'; import { ComboboxOption } from './Combobox'; +import { MENU_ITEM_FONT_SIZE, MENU_ITEM_FONT_WEIGHT, MENU_ITEM_PADDING_X } from './getComboboxStyles'; -// On every 100th index we will recalculate the width of the popover. -const INDEX_WIDTH_CALCULATION = 100; -// A multiplier guesstimate times the amount of characters. If any padding or image support etc. is added this will need to be updated. -const WIDTH_MULTIPLIER = 7.3; +// Only consider the first n items when calculating the width of the popover. +const WIDTH_CALCULATION_LIMIT_ITEMS = 100_000; /** * Used with Downshift to get the height of each item @@ -22,9 +23,10 @@ export const useComboboxFloat = ( ) => { const inputRef = useRef(null); const floatingRef = useRef(null); - const [popoverWidth, setPopoverWidth] = useState(undefined); const [popoverMaxWidth, setPopoverMaxWidth] = useState(undefined); + const scrollbarWidth = useMemo(() => getScrollbarWidth(), []); + // the order of middleware is important! const middleware = [ flip({ @@ -48,49 +50,43 @@ export const useComboboxFloat = ( whileElementsMounted: autoUpdate, }); - useEffect(() => { - if (range === null) { - return; - } - const startVisibleIndex = range?.startIndex; - const endVisibleIndex = range?.endIndex; + const longestItemWidth = useMemo(() => { + let longestItem = ''; + const itemsToLookAt = Math.min(items.length, WIDTH_CALCULATION_LIMIT_ITEMS); - if (typeof startVisibleIndex === 'undefined' || typeof endVisibleIndex === 'undefined') { - return; + for (let i = 0; i < itemsToLookAt; i++) { + const itemLabel = items[i].label; + longestItem = itemLabel.length > longestItem.length ? itemLabel : longestItem; } - // Scroll down and default case - if ( - startVisibleIndex === 0 || - (startVisibleIndex % INDEX_WIDTH_CALCULATION === 0 && startVisibleIndex >= INDEX_WIDTH_CALCULATION) - ) { - let maxLength = 0; - const calculationEnd = Math.min(items.length, endVisibleIndex + INDEX_WIDTH_CALCULATION); + const size = measureText(longestItem, MENU_ITEM_FONT_SIZE, MENU_ITEM_FONT_WEIGHT).width; - for (let i = startVisibleIndex; i < calculationEnd; i++) { - maxLength = Math.max(maxLength, items[i].label.length); - } - - setPopoverWidth(maxLength * WIDTH_MULTIPLIER); - } else if (endVisibleIndex % INDEX_WIDTH_CALCULATION === 0 && endVisibleIndex >= INDEX_WIDTH_CALCULATION) { - // Scroll up case - let maxLength = 0; - const calculationStart = Math.max(0, startVisibleIndex - INDEX_WIDTH_CALCULATION); - - for (let i = calculationStart; i < endVisibleIndex; i++) { - maxLength = Math.max(maxLength, items[i].label.length); - } - - setPopoverWidth(maxLength * WIDTH_MULTIPLIER); - } - }, [items, range, setPopoverWidth]); + return size + MENU_ITEM_PADDING_X * 2 + scrollbarWidth; + }, [items, scrollbarWidth]); const floatStyles = { ...floatingStyles, - width: popoverWidth, + width: longestItemWidth, maxWidth: popoverMaxWidth, minWidth: inputRef.current?.offsetWidth, }; return { inputRef, floatingRef, floatStyles }; }; + +// Creates a temporary div with a scrolling inner div to calculate the width of the scrollbar +function getScrollbarWidth(): number { + const outer = document.createElement('div'); + outer.style.visibility = 'hidden'; + outer.style.overflow = 'scroll'; + document.body.appendChild(outer); + + const inner = document.createElement('div'); + outer.appendChild(inner); + + const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; + + outer.parentNode?.removeChild(outer); + + return scrollbarWidth; +} diff --git a/packages/grafana-ui/src/components/Input/AutoSizeInput.test.tsx b/packages/grafana-ui/src/components/Input/AutoSizeInput.test.tsx index b47fc7da3d9..b2756e7b2d9 100644 --- a/packages/grafana-ui/src/components/Input/AutoSizeInput.test.tsx +++ b/packages/grafana-ui/src/components/Input/AutoSizeInput.test.tsx @@ -15,6 +15,16 @@ jest.mock('../../utils/measureText', () => { }); describe('AutoSizeInput', () => { + it('should support default Input API', () => { + const onChange = jest.fn(); + render(); + + const input: HTMLInputElement = screen.getByTestId('autosize-input'); + fireEvent.change(input, { target: { value: 'foo' } }); + + expect(onChange).toHaveBeenCalled(); + }); + it('should have default minWidth when empty', () => { render(); diff --git a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx index de855ff5302..d1e61001de3 100644 --- a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx +++ b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx @@ -20,6 +20,7 @@ export const AutoSizeInput = React.forwardRef((props, r minWidth = 10, maxWidth, onCommitChange, + onChange, onKeyDown, onBlur, value: controlledValue, @@ -48,6 +49,9 @@ export const AutoSizeInput = React.forwardRef((props, r ref={ref} value={value.toString()} onChange={(event) => { + if (onChange) { + onChange(event); + } setValue(event.currentTarget.value); }} width={inputWidth} diff --git a/packages/grafana-ui/src/components/Table/utils.test.ts b/packages/grafana-ui/src/components/Table/utils.test.ts index 6f1d7f937f5..4883ffec578 100644 --- a/packages/grafana-ui/src/components/Table/utils.test.ts +++ b/packages/grafana-ui/src/components/Table/utils.test.ts @@ -545,7 +545,8 @@ describe('Table utils', () => { }); describe('guessLongestField', () => { - it('should guess the longest field correct if there are few records', () => { + // FLAKY TEST - https://drone.grafana.net/grafana/grafana/201232/1/5 + it.skip('should guess the longest field correct if there are few records', () => { const data = getWrappableData(10); const config = { defaults: { diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index fd0a44d51e4..601011db495 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -23,7 +23,7 @@ require ( require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -53,7 +53,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/cel-go v0.20.1 // indirect + github.com/google/cel-go v0.21.0 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -98,7 +98,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.20.3 // indirect + github.com/prometheus/client_golang v1.20.4 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -118,21 +118,21 @@ require ( go.etcd.io/etcd/api/v3 v3.5.14 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/client/v3 v3.5.14 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/otel/trace v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.27.0 // indirect - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect @@ -143,11 +143,11 @@ require ( golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect - gonum.org/v1/gonum v0.14.0 // indirect + gonum.org/v1/gonum v0.15.1 // indirect google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/grpc v1.66.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index c52f20d6294..51002ade3a7 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -5,8 +5,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -106,8 +106,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= -github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= +github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -247,8 +247,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= -github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -342,8 +342,8 @@ go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA= go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw= go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok= go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= @@ -355,16 +355,16 @@ go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0/go.mod h1:1kbAgQa5lgYC go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 h1:m0yTiGDLUvVYaTFbAvCkVYIYcvwKt3G7OLoN77NUs/8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0/go.mod h1:wBQbT4UekBfegL2nx0Xk1vBcnzyBPsIVm9hRG4fYcr4= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= @@ -386,8 +386,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -462,8 +462,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0= -gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -471,18 +471,18 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 h1:CT2Thj5AuPV9phrYMtzX11k+XkzMGfRAet42PmoTATM= google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 12b2e6b2d41..c9f2963f120 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/slugify" internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/services/accesscontrol" grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" @@ -635,8 +636,6 @@ type folderK8sHandler struct { // #TODO check if it makes more sense to move this to FolderAPIBuilder accesscontrolService accesscontrol.Service userService user.Service - // #TODO remove after we handle the nested folder case - folderService folder.Service } //----------------------------------------------------------------------------------------- @@ -650,7 +649,6 @@ func newFolderK8sHandler(hs *HTTPServer) *folderK8sHandler { clientConfigProvider: hs.clientConfigProvider, accesscontrolService: hs.accesscontrolService, userService: hs.userService, - folderService: hs.folderService, } } @@ -884,55 +882,38 @@ func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item un return dtos.Folder{}, err } - parents := []*folder.Folder{} - if folderDTO.ParentUID != "" { - parents, err = fk8s.folderService.GetParents( - c.Req.Context(), - folder.GetParentsQuery{ - UID: folderDTO.UID, - OrgID: folderDTO.OrgID, - }) - if err != nil { - return dtos.Folder{}, err - } + if len(f.Fullpath) == 0 || len(f.FullpathUIDs) == 0 { + return folderDTO, nil } - // #TODO refactor so that we have just one function for converting to folder DTO - toParentDTO := func(fold *folder.Folder, checkCanView bool) (dtos.Folder, error) { - g, err := guardian.NewByFolder(c.Req.Context(), fold, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return dtos.Folder{}, err - } + parentsFullPath, err := internalfolders.GetParentTitles(f.Fullpath) + if err != nil { + return dtos.Folder{}, err + } + parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/") - if checkCanView { - canView, _ := g.CanView() - if !canView { - return dtos.Folder{ - UID: REDACTED, - Title: REDACTED, - }, nil - } - } - metrics.MFolderIDsAPICount.WithLabelValues(metrics.NewToFolderDTO).Inc() - - return dtos.Folder{ - UID: fold.UID, - Title: fold.Title, - URL: fold.URL, - }, nil + // The first part of the path is the newly created folder which we don't need to include + // in the parents field + if len(parentsFullPath) < 2 || len(parentsFullPathUIDs) < 2 { + return folderDTO, nil } - folderDTO.Parents = make([]dtos.Folder, 0, len(parents)) - for _, f := range parents { - DTO, err := toParentDTO(f, true) - if err != nil { - // #TODO add logging - // fk8s.log.Error("failed to convert folder to DTO", "folder", f.UID, "org", f.OrgID, "error", err) - continue - } - folderDTO.Parents = append(folderDTO.Parents, DTO) + parents := []dtos.Folder{} + for i, v := range parentsFullPath[1:] { + slug := slugify.Slugify(v) + uid := parentsFullPathUIDs[1:][i] + url := dashboards.GetFolderURL(uid, slug) + + parents = append(parents, dtos.Folder{ + UID: uid, + OrgID: c.SignedInUser.GetOrgID(), + Title: v, + URL: url, + }) } + folderDTO.Parents = parents + return folderDTO, nil } @@ -953,23 +934,19 @@ func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f return nil, nil } - var err error - parents := []*folder.Folder{} - if f.ParentUID != "" { - parents, err = fk8s.folderService.GetParents( - c.Req.Context(), - folder.GetParentsQuery{ - UID: f.UID, - OrgID: c.SignedInUser.GetOrgID(), - }) - if err != nil { - return nil, err - } + if len(f.FullpathUIDs) == 0 { + return map[string]bool{}, nil + } + + parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/") + // The first part of the path is the newly created folder which we don't need to check here + if len(parentsFullPathUIDs) < 2 { + return map[string]bool{}, nil } folderIDs := map[string]bool{f.UID: true} - for _, p := range parents { - folderIDs[p.UID] = true + for _, uid := range parentsFullPathUIDs[1:] { + folderIDs[uid] = true } allMetadata := getMultiAccessControlMetadata(c, dashboards.ScopeFoldersPrefix, folderIDs) diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 481dbeb1b39..f0ab30ee9c0 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -39,8 +39,8 @@ require ( golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/grpc v1.66.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 3e4397d878d..f2a1732dde0 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -135,10 +135,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index 3c16efc81ca..b0074c3eaf2 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -34,6 +34,11 @@ const AnnoKeyOriginPath = "grafana.app/originPath" const AnnoKeyOriginHash = "grafana.app/originHash" const AnnoKeyOriginTimestamp = "grafana.app/originTimestamp" +// #TODO revisit keeping these folder-specific annotations once we have complete support for mode 1 + +const AnnoKeyFullPath = "grafana.app/fullPath" +const AnnoKeyFullPathUIDs = "grafana.app/fullPathUIDs" + // ResourceOriginInfo is saved in annotations. This is used to identify where the resource came from // This object can model the same data as our existing provisioning table or a more general git sync type ResourceOriginInfo struct { @@ -101,6 +106,11 @@ type GrafanaMetaAccessor interface { // NOTE the type must match the existing value, or an error will be thrown SetStatus(any) error + GetFullPath() string + SetFullPath(path string) + GetFullPathUIDs() string + SetFullPathUIDs(path string) + // Find a title in the object // This will reflect the object and try to get: // * spec.title @@ -598,6 +608,22 @@ func (m *grafanaMetaAccessor) SetStatus(s any) (err error) { return } +func (m *grafanaMetaAccessor) GetFullPath() string { + return m.get(AnnoKeyFullPath) +} + +func (m *grafanaMetaAccessor) SetFullPath(path string) { + m.SetAnnotation(AnnoKeyFullPath, path) +} + +func (m *grafanaMetaAccessor) GetFullPathUIDs() string { + return m.get(AnnoKeyFullPathUIDs) +} + +func (m *grafanaMetaAccessor) SetFullPathUIDs(path string) { + m.SetAnnotation(AnnoKeyFullPathUIDs, path) +} + func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string { // look for Spec.Title or Spec.Name spec := m.r.FieldByName("Spec") diff --git a/pkg/apis/iam/v0alpha1/types_team.go b/pkg/apis/iam/v0alpha1/types_team.go index 9def089d78f..eff4e770f84 100644 --- a/pkg/apis/iam/v0alpha1/types_team.go +++ b/pkg/apis/iam/v0alpha1/types_team.go @@ -1,6 +1,8 @@ package v0alpha1 import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -15,6 +17,13 @@ type Team struct { type TeamSpec struct { Title string `json:"title,omitempty"` Email string `json:"email,omitempty"` + + // This is currently used for authorization checks but we don't want to expose it + InternalID int64 `json:"-"` +} + +func (t Team) AuthID() string { + return fmt.Sprintf("%d", t.Spec.InternalID) } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 4ec79bb6de8..3a728b84ae9 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -6,7 +6,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/grafana/authlib/claims v0.0.0-20240903121118-16441568af1e github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 - github.com/prometheus/client_golang v1.20.3 + github.com/prometheus/client_golang v1.20.4 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 go.opentelemetry.io/otel v1.30.0 @@ -64,16 +64,16 @@ require ( go.etcd.io/etcd/api/v3 v3.5.14 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/client/v3 v3.5.14 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sys v0.25.0 // indirect @@ -82,9 +82,9 @@ require ( golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/grpc v1.66.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index ca022eacc5f..73455d71aea 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -132,8 +132,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= -github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -188,22 +188,22 @@ go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA= go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw= go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok= go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 h1:g8+Y+7lnhH1DB0THjPPthzQ+RlzAntmTz8+TH2sRU0k= go.opentelemetry.io/contrib/propagators/jaeger v1.30.0/go.mod h1:lRMaD/FjOQJ2yz/MwOHYxP/BTCMFodNW/wuYDkJvdA4= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 h1:m0yTiGDLUvVYaTFbAvCkVYIYcvwKt3G7OLoN77NUs/8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0/go.mod h1:wBQbT4UekBfegL2nx0Xk1vBcnzyBPsIVm9hRG4fYcr4= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= @@ -224,8 +224,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -290,18 +290,18 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 h1:CT2Thj5AuPV9phrYMtzX11k+XkzMGfRAet42PmoTATM= google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index eb21e9948a2..2d855a277ba 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -20,7 +20,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 // @grafana/grafana-release-guild github.com/aws/aws-sdk-go v1.55.5 // @grafana/aws-datasources github.com/blang/semver/v4 v4.0.0 // @grafana/grafana-release-guild - github.com/docker/docker v26.0.2+incompatible // @grafana/grafana-release-guild + github.com/docker/docker v27.3.1+incompatible // @grafana/grafana-release-guild github.com/drone/drone-cli v1.6.1 // @grafana/grafana-release-guild github.com/gogo/protobuf v1.3.2 // indirect; @grafana/alerting-backend github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group @@ -32,9 +32,9 @@ require ( github.com/stretchr/testify v1.9.0 // @grafana/grafana-backend-group github.com/urfave/cli v1.22.15 // @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.27.1 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect; @grafana/plugins-platform-backend + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect; @grafana/plugins-platform-backend go.opentelemetry.io/otel v1.30.0 // indirect; @grafana/grafana-backend-group - go.opentelemetry.io/otel/sdk v1.29.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel/sdk v1.30.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/trace v1.30.0 // indirect; @grafana/grafana-backend-group golang.org/x/crypto v0.27.0 // indirect; @grafana/grafana-backend-group golang.org/x/mod v0.20.0 // @grafana/grafana-backend-group @@ -45,7 +45,7 @@ require ( golang.org/x/time v0.6.0 // indirect; @grafana/grafana-backend-group golang.org/x/tools v0.24.0 // indirect; @grafana/grafana-as-code google.golang.org/api v0.191.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.66.0 // indirect; @grafana/plugins-platform-backend + google.golang.org/grpc v1.67.0 // indirect; @grafana/plugins-platform-backend google.golang.org/protobuf v1.34.2 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) @@ -87,8 +87,8 @@ require ( go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/sys v0.25.0 // indirect google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect; @grafana/grafana-backend-group - google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -110,13 +110,13 @@ require ( github.com/vektah/gqlparser/v2 v2.5.11 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 // indirect go.opentelemetry.io/otel/log v0.2.0-alpha // indirect go.opentelemetry.io/otel/sdk/log v0.2.0-alpha // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect gotest.tools/v3 v3.5.1 // indirect ) diff --git a/pkg/build/go.sum b/pkg/build/go.sum index e348659ac5e..3e6e7758deb 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -227,8 +227,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= @@ -237,18 +237,18 @@ go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-2024051809000 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88/go.mod h1:JGG8ebaMO5nXOPnvKEl+DiA4MGwFjCbjsxT1WHIEBPY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha h1:z2s6Zba+OUyayRv5m1AXWNUTGh57K1iMhy6emU5QT5Y= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha/go.mod h1:paOXXyUgPW6jYxYkP0pB47H2zHE1fPvMJ4E4G9LHOi0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 h1:m0yTiGDLUvVYaTFbAvCkVYIYcvwKt3G7OLoN77NUs/8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0/go.mod h1:wBQbT4UekBfegL2nx0Xk1vBcnzyBPsIVm9hRG4fYcr4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0Jwtl01UFiyWZEMiJZBiTlv5A50zNs8lsthXqIio= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc= go.opentelemetry.io/otel/log v0.2.0-alpha h1:ixOPvMzserpqA07SENHvRzkZOsnG0XbPr74hv1AQ+n0= go.opentelemetry.io/otel/log v0.2.0-alpha/go.mod h1:vbFZc65yq4c4ssvXY43y/nIqkNJLxORrqw0L85P59LA= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= go.opentelemetry.io/otel/sdk/log v0.2.0-alpha h1:jGTkL/jroJ31jnP6jDl34N/mDOfRGGYZHcHsCM+5kWA= go.opentelemetry.io/otel/sdk/log v0.2.0-alpha/go.mod h1:Hd8Lw9FPGUM3pfY7iGMRvFaC2Nyau4Ajb5WnQ9OdIho= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= @@ -266,8 +266,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -339,18 +339,18 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 h1:CT2Thj5AuPV9phrYMtzX11k+XkzMGfRAet42PmoTATM= google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/pkg/plugins/backendplugin/backendplugin.go b/pkg/plugins/backendplugin/backendplugin.go index 2d8f6de2c27..baf755f88fa 100644 --- a/pkg/plugins/backendplugin/backendplugin.go +++ b/pkg/plugins/backendplugin/backendplugin.go @@ -3,7 +3,8 @@ package backendplugin import ( "github.com/grafana/grafana/pkg/plugins/log" + "go.opentelemetry.io/otel/trace" ) // PluginFactoryFunc is a function type for creating a Plugin. -type PluginFactoryFunc func(pluginID string, logger log.Logger, env func() []string) (Plugin, error) +type PluginFactoryFunc func(pluginID string, logger log.Logger, tracer trace.Tracer, env func() []string) (Plugin, error) diff --git a/pkg/plugins/backendplugin/coreplugin/core_plugin.go b/pkg/plugins/backendplugin/coreplugin/core_plugin.go index 6010b2650c5..0cb85b38c89 100644 --- a/pkg/plugins/backendplugin/coreplugin/core_plugin.go +++ b/pkg/plugins/backendplugin/coreplugin/core_plugin.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/backend" + "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" @@ -24,7 +25,7 @@ type corePlugin struct { // New returns a new backendplugin.PluginFactoryFunc for creating a core (built-in) backendplugin.Plugin. func New(opts backend.ServeOpts) backendplugin.PluginFactoryFunc { - return func(pluginID string, logger log.Logger, _ func() []string) (backendplugin.Plugin, error) { + return func(pluginID string, logger log.Logger, _ trace.Tracer, _ func() []string) (backendplugin.Plugin, error) { return &corePlugin{ pluginID: pluginID, logger: logger, diff --git a/pkg/plugins/backendplugin/coreplugin/core_plugin_test.go b/pkg/plugins/backendplugin/coreplugin/core_plugin_test.go index c3243c52ae9..6b4cc5a385a 100644 --- a/pkg/plugins/backendplugin/coreplugin/core_plugin_test.go +++ b/pkg/plugins/backendplugin/coreplugin/core_plugin_test.go @@ -8,13 +8,14 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/stretchr/testify/require" ) func TestCorePlugin(t *testing.T) { t.Run("New core plugin with empty opts should return expected values", func(t *testing.T) { factory := coreplugin.New(backend.ServeOpts{}) - p, err := factory("plugin", log.New("test"), nil) + p, err := factory("plugin", log.New("test"), fakes.InitializeNoopTracerForTest(), nil) require.NoError(t, err) require.NotNil(t, p) require.NoError(t, p.Start(context.Background())) @@ -47,7 +48,7 @@ func TestCorePlugin(t *testing.T) { return nil }), }) - p, err := factory("plugin", log.New("test"), nil) + p, err := factory("plugin", log.New("test"), fakes.InitializeNoopTracerForTest(), nil) require.NoError(t, err) require.NotNil(t, p) require.NoError(t, p.Start(context.Background())) diff --git a/pkg/plugins/backendplugin/coreplugin/registry.go b/pkg/plugins/backendplugin/coreplugin/registry.go index 23dc4524f75..423d74ac29d 100644 --- a/pkg/plugins/backendplugin/coreplugin/registry.go +++ b/pkg/plugins/backendplugin/coreplugin/registry.go @@ -254,7 +254,7 @@ func NewPlugin(pluginID string, cfg *setting.Cfg, httpClientProvider *httpclient if backendFactory == nil { return nil, ErrCorePluginNotFound } - bp, err := backendFactory(p.ID, p.Logger(), nil) + bp, err := backendFactory(p.ID, p.Logger(), tracer, nil) if err != nil { return nil, err } diff --git a/pkg/plugins/backendplugin/grpcplugin/client.go b/pkg/plugins/backendplugin/grpcplugin/client.go index 5ce776ec757..35920fef2cf 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client.go +++ b/pkg/plugins/backendplugin/grpcplugin/client.go @@ -6,6 +6,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/grpcplugin" goplugin "github.com/hashicorp/go-plugin" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + trace "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" "google.golang.org/grpc" "github.com/grafana/grafana/pkg/plugins/backendplugin" @@ -40,7 +42,20 @@ var pluginSet = map[int]goplugin.PluginSet{ }, } -func newClientConfig(executablePath string, args []string, env []string, skipHostEnvVars bool, logger log.Logger, +type clientTracerProvider struct { + tracer trace.Tracer + embedded.TracerProvider +} + +func (ctp *clientTracerProvider) Tracer(instrumentationName string, opts ...trace.TracerOption) trace.Tracer { + return ctp.tracer +} + +func newClientTracerProvider(tracer trace.Tracer) trace.TracerProvider { + return &clientTracerProvider{tracer: tracer} +} + +func newClientConfig(executablePath string, args []string, env []string, skipHostEnvVars bool, logger log.Logger, tracer trace.Tracer, versionedPlugins map[int]goplugin.PluginSet) *goplugin.ClientConfig { // We can ignore gosec G201 here, since the dynamic part of executablePath comes from the plugin definition // nolint:gosec @@ -55,7 +70,13 @@ func newClientConfig(executablePath string, args []string, env []string, skipHos Logger: logWrapper{Logger: logger}, AllowedProtocols: []goplugin.Protocol{goplugin.ProtocolGRPC}, GRPCDialOptions: []grpc.DialOption{ - grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + // https://github.com/grafana/app-platform-wg/issues/140 + // external plugins are loaded before k8s API server + // configures the tracing service thus failing to + // record trace span in the middleware. + // With code below we are passing the same tracer that k8s API server + // uses so that middleware is configured with tracer. + grpc.WithStatsHandler(otelgrpc.NewClientHandler(otelgrpc.WithTracerProvider(newClientTracerProvider(tracer)))), }, } } diff --git a/pkg/plugins/backendplugin/grpcplugin/client_proto.go b/pkg/plugins/backendplugin/grpcplugin/client_proto.go index 19c83d0c7dd..9922258f9e2 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client_proto.go +++ b/pkg/plugins/backendplugin/grpcplugin/client_proto.go @@ -4,6 +4,7 @@ import ( "context" "errors" + trace "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2" @@ -44,6 +45,7 @@ type ProtoClientOpts struct { ExecutableArgs []string Env []string Logger log.Logger + Tracer trace.Tracer } func NewProtoClient(opts ProtoClientOpts) (ProtoClient, error) { @@ -56,6 +58,7 @@ func NewProtoClient(opts ProtoClientOpts) (ProtoClient, error) { versionedPlugins: pluginSet, }, opts.Logger, + opts.Tracer, func() []string { return opts.Env }, ) diff --git a/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go b/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go index bce188928aa..d1c6152a0ef 100644 --- a/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go +++ b/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/hashicorp/go-plugin" + trace "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/infra/process" "github.com/grafana/grafana/pkg/plugins" @@ -37,17 +38,17 @@ const ( // newPlugin allocates and returns a new gRPC (external) backendplugin.Plugin. func newPlugin(descriptor PluginDescriptor) backendplugin.PluginFactoryFunc { - return func(pluginID string, logger log.Logger, env func() []string) (backendplugin.Plugin, error) { - return newGrpcPlugin(descriptor, logger, env), nil + return func(pluginID string, logger log.Logger, tracer trace.Tracer, env func() []string) (backendplugin.Plugin, error) { + return newGrpcPlugin(descriptor, logger, tracer, env), nil } } -func newGrpcPlugin(descriptor PluginDescriptor, logger log.Logger, env func() []string) *grpcPlugin { +func newGrpcPlugin(descriptor PluginDescriptor, logger log.Logger, tracer trace.Tracer, env func() []string) *grpcPlugin { return &grpcPlugin{ descriptor: descriptor, logger: logger, clientFactory: func() *plugin.Client { - return plugin.NewClient(newClientConfig(descriptor.executablePath, descriptor.executableArgs, env(), descriptor.skipHostEnvVars, logger, descriptor.versionedPlugins)) + return plugin.NewClient(newClientConfig(descriptor.executablePath, descriptor.executableArgs, env(), descriptor.skipHostEnvVars, logger, tracer, descriptor.versionedPlugins)) }, state: pluginStateNotStarted, } diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 2ba324f311d..f25daddc504 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -8,6 +8,8 @@ import ( "sync" "github.com/grafana/grafana-plugin-sdk-go/backend" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/auth" @@ -267,6 +269,14 @@ func (r *FakePluginRepo) PluginVersion(ctx context.Context, pluginID, version st return repo.VersionData{}, nil } +type fakeTracerProvider struct { + noop.TracerProvider +} + +func InitializeNoopTracerForTest() trace.Tracer { + return fakeTracerProvider{}.Tracer("test") +} + type FakePluginStorage struct { ExtractFunc func(_ context.Context, pluginID string, dirNameFunc storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) } @@ -340,7 +350,7 @@ func NewFakeBackendProcessProvider() *FakeBackendProcessProvider { } f.BackendFactoryFunc = func(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { f.Requested[p.ID]++ - return func(pluginID string, _ log.Logger, _ func() []string) (backendplugin.Plugin, error) { + return func(pluginID string, _ log.Logger, _ trace.Tracer, _ func() []string) (backendplugin.Plugin, error) { f.Invoked[pluginID]++ return &FakePluginClient{}, nil } diff --git a/pkg/plugins/manager/pipeline/initialization/steps.go b/pkg/plugins/manager/pipeline/initialization/steps.go index 2df09d1144a..02573f6ba20 100644 --- a/pkg/plugins/manager/pipeline/initialization/steps.go +++ b/pkg/plugins/manager/pipeline/initialization/steps.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" + "go.opentelemetry.io/otel/trace" ) // BackendClientInit implements an InitializeFunc for initializing a backend plugin process. @@ -21,20 +22,22 @@ type BackendClientInit struct { envVarProvider envvars.Provider backendProvider plugins.BackendFactoryProvider log log.Logger + tracer trace.Tracer } // BackendClientInitStep returns a new InitializeFunc for registering a backend plugin process. func BackendClientInitStep(envVarProvider envvars.Provider, - backendProvider plugins.BackendFactoryProvider) InitializeFunc { - return newBackendProcessRegistration(envVarProvider, backendProvider).Initialize + backendProvider plugins.BackendFactoryProvider, tracer trace.Tracer) InitializeFunc { + return newBackendProcessRegistration(envVarProvider, backendProvider, tracer).Initialize } func newBackendProcessRegistration(envVarProvider envvars.Provider, - backendProvider plugins.BackendFactoryProvider) *BackendClientInit { + backendProvider plugins.BackendFactoryProvider, tracer trace.Tracer) *BackendClientInit { return &BackendClientInit{ backendProvider: backendProvider, envVarProvider: envVarProvider, log: log.New("plugins.backend.registration"), + tracer: tracer, } } @@ -49,7 +52,7 @@ func (b *BackendClientInit) Initialize(ctx context.Context, p *plugins.Plugin) ( // this will ensure that the env variables are calculated every time a plugin is started envFunc := func() []string { return b.envVarProvider.PluginEnvVars(ctx, p) } - if backendClient, err := backendFactory(p.ID, p.Logger(), envFunc); err != nil { + if backendClient, err := backendFactory(p.ID, p.Logger(), b.tracer, envFunc); err != nil { return nil, err } else { p.RegisterClient(backendClient) diff --git a/pkg/plugins/manager/pipeline/initialization/steps_test.go b/pkg/plugins/manager/pipeline/initialization/steps_test.go index b5e43195e71..da00444e93e 100644 --- a/pkg/plugins/manager/pipeline/initialization/steps_test.go +++ b/pkg/plugins/manager/pipeline/initialization/steps_test.go @@ -4,11 +4,12 @@ import ( "context" "testing" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" ) func TestInitializer_Initialize(t *testing.T) { @@ -28,7 +29,7 @@ func TestInitializer_Initialize(t *testing.T) { Class: plugins.ClassCore, } - stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) + stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}, fakes.InitializeNoopTracerForTest()) var err error p, err = stepFunc(context.Background(), p) @@ -52,7 +53,7 @@ func TestInitializer_Initialize(t *testing.T) { Class: plugins.ClassExternal, } - stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) + stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}, fakes.InitializeNoopTracerForTest()) var err error p, err = stepFunc(context.Background(), p) @@ -76,7 +77,7 @@ func TestInitializer_Initialize(t *testing.T) { Class: plugins.ClassExternal, } - stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) + stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}, fakes.InitializeNoopTracerForTest()) var err error p, err = stepFunc(context.Background(), p) @@ -96,7 +97,7 @@ func TestInitializer_Initialize(t *testing.T) { i := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{ plugin: p, - }) + }, fakes.InitializeNoopTracerForTest()) var err error p, err = i(context.Background(), p) @@ -115,7 +116,7 @@ type fakeBackendProvider struct { } func (f *fakeBackendProvider) BackendFactory(_ context.Context, _ *plugins.Plugin) backendplugin.PluginFactoryFunc { - return func(_ string, _ log.Logger, _ func() []string) (backendplugin.Plugin, error) { + return func(_ string, _ log.Logger, _ trace.Tracer, _ func() []string) (backendplugin.Plugin, error) { return f.plugin, nil } } diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 8d79b5ff460..51e5bc9cc60 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -7,13 +7,13 @@ require ( github.com/grafana/grafana-plugin-sdk-go v0.251.0 github.com/json-iterator/go v1.1.12 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/prometheus/client_golang v1.20.3 + github.com/prometheus/client_golang v1.20.4 github.com/prometheus/common v0.55.0 github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.30.0 go.opentelemetry.io/otel/trace v1.30.0 - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e k8s.io/apimachinery v0.31.1 ) @@ -101,14 +101,14 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/goleak v1.3.0 // indirect @@ -119,10 +119,10 @@ require ( golang.org/x/text v0.18.0 // indirect golang.org/x/tools v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect - gonum.org/v1/gonum v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/grpc v1.66.0 // indirect + gonum.org/v1/gonum v0.15.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 2d878a7363f..af656a4d453 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -199,8 +199,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= -github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= @@ -262,8 +262,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= @@ -275,16 +275,16 @@ go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0/go.mod h1:1kbAgQa5lgYC go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 h1:m0yTiGDLUvVYaTFbAvCkVYIYcvwKt3G7OLoN77NUs/8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0/go.mod h1:wBQbT4UekBfegL2nx0Xk1vBcnzyBPsIVm9hRG4fYcr4= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= @@ -297,8 +297,8 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= @@ -349,14 +349,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0= -gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index 0deabc23ed3..31e8c051f2b 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -2,6 +2,7 @@ package folders import ( "fmt" + "regexp" "strconv" "time" @@ -85,10 +86,12 @@ func UnstructuredToLegacyFolder(item unstructured.Unstructured, orgID int64) *fo // #TODO add created by field if necessary // CreatedBy: meta.GetCreatedBy(), // UpdatedBy: meta.GetCreatedBy(), - URL: getURL(meta, title), - Created: createdTime, - Updated: createdTime, - OrgID: orgID, + URL: getURL(meta, title), + Created: createdTime, + Updated: createdTime, + OrgID: orgID, + Fullpath: meta.GetFullPath(), + FullpathUIDs: meta.GetFullPathUIDs(), } return f } @@ -183,6 +186,12 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) if v.ParentUID != "" { meta.SetFolder(v.ParentUID) } + if v.Fullpath != "" { + meta.SetFullPath(v.Fullpath) + } + if v.FullpathUIDs != "" { + meta.SetFullPathUIDs(v.FullpathUIDs) + } f.UID = gapiutil.CalculateClusterWideUID(f) return f, nil } @@ -226,3 +235,22 @@ func getCreated(meta utils.GrafanaMetaAccessor) (*time.Time, error) { } return created, nil } + +func GetParentTitles(fullPath string) ([]string, error) { + // Find all forward slashes which aren't escaped + r, err := regexp.Compile(`[^\\](/)`) + if err != nil { + return nil, err + } + indices := r.FindAllStringIndex(fullPath, -1) + + var start int + titles := []string{} + for _, i := range indices { + titles = append(titles, fullPath[start:i[0]+1]) + start = i[0] + 2 + } + + titles = append(titles, fullPath[start:]) + return titles, nil +} diff --git a/pkg/registry/apis/folders/conversions_test.go b/pkg/registry/apis/folders/conversions_test.go new file mode 100644 index 00000000000..70310d50968 --- /dev/null +++ b/pkg/registry/apis/folders/conversions_test.go @@ -0,0 +1,18 @@ +package folders + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetParentTitles(t *testing.T) { + path := "get\\/folder-folder-0/get\\/folder-folder-1/another" + + titles, err := GetParentTitles(path) + require.Nil(t, err) + require.Equal(t, 3, len(titles)) + require.Equal(t, "get\\/folder-folder-0", titles[0]) + require.Equal(t, "get\\/folder-folder-1", titles[1]) + require.Equal(t, "another", titles[2]) +} diff --git a/pkg/registry/apis/folders/legacy_storage.go b/pkg/registry/apis/folders/legacy_storage.go index 283c5d47887..858d2fdaef4 100644 --- a/pkg/registry/apis/folders/legacy_storage.go +++ b/pkg/registry/apis/folders/legacy_storage.go @@ -174,6 +174,7 @@ func (s *legacyStorage) Create(ctx context.Context, if err != nil { return nil, err } + parent := accessor.GetFolder() out, err := s.service.Create(ctx, &folder.CreateFolderCommand{ diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 470d4baf287..a891d5f249d 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -15,6 +15,7 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" @@ -154,7 +155,9 @@ func (b *FolderAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAP func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer { return authorizer.AuthorizerFunc( func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - if !attr.IsResourceRequest() || attr.GetName() == "" { + verb := attr.GetVerb() + name := attr.GetName() + if (!attr.IsResourceRequest()) || (name == "" && verb != utils.VerbCreate) { return authorizer.DecisionNoOpinion, "", nil } @@ -164,24 +167,24 @@ func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer { return authorizer.DecisionDeny, "valid user is required", err } - action := dashboards.ActionFoldersRead - scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(attr.GetName()) + scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(name) + eval := accesscontrol.EvalPermission(dashboards.ActionFoldersRead, scope) // "get" is used for sub-resources with GET http (parents, access, count) - switch attr.GetVerb() { - case "patch": + switch verb { + case utils.VerbCreate: + eval = accesscontrol.EvalPermission(dashboards.ActionFoldersCreate) + case utils.VerbPatch: fallthrough - case "create": + case utils.VerbUpdate: + eval = accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, scope) + case utils.VerbDeleteCollection: fallthrough - case "update": - action = dashboards.ActionFoldersWrite - case "deletecollection": - fallthrough - case "delete": - action = dashboards.ActionFoldersDelete + case utils.VerbDelete: + eval = accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, scope) } - ok, err := b.accessControl.Evaluate(ctx, user, accesscontrol.EvalPermission(action, scope)) + ok, err := b.accessControl.Evaluate(ctx, user, eval) if ok { return authorizer.DecisionAllow, "", nil } diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 8cc8cb68aba..6ea63508d4c 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -54,6 +54,19 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden return []string{fmt.Sprintf("serviceaccounts:id:%d", res.ID)}, nil }), }, + accesscontrol.ResourceAuthorizerOptions{ + Resource: iamv0.TeamResourceInfo.GetName(), + Attr: "id", + Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error) { + res, err := store.GetTeamInternalID(ctx, ns, legacy.GetTeamInternalIDQuery{ + UID: name, + }) + if err != nil { + return nil, err + } + return []string{fmt.Sprintf("teams:id:%d", res.ID)}, nil + }), + }, ) return gfauthorizer.NewResourceAuthorizer(client), client diff --git a/pkg/registry/apis/iam/legacy/sql.go b/pkg/registry/apis/iam/legacy/sql.go index 5e0e3f470ed..cdabeb6ac7c 100644 --- a/pkg/registry/apis/iam/legacy/sql.go +++ b/pkg/registry/apis/iam/legacy/sql.go @@ -22,6 +22,7 @@ type LegacyIdentityStore interface { ListServiceAccounts(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountsQuery) (*ListServiceAccountResult, error) ListServiceAccountTokens(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountTokenQuery) (*ListServiceAccountTokenResult, error) + GetTeamInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetTeamInternalIDQuery) (*GetTeamInternalIDResult, error) ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error) ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error) ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error) diff --git a/pkg/registry/apis/iam/legacy/team.go b/pkg/registry/apis/iam/legacy/team.go index b5e1e5d0e12..be7cb9ab61a 100644 --- a/pkg/registry/apis/iam/legacy/team.go +++ b/pkg/registry/apis/iam/legacy/team.go @@ -3,6 +3,7 @@ package legacy import ( "context" "database/sql" + "errors" "fmt" "time" @@ -14,6 +15,79 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) +type GetTeamInternalIDQuery struct { + OrgID int64 + UID string +} + +type GetTeamInternalIDResult struct { + ID int64 +} + +var sqlQueryTeamInternalIDTemplate = mustTemplate("team_internal_id.sql") + +func newGetTeamInternalID(sql *legacysql.LegacyDatabaseHelper, q *GetTeamInternalIDQuery) getTeamInternalIDQuery { + return getTeamInternalIDQuery{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + TeamTable: sql.Table("team"), + Query: q, + } +} + +type getTeamInternalIDQuery struct { + sqltemplate.SQLTemplate + TeamTable string + Query *GetTeamInternalIDQuery +} + +func (r getTeamInternalIDQuery) Validate() error { return nil } + +func (s *legacySQLStore) GetTeamInternalID( + ctx context.Context, + ns claims.NamespaceInfo, + query GetTeamInternalIDQuery, +) (*GetTeamInternalIDResult, error) { + query.OrgID = ns.OrgID + if query.OrgID == 0 { + return nil, fmt.Errorf("expected non zero org id") + } + + sql, err := s.sql(ctx) + if err != nil { + return nil, err + } + + req := newGetTeamInternalID(sql, &query) + q, err := sqltemplate.Execute(sqlQueryTeamInternalIDTemplate, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeamInternalIDTemplate.Name(), err) + } + + rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) + defer func() { + if rows != nil { + _ = rows.Close() + } + }() + + if err != nil { + return nil, err + } + + if !rows.Next() { + return nil, errors.New("team not found") + } + + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + + return &GetTeamInternalIDResult{ + id, + }, nil +} + type ListTeamQuery struct { OrgID int64 UID string diff --git a/pkg/registry/apis/iam/legacy/team_internal_id.sql b/pkg/registry/apis/iam/legacy/team_internal_id.sql new file mode 100644 index 00000000000..f2f4e3e5fad --- /dev/null +++ b/pkg/registry/apis/iam/legacy/team_internal_id.sql @@ -0,0 +1,5 @@ +SELECT t.id +FROM {{ .Ident .TeamTable }} as t +WHERE t.org_id = {{ .Arg .Query.OrgID }} +AND t.uid = {{ .Arg .Query.UID }} +LIMIT 1; diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 1a69dcf136c..1ef15561291 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -97,7 +97,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage := map[string]rest.Storage{} teamResource := iamv0.TeamResourceInfo - storage[teamResource.StoragePath()] = team.NewLegacyStore(b.store) + storage[teamResource.StoragePath()] = team.NewLegacyStore(b.store, b.accessClient) storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) teamBindingResource := iamv0.TeamBindingResourceInfo diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go index ae6f78291fd..864e5a36df8 100644 --- a/pkg/registry/apis/iam/team/store.go +++ b/pkg/registry/apis/iam/team/store.go @@ -2,6 +2,7 @@ package team import ( "context" + "fmt" "strconv" "k8s.io/apimachinery/pkg/apis/meta/internalversion" @@ -15,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/common" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/team" ) var ( @@ -27,12 +29,13 @@ var ( var resource = iamv0.TeamResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore) *LegacyStore { - return &LegacyStore{store} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient) *LegacyStore { + return &LegacyStore{store, ac} } type LegacyStore struct { store legacy.LegacyIdentityStore + ac claims.AccessClient } func (s *LegacyStore) New() runtime.Object { @@ -58,74 +61,84 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, return resource.TableConverter().ConvertToTable(ctx, object, tableOptions) } -func (s *LegacyStore) doList(ctx context.Context, ns claims.NamespaceInfo, query legacy.ListTeamQuery) (*iamv0.TeamList, error) { - rsp, err := s.store.ListTeams(ctx, ns, query) - if err != nil { - return nil, err - } - list := &iamv0.TeamList{ - ListMeta: metav1.ListMeta{ - ResourceVersion: strconv.FormatInt(rsp.RV, 10), +func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + res, err := common.List( + ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options), + func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.Team], error) { + found, err := s.store.ListTeams(ctx, ns, legacy.ListTeamQuery{ + Pagination: p, + }) + + if err != nil { + return nil, err + } + + teams := make([]iamv0.Team, 0, len(found.Teams)) + for _, t := range found.Teams { + teams = append(teams, toTeamObject(t, ns)) + } + + return &common.ListResponse[iamv0.Team]{ + Items: teams, + RV: found.RV, + Continue: found.Continue, + }, nil }, - } - for _, team := range rsp.Teams { - item := iamv0.Team{ - ObjectMeta: metav1.ObjectMeta{ - Name: team.UID, - Namespace: ns.Value, - CreationTimestamp: metav1.NewTime(team.Created), - ResourceVersion: strconv.FormatInt(team.Updated.UnixMilli(), 10), - }, - Spec: iamv0.TeamSpec{ - Title: team.Name, - Email: team.Email, - }, - } - meta, err := utils.MetaAccessor(&item) - if err != nil { - return nil, err - } - meta.SetUpdatedTimestamp(&team.Updated) - meta.SetOriginInfo(&utils.ResourceOriginInfo{ - Name: "SQL", - Path: strconv.FormatInt(team.ID, 10), - }) - list.Items = append(list.Items, item) + ) + + if err != nil { + return nil, fmt.Errorf("failed to list teams: %w", err) } - list.ListMeta.Continue = common.OptionalFormatInt(rsp.Continue) - list.ListMeta.ResourceVersion = common.OptionalFormatInt(rsp.RV) + list := &iamv0.TeamList{Items: res.Items} + list.ListMeta.Continue = common.OptionalFormatInt(res.Continue) + list.ListMeta.ResourceVersion = common.OptionalFormatInt(res.RV) return list, nil } -func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - ns, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - return s.doList(ctx, ns, legacy.ListTeamQuery{ - OrgID: ns.OrgID, - Pagination: common.PaginationFromListOptions(options), - }) -} - func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err } - rsp, err := s.doList(ctx, ns, legacy.ListTeamQuery{ + + found, err := s.store.ListTeams(ctx, ns, legacy.ListTeamQuery{ OrgID: ns.OrgID, UID: name, Pagination: common.Pagination{Limit: 1}, }) - if err != nil { - return nil, err + if found == nil || err != nil { + return nil, resource.NewNotFound(name) } - if len(rsp.Items) > 0 { - return &rsp.Items[0], nil + if len(found.Teams) < 1 { + return nil, resource.NewNotFound(name) } - return nil, resource.NewNotFound(name) + + obj := toTeamObject(found.Teams[0], ns) + return &obj, nil +} + +func toTeamObject(t team.Team, ns claims.NamespaceInfo) iamv0.Team { + obj := iamv0.Team{ + ObjectMeta: metav1.ObjectMeta{ + Name: t.UID, + Namespace: ns.Value, + CreationTimestamp: metav1.NewTime(t.Created), + ResourceVersion: strconv.FormatInt(t.Updated.UnixMilli(), 10), + }, + Spec: iamv0.TeamSpec{ + Title: t.Name, + Email: t.Email, + InternalID: t.ID, + }, + } + meta, _ := utils.MetaAccessor(&obj) + meta.SetUpdatedTimestamp(&t.Updated) + meta.SetOriginInfo(&utils.ResourceOriginInfo{ + Name: "SQL", + Path: strconv.FormatInt(t.ID, 10), + }) + + return obj } diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index e5bd2a0d4ba..dcdcb00d136 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -122,15 +122,22 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O // Parses the request and splits it into multiple sub queries (if necessary) req, err := b.parser.parseRequest(ctx, raw) if err != nil { + reason := metav1.StatusReasonInvalid + message := err.Error() + if errors.Is(err, datasources.ErrDataSourceNotFound) { + reason = metav1.StatusReasonNotFound // TODO, can we wrap the error somehow? - err = &errorsK8s.StatusError{ErrStatus: metav1.Status{ - Status: metav1.StatusFailure, - Code: http.StatusBadRequest, // the URL is found, but includes bad requests - Reason: metav1.StatusReasonNotFound, - Message: "datasource not found", - }} + message = "datasource not found" } + + err = &errorsK8s.StatusError{ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusBadRequest, + Reason: reason, + Message: message, + }} + responder.Error(err) return } diff --git a/pkg/registry/apis/search/register.go b/pkg/registry/apis/search/register.go index 78b2afbfd07..683f9dfbcf5 100644 --- a/pkg/registry/apis/search/register.go +++ b/pkg/registry/apis/search/register.go @@ -3,7 +3,12 @@ package search import ( "encoding/json" "net/http" + "net/url" + "strconv" + "github.com/grafana/grafana/pkg/api/response" + request2 "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/setting" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" @@ -21,14 +26,17 @@ import ( var _ builder.APIGroupBuilder = (*SearchAPIBuilder)(nil) type SearchAPIBuilder struct { - unified resource.ResourceClient + unified resource.ResourceClient + namespacer request2.NamespaceMapper } func NewSearchAPIBuilder( unified resource.ResourceClient, + cfg *setting.Cfg, ) (*SearchAPIBuilder, error) { return &SearchAPIBuilder{ - unified: unified, + unified: unified, + namespacer: request2.GetNamespaceMapper(cfg), }, nil } @@ -36,11 +44,12 @@ func RegisterAPIService( features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, unified resource.ResourceClient, + cfg *setting.Cfg, ) (*SearchAPIBuilder, error) { if !(features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) || features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs)) { return nil, nil } - builder, err := NewSearchAPIBuilder(unified) + builder, err := NewSearchAPIBuilder(unified, cfg) apiregistration.RegisterAPI(builder) return builder, err } @@ -73,17 +82,55 @@ func (b *SearchAPIBuilder) GetAPIRoutes() *builder.APIRoutes { }, }, }, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - urlQuery := r.URL.Query().Get("query") - searchRequest := &resource.SearchRequest{Query: urlQuery} + Handler: func(w http.ResponseWriter, r *http.Request) { + // get tenant + orgId, err := request2.OrgIDForList(r.Context()) + if err != nil { + response.Error(500, "failed to get orgId", err) + } + tenant := b.namespacer(orgId) + + queryParams, err := url.ParseQuery(r.URL.RawQuery) + if err != nil { + response.Error(500, "failed to parse query params", err) + } + + // get limit and offset from query params + limit := 0 + offset := 0 + if queryParams.Has("limit") { + limit, _ = strconv.Atoi(queryParams.Get("limit")) + } + if queryParams.Has("offset") { + offset, _ = strconv.Atoi(queryParams.Get("offset")) + } + + searchRequest := &resource.SearchRequest{ + Tenant: tenant, + Kind: queryParams.Get("kind"), + QueryType: queryParams.Get("queryType"), + Query: queryParams.Get("query"), + Limit: int64(limit), + Offset: int64(offset), + } + res, err := b.unified.Search(r.Context(), searchRequest) if err != nil { - panic(err) + response.Error(500, "search request failed", err) } - if err := json.NewEncoder(w).Encode(res); err != nil { - panic(err) + + // TODO need a nicer way of handling this + // the [][]byte response already contains the marshalled JSON, so we don't need to re-encode it + rawMessages := make([]json.RawMessage, len(res.GetItems())) + for i, item := range res.GetItems() { + rawMessages[i] = item.Value } - }), + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(rawMessages); err != nil { + response.Error(500, "failed to json encode raw response", err) + } + }, }, }, } diff --git a/pkg/services/accesscontrol/acimpl/accesscontrol.go b/pkg/services/accesscontrol/acimpl/accesscontrol.go index d540474abfc..ed3ee6eba45 100644 --- a/pkg/services/accesscontrol/acimpl/accesscontrol.go +++ b/pkg/services/accesscontrol/acimpl/accesscontrol.go @@ -3,12 +3,15 @@ package acimpl import ( "context" "errors" + "strconv" "time" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" @@ -127,6 +130,7 @@ func (a *AccessControl) evaluateZanzana(ctx context.Context, user identity.Reque a.log.Debug("evaluating zanzana", "user", tupleKey.User, "relation", tupleKey.Relation, "object", tupleKey.Object) allowed, err := a.Check(ctx, accesscontrol.CheckRequest{ + // Namespace: claims.OrgNamespaceFormatter(user.GetOrgID()), User: tupleKey.User, Relation: tupleKey.Relation, Object: tupleKey.Object, @@ -226,12 +230,44 @@ func (a *AccessControl) Check(ctx context.Context, req accesscontrol.CheckReques Relation: req.Relation, Object: req.Object, } - in := &openfgav1.CheckRequest{TupleKey: key} + + in := &openfgav1.CheckRequest{ + TupleKey: key, + } + + // Check direct access to resource first res, err := a.zclient.Check(ctx, in) if err != nil { return false, err } - return res.Allowed, err + + // no need to check folder access + if res.Allowed || req.Parent == "" { + return res.Allowed, nil + } + + // Check access through the parent folder + ns, err := claims.ParseNamespace(req.Namespace) + if err != nil { + return false, err + } + + folderKey := &openfgav1.CheckRequestTupleKey{ + User: req.User, + Relation: zanzana.TranslateToFolderRelation(req.Relation, req.ObjectType), + Object: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, req.Parent, "", strconv.FormatInt(ns.OrgID, 10)), + } + + folderReq := &openfgav1.CheckRequest{ + TupleKey: folderKey, + } + + folderRes, err := a.zclient.Check(ctx, folderReq) + if err != nil { + return false, err + } + + return folderRes.Allowed, nil } func (a *AccessControl) ListObjects(ctx context.Context, req accesscontrol.ListObjectsRequest) ([]string, error) { diff --git a/pkg/services/accesscontrol/migrator/zanzana.go b/pkg/services/accesscontrol/migrator/zanzana.go index f27316f2630..7c27f31b624 100644 --- a/pkg/services/accesscontrol/migrator/zanzana.go +++ b/pkg/services/accesscontrol/migrator/zanzana.go @@ -39,7 +39,6 @@ func NewZanzanaSynchroniser(client zanzana.Client, store db.DB, collectors ...Tu teamMembershipCollector(store), managedPermissionsCollector(store), folderTreeCollector(store), - dashboardFolderCollector(store), basicRolesCollector(store), customRolesCollector(store), basicRoleAssignemtCollector(store), @@ -58,6 +57,7 @@ func NewZanzanaSynchroniser(client zanzana.Client, store db.DB, collectors ...Tu // Sync runs all collectors and tries to write all collected tuples. // It will skip over any "sync group" that has already been written. func (z *ZanzanaSynchroniser) Sync(ctx context.Context) error { + z.log.Info("Starting zanzana permissions sync") ctx, span := tracer.Start(ctx, "accesscontrol.migrator.Sync") defer span.End() @@ -246,47 +246,6 @@ func folderTreeCollector(store db.DB) TupleCollector { } } -// dashboardFolderCollector collects information about dashboards parent folders -func dashboardFolderCollector(store db.DB) TupleCollector { - return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error { - ctx, span := tracer.Start(ctx, "accesscontrol.migrator.dashboardFolderCollector") - defer span.End() - - const collectorID = "folder" - query := ` - SELECT org_id, uid, folder_uid, is_folder FROM dashboard - WHERE is_folder = ` + store.GetDialect().BooleanStr(false) + ` - AND folder_uid IS NOT NULL - ` - type dashboard struct { - OrgID int64 `xorm:"org_id"` - UID string `xorm:"uid"` - ParentUID string `xorm:"folder_uid"` - } - - var dashboards []dashboard - err := store.WithDbSession(ctx, func(sess *db.Session) error { - return sess.SQL(query).Find(&dashboards) - }) - - if err != nil { - return err - } - - for _, d := range dashboards { - tuple := &openfgav1.TupleKey{ - User: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, d.ParentUID, "", strconv.FormatInt(d.OrgID, 10)), - Object: zanzana.NewScopedTupleEntry(zanzana.TypeDashboard, d.UID, "", strconv.FormatInt(d.OrgID, 10)), - Relation: zanzana.RelationParent, - } - - tuples[collectorID] = append(tuples[collectorID], tuple) - } - - return nil - } -} - // basicRolesCollector migrates basic roles to OpenFGA tuples func basicRolesCollector(store db.DB) TupleCollector { return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error { diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index c28a01429e1..9ee384540c1 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -589,9 +589,12 @@ type QueryWithOrg struct { } type CheckRequest struct { - User string - Relation string - Object string + Namespace string + User string + Relation string + Object string + ObjectType string + Parent string } type ListObjectsRequest struct { diff --git a/pkg/services/apiserver/options/extra.go b/pkg/services/apiserver/options/extra.go index aaf1052f3b0..715c319a62f 100644 --- a/pkg/services/apiserver/options/extra.go +++ b/pkg/services/apiserver/options/extra.go @@ -4,12 +4,15 @@ import ( "log/slog" "strconv" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/log/slogadapter" "github.com/spf13/pflag" + genericfeatures "k8s.io/apiserver/pkg/features" genericapiserver "k8s.io/apiserver/pkg/server" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/component-base/logs" "k8s.io/klog/v2" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/log/slogadapter" ) type ExtraOptions struct { @@ -40,7 +43,11 @@ func (o *ExtraOptions) Validate() []error { func (o *ExtraOptions) ApplyTo(c *genericapiserver.RecommendedConfig) error { handler := slogadapter.New(log.New("grafana-apiserver")) logger := slog.New(handler) - + if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(genericfeatures.APIServerTracing): false, + }); err != nil { + return err + } klog.SetSlogLogger(logger) if _, err := logs.GlogSetter(strconv.Itoa(o.Verbosity)); err != nil { logger.Error("failed to set log level", "error", err) diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index e7182dcab52..644adebc386 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -491,7 +491,7 @@ func orgIDFromHeader(req *http.Request) int64 { } func (s *Service) resolveExternalSessionFromIdentity(ctx context.Context, identity *authn.Identity, userID int64) *auth.ExternalSession { - if identity.OAuthToken == nil { + if identity.OAuthToken == nil && identity.SAMLSession == nil { return nil } @@ -506,18 +506,26 @@ func (s *Service) resolveExternalSessionFromIdentity(ctx context.Context, identi UserAuthID: info.Id, UserID: userID, } - extSession.AccessToken = identity.OAuthToken.AccessToken - extSession.RefreshToken = identity.OAuthToken.RefreshToken - extSession.ExpiresAt = identity.OAuthToken.Expiry - if idToken, ok := identity.OAuthToken.Extra("id_token").(string); ok && idToken != "" { - extSession.IDToken = idToken + if identity.OAuthToken != nil { + extSession.AccessToken = identity.OAuthToken.AccessToken + extSession.RefreshToken = identity.OAuthToken.RefreshToken + extSession.ExpiresAt = identity.OAuthToken.Expiry + + if idToken, ok := identity.OAuthToken.Extra("id_token").(string); ok && idToken != "" { + extSession.IDToken = idToken + } + + // As of https://openid.net/specs/openid-connect-session-1_0.html + if sessionState, ok := identity.OAuthToken.Extra("session_state").(string); ok && sessionState != "" { + extSession.SessionID = sessionState + } + + return extSession } - // As of https://openid.net/specs/openid-connect-session-1_0.html - if sessionState, ok := identity.OAuthToken.Extra("session_state").(string); ok && sessionState != "" { - extSession.SessionID = sessionState - } + extSession.SessionID = identity.SAMLSession.SessionIndex + extSession.NameID = identity.SAMLSession.NameID return extSession } diff --git a/pkg/services/authn/authnimpl/sync/org_sync.go b/pkg/services/authn/authnimpl/sync/org_sync.go index 87d79215ce5..8a50d4cd73f 100644 --- a/pkg/services/authn/authnimpl/sync/org_sync.go +++ b/pkg/services/authn/authnimpl/sync/org_sync.go @@ -87,18 +87,25 @@ func (s *OrgSync) SyncOrgRolesHook(ctx context.Context, id *authn.Identity, _ *a orgIDs := make([]int64, 0, len(id.OrgRoles)) // add any new org roles for orgId, orgRole := range id.OrgRoles { - orgIDs = append(orgIDs, orgId) if _, exists := handledOrgIds[orgId]; exists { + orgIDs = append(orgIDs, orgId) continue } // add role cmd := &org.AddOrgUserCommand{UserID: userID, Role: orgRole, OrgID: orgId} err := s.orgService.AddOrgUser(ctx, cmd) - if err != nil && !errors.Is(err, org.ErrOrgNotFound) { + + if errors.Is(err, org.ErrOrgNotFound) { + continue + } + + if err != nil { ctxLogger.Error("Failed to update active org for user", "error", err) return err } + + orgIDs = append(orgIDs, orgId) } // delete any removed org roles diff --git a/pkg/services/authn/authnimpl/sync/org_sync_test.go b/pkg/services/authn/authnimpl/sync/org_sync_test.go index b62bcbcbf00..246aec07399 100644 --- a/pkg/services/authn/authnimpl/sync/org_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/org_sync_test.go @@ -24,7 +24,8 @@ import ( ) func TestOrgSync_SyncOrgRolesHook(t *testing.T) { - orgService := &orgtest.FakeOrgService{ExpectedUserOrgDTO: []*org.UserOrgDTO{ + orgService := &orgtest.MockService{} + orgService.On("GetUserOrgList", mock.Anything, mock.Anything).Return([]*org.UserOrgDTO{ { OrgID: 1, Role: org.RoleEditor, @@ -33,14 +34,16 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) { OrgID: 3, Role: org.RoleViewer, }, - }, - ExpectedOrgListResponse: orgtest.OrgListResponse{ - { - OrgID: 3, - Response: nil, - }, - }, - } + }, nil) + orgService.On("RemoveOrgUser", mock.Anything, mock.MatchedBy(func(cmd *org.RemoveOrgUserCommand) bool { + return cmd.OrgID == 3 && cmd.UserID == 1 + })).Return(nil) + orgService.On("UpdateOrgUser", mock.Anything, mock.MatchedBy(func(cmd *org.UpdateOrgUserCommand) bool { + return cmd.OrgID == 1 && cmd.UserID == 1 && cmd.Role == org.RoleAdmin + })).Return(nil) + orgService.On("AddOrgUser", mock.Anything, mock.MatchedBy(func(cmd *org.AddOrgUserCommand) bool { + return cmd.OrgID == 2 && cmd.UserID == 1 && cmd.Role == org.RoleEditor + })).Return(org.ErrOrgNotFound) acService := &actest.FakeService{} userService := &usertest.FakeUserService{ExpectedUser: &user.User{ ID: 1, @@ -67,7 +70,7 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) { wantID *authn.Identity }{ { - name: "add user to multiple orgs", + name: "add user to multiple orgs, should not set the user's default orgID to an org that does not exist", fields: fields{ userService: userService, orgService: orgService, @@ -100,7 +103,7 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) { Name: "test", Email: "test", OrgRoles: map[int64]identity.RoleType{1: org.RoleAdmin, 2: org.RoleEditor}, - OrgID: 1, //set using org + OrgID: 1, // set using org IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncOrgRoles: true, diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 26f98338ec2..638101615a2 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -185,10 +185,10 @@ func (s *ExtendedJWT) authenticateAsService(accessTokenClaims authlib.Claims[aut return &authn.Identity{ ID: id, UID: id, + Name: id, Type: t, OrgID: s.cfg.DefaultOrgID(), AccessTokenClaims: &accessTokenClaims, - IDTokenClaims: nil, AuthenticatedBy: login.ExtendedJWTModule, AuthID: accessTokenClaims.Subject, AllowedKubernetesNamespace: accessTokenClaims.Rest.Namespace, diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 6b61748dd03..3f9e9ee6197 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -228,6 +228,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { want: &authn.Identity{ ID: "this-uid", UID: "this-uid", + Name: "this-uid", Type: claims.TypeAccessPolicy, OrgID: 1, AccessTokenClaims: &validAccessTokenClaims, @@ -246,6 +247,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { want: &authn.Identity{ ID: "this-uid", UID: "this-uid", + Name: "this-uid", Type: claims.TypeAccessPolicy, OrgID: 1, AccessTokenClaims: &validAccessTokenClaimsWildcard, @@ -343,6 +345,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { want: &authn.Identity{ ID: "this-uid", UID: "this-uid", + Name: "this-uid", Type: claims.TypeAccessPolicy, OrgID: 1, AccessTokenClaims: &validAccessTokenClaimsWithStackSet, @@ -369,6 +372,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { want: &authn.Identity{ ID: "this-uid", UID: "this-uid", + Name: "this-uid", Type: claims.TypeAccessPolicy, OrgID: 1, AccessTokenClaims: &validAccessTokenClaimsWithDeprecatedStackClaimSet, diff --git a/pkg/services/authn/identity.go b/pkg/services/authn/identity.go index e137809c1d2..a072ee7d807 100644 --- a/pkg/services/authn/identity.go +++ b/pkg/services/authn/identity.go @@ -64,6 +64,8 @@ type Identity struct { Groups []string // OAuthToken is the OAuth token used to authenticate the entity. OAuthToken *oauth2.Token + // SAMLSession is the SAML session information. + SAMLSession *login.SAMLSession // SessionToken is the session token used to authenticate the entity. SessionToken *usertoken.UserToken // ClientParams are hints for the auth service on how to handle the identity. diff --git a/pkg/services/authz/zanzana/logger/logger.go b/pkg/services/authz/zanzana/logger/logger.go index ac953ed4f15..1e1f2a6435d 100644 --- a/pkg/services/authz/zanzana/logger/logger.go +++ b/pkg/services/authz/zanzana/logger/logger.go @@ -4,10 +4,14 @@ import ( "context" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/openfga/openfga/pkg/logger" ) +var _ logger.Logger = (*ZanzanaLogger)(nil) + // ZanzanaLogger is a grafana logger wrapper compatible with OpenFGA logger interface type ZanzanaLogger struct { logger log.Logger @@ -36,6 +40,13 @@ func zapFieldsToArgs(fields []zap.Field) []any { return args } +// With implements logger.Logger. +func (l *ZanzanaLogger) With(fields ...zapcore.Field) logger.Logger { + return &ZanzanaLogger{ + logger: l.logger.New(zapFieldsToArgs(fields)...), + } +} + func (l *ZanzanaLogger) Debug(msg string, fields ...zap.Field) { l.logger.Debug(msg, zapFieldsToArgs(fields)...) } diff --git a/pkg/services/authz/zanzana/schema/dashboard.fga b/pkg/services/authz/zanzana/schema/dashboard.fga index ee08372ac7f..b9e77da81a1 100644 --- a/pkg/services/authz/zanzana/schema/dashboard.fga +++ b/pkg/services/authz/zanzana/schema/dashboard.fga @@ -28,18 +28,17 @@ extend type org type dashboard relations define org: [org] - define parent: [folder] - define read: [user, team#member, role#assignee] or dashboard_read from parent or dashboard_read from org - define write: [user, team#member, role#assignee] or dashboard_write from parent or dashboard_write from org - define delete: [user, team#member, role#assignee] or dashboard_delete from parent or dashboard_delete from org - define create: [user, team#member, role#assignee] or dashboard_create from parent or dashboard_create from org - define permissions_read: [user, team#member, role#assignee] or dashboard_permissions_read from parent or dashboard_permissions_read from org - define permissions_write: [user, team#member, role#assignee] or dashboard_permissions_write from parent or dashboard_permissions_write from org + define read: [user, team#member, role#assignee] or dashboard_read from org + define write: [user, team#member, role#assignee] or dashboard_write from org + define delete: [user, team#member, role#assignee] or dashboard_delete from org + define create: [user, team#member, role#assignee] or dashboard_create from org + define permissions_read: [user, team#member, role#assignee] or dashboard_permissions_read from org + define permissions_write: [user, team#member, role#assignee] or dashboard_permissions_write from org - define public_write: [user, team#member, role#assignee] or dashboard_public_write from parent or dashboard_public_write from org or write - define annotations_create: [user, team#member, role#assignee] or dashboard_annotations_create from parent or dashboard_annotations_create from org - define annotations_read: [user, team#member, role#assignee] or dashboard_annotations_read from parent or dashboard_annotations_read from org - define annotations_write: [user, team#member, role#assignee] or dashboard_annotations_write from parent or dashboard_annotations_write from org - define annotations_delete: [user, team#member, role#assignee] or dashboard_annotations_delete from parent or dashboard_annotations_delete from org + define public_write: [user, team#member, role#assignee] or dashboard_public_write from org or write + define annotations_create: [user, team#member, role#assignee] or dashboard_annotations_create from org + define annotations_read: [user, team#member, role#assignee] or dashboard_annotations_read from org + define annotations_write: [user, team#member, role#assignee] or dashboard_annotations_write from org + define annotations_delete: [user, team#member, role#assignee] or dashboard_annotations_delete from org diff --git a/pkg/services/authz/zanzana/store/assets/assets.go b/pkg/services/authz/zanzana/store/assets/assets.go deleted file mode 100644 index fb55d16eef2..00000000000 --- a/pkg/services/authz/zanzana/store/assets/assets.go +++ /dev/null @@ -1,10 +0,0 @@ -package assets - -import "embed" - -// EmbedMigrations within the grafana binary. -// -//go:embed migrations/* -var EmbedMigrations embed.FS - -const SQLiteMigrationDir = "migrations/sqlite" diff --git a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/001_initialize_schema.sql b/pkg/services/authz/zanzana/store/assets/migrations/sqlite/001_initialize_schema.sql deleted file mode 100644 index ecceebfb58b..00000000000 --- a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/001_initialize_schema.sql +++ /dev/null @@ -1,56 +0,0 @@ --- +goose Up -CREATE TABLE tuple ( - store CHAR(26) NOT NULL, - object_type VARCHAR(128) NOT NULL, - object_id VARCHAR(128) NOT NULL, - relation VARCHAR(50) NOT NULL, - _user VARCHAR(256) NOT NULL, - user_type VARCHAR(7) NOT NULL, - ulid CHAR(26) NOT NULL, - inserted_at TIMESTAMP NOT NULL, - PRIMARY KEY (store, object_type, object_id, relation, _user) -); - -CREATE UNIQUE INDEX idx_tuple_ulid ON tuple (ulid); - -CREATE TABLE authorization_model ( - store CHAR(26) NOT NULL, - authorization_model_id CHAR(26) NOT NULL, - type VARCHAR(256) NOT NULL, - type_definition BLOB, - PRIMARY KEY (store, authorization_model_id, type) -); - -CREATE TABLE store ( - id CHAR(26) PRIMARY KEY, - name VARCHAR(64) NOT NULL, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP, - deleted_at TIMESTAMP -); - -CREATE TABLE assertion ( - store CHAR(26) NOT NULL, - authorization_model_id CHAR(26) NOT NULL, - assertions BLOB, - PRIMARY KEY (store, authorization_model_id) -); - -CREATE TABLE changelog ( - store CHAR(26) NOT NULL, - object_type VARCHAR(256) NOT NULL, - object_id VARCHAR(256) NOT NULL, - relation VARCHAR(50) NOT NULL, - _user VARCHAR(512) NOT NULL, - operation INTEGER NOT NULL, - ulid CHAR(26) NOT NULL, - inserted_at TIMESTAMP NOT NULL, - PRIMARY KEY (store, ulid, object_type) -); - --- +goose Down -DROP TABLE tuple; -DROP TABLE authorization_model; -DROP TABLE store; -DROP TABLE assertion; -DROP TABLE changelog; diff --git a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/002_add_authorization_model_version.sql b/pkg/services/authz/zanzana/store/assets/migrations/sqlite/002_add_authorization_model_version.sql deleted file mode 100644 index 26ce15ac18b..00000000000 --- a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/002_add_authorization_model_version.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +goose Up -ALTER TABLE authorization_model ADD COLUMN schema_version VARCHAR(5) NOT NULL DEFAULT '1.0'; - --- +goose Down -ALTER TABLE authorization_model DROP COLUMN schema_version; diff --git a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/003_add_reverse_lookup_index.sql b/pkg/services/authz/zanzana/store/assets/migrations/sqlite/003_add_reverse_lookup_index.sql deleted file mode 100644 index 9a3ee2a82bc..00000000000 --- a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/003_add_reverse_lookup_index.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +goose Up -CREATE INDEX idx_reverse_lookup_user on tuple (store, object_type, relation, _user); - --- +goose Down -DROP INDEX idx_reverse_lookup_user on tuple; diff --git a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/004_add_authorization_model_serialized_protobuf.sql b/pkg/services/authz/zanzana/store/assets/migrations/sqlite/004_add_authorization_model_serialized_protobuf.sql deleted file mode 100644 index 1e47f290e51..00000000000 --- a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/004_add_authorization_model_serialized_protobuf.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +goose Up -ALTER TABLE authorization_model ADD COLUMN serialized_protobuf LONGBLOB; - --- +goose Down -ALTER TABLE authorization_model DROP COLUMN serialized_protobuf; diff --git a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/005_add_conditions_to_tuples.sql b/pkg/services/authz/zanzana/store/assets/migrations/sqlite/005_add_conditions_to_tuples.sql deleted file mode 100644 index 37d039ca799..00000000000 --- a/pkg/services/authz/zanzana/store/assets/migrations/sqlite/005_add_conditions_to_tuples.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up -ALTER TABLE tuple ADD COLUMN condition_name VARCHAR(256); -ALTER TABLE tuple ADD COLUMN condition_context LONGBLOB; -ALTER TABLE changelog ADD COLUMN condition_name VARCHAR(256); -ALTER TABLE changelog ADD COLUMN condition_context LONGBLOB; - --- +goose Down -ALTER TABLE tuple DROP COLUMN condition_name; -ALTER TABLE tuple DROP COLUMN condition_context; -ALTER TABLE changelog DROP COLUMN condition_name; -ALTER TABLE changelog DROP COLUMN condition_context; diff --git a/pkg/services/authz/zanzana/store/migration/migrator.go b/pkg/services/authz/zanzana/store/migration/migrator.go index 3f0d13f7343..3a2db5aa8c9 100644 --- a/pkg/services/authz/zanzana/store/migration/migrator.go +++ b/pkg/services/authz/zanzana/store/migration/migrator.go @@ -15,13 +15,17 @@ import ( func Run(cfg *setting.Cfg, typ, connStr string, fs embed.FS, path string) error { engine, err := xorm.NewEngine(typ, connStr) if err != nil { - return fmt.Errorf("failed to parse database config: %w", err) + return fmt.Errorf("failed to create db engine: %w", err) } m := migrator.NewMigrator(engine, cfg) m.AddCreateMigration() - return RunWithMigrator(m, cfg, fs, path) + if err := RunWithMigrator(m, cfg, fs, path); err != nil { + return err + } + + return engine.Close() } func RunWithMigrator(m *migrator.Migrator, cfg *setting.Cfg, fs embed.FS, path string) error { diff --git a/pkg/services/authz/zanzana/store/sqlite/config.go b/pkg/services/authz/zanzana/store/sqlite/config.go deleted file mode 100644 index b47404d6028..00000000000 --- a/pkg/services/authz/zanzana/store/sqlite/config.go +++ /dev/null @@ -1,15 +0,0 @@ -package sqlite - -import "github.com/openfga/openfga/pkg/storage/sqlcommon" - -type Config struct { - *sqlcommon.Config - QueryRetries int -} - -func NewConfig() *Config { - return &Config{ - Config: sqlcommon.NewConfig(), - QueryRetries: 0, - } -} diff --git a/pkg/services/authz/zanzana/store/sqlite/store.go b/pkg/services/authz/zanzana/store/sqlite/store.go deleted file mode 100644 index 0860eb3b968..00000000000 --- a/pkg/services/authz/zanzana/store/sqlite/store.go +++ /dev/null @@ -1,821 +0,0 @@ -package sqlite - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/url" - "strings" - "time" - - sq "github.com/Masterminds/squirrel" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "go.opentelemetry.io/otel" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - // Pull in sqlite driver. - "github.com/mattn/go-sqlite3" - - openfgav1 "github.com/openfga/api/proto/openfga/v1" - "github.com/openfga/openfga/pkg/logger" - "github.com/openfga/openfga/pkg/storage" - "github.com/openfga/openfga/pkg/storage/sqlcommon" - tupleUtils "github.com/openfga/openfga/pkg/tuple" -) - -var tracer = otel.Tracer("openfga/pkg/storage/sqlite") - -// SQLite provides a SQLite based implementation of [storage.OpenFGADatastore]. -type SQLite struct { - stbl sq.StatementBuilderType - cfg *Config - db *sql.DB - dbInfo *sqlcommon.DBInfo - sqlTime sq.Sqlizer - logger logger.Logger - dbStatsCollector prometheus.Collector -} - -// Ensures that SQLite implements the OpenFGADatastore interface. -var _ storage.OpenFGADatastore = (*SQLite)(nil) - -// New creates a new [SQLite] storage. -func New(uri string, cfg *Config) (*SQLite, error) { - // Set journal mode and busy timeout pragmas if not specified. - query := url.Values{} - var err error - - if i := strings.Index(uri, "?"); i != -1 { - query, err = url.ParseQuery(uri[i+1:]) - if err != nil { - return nil, fmt.Errorf("error parsing dsn: %w", err) - } - - uri = uri[:i] - } - - foundJournalMode := false - foundBusyTimeout := false - for _, val := range query["_pragma"] { - if strings.HasPrefix(val, "journal_mode") { - foundJournalMode = true - } else if strings.HasPrefix(val, "busy_timeout") { - foundBusyTimeout = true - } - } - - if !foundJournalMode { - query.Add("_pragma", "journal_mode(WAL)") - } - if !foundBusyTimeout { - query.Add("_pragma", "busy_timeout(500)") - } - - uri += "?" + query.Encode() - - db, err := sql.Open("sqlite", uri) - if err != nil { - return nil, fmt.Errorf("initialize sqlite connection: %w", err) - } - - return NewWithDB(db, cfg) -} - -// NewWithDB creates a new [SQLite] storage using provided [*sql.DB] -func NewWithDB(db *sql.DB, cfg *Config) (*SQLite, error) { - var collector prometheus.Collector - if cfg.ExportMetrics { - collector = collectors.NewDBStatsCollector(db, "openfga") - if err := prometheus.Register(collector); err != nil { - return nil, fmt.Errorf("initialize metrics: %w", err) - } - } - - sqlTime := sq.Expr("datetime('subsec')") - stbl := sq.StatementBuilder.RunWith(db) - dbInfo := sqlcommon.NewDBInfo(db, stbl, sqlTime) - - return &SQLite{ - cfg: cfg, - stbl: stbl, - db: db, - sqlTime: sqlTime, - dbInfo: dbInfo, - logger: cfg.Logger, - dbStatsCollector: collector, - }, nil -} - -// Close see [storage.OpenFGADatastore].Close. -func (m *SQLite) Close() { - if m.dbStatsCollector != nil { - prometheus.Unregister(m.dbStatsCollector) - } - _ = m.db.Close() -} - -// Read see [storage.RelationshipTupleReader].Read. -func (m *SQLite) Read(ctx context.Context, store string, tupleKey *openfgav1.TupleKey) (storage.TupleIterator, error) { - ctx, span := tracer.Start(ctx, "sqlite.Read") - defer span.End() - - return m.read(ctx, store, tupleKey, nil) -} - -// ReadPage see [storage.RelationshipTupleReader].ReadPage. -func (m *SQLite) ReadPage( - ctx context.Context, - store string, - tupleKey *openfgav1.TupleKey, - opts storage.PaginationOptions, -) ([]*openfgav1.Tuple, []byte, error) { - ctx, span := tracer.Start(ctx, "sqlite.ReadPage") - defer span.End() - - iter, err := m.read(ctx, store, tupleKey, &opts) - if err != nil { - return nil, nil, err - } - defer iter.Stop() - - return iter.ToArray(opts) -} - -func (m *SQLite) read(ctx context.Context, store string, tupleKey *openfgav1.TupleKey, opts *storage.PaginationOptions) (*sqlcommon.SQLTupleIterator, error) { - ctx, span := tracer.Start(ctx, "sqlite.read") - defer span.End() - - sb := m.stbl. - Select( - "store", "object_type", "object_id", "relation", "_user", - "condition_name", "condition_context", "ulid", "inserted_at", - ). - From("tuple"). - Where(sq.Eq{"store": store}) - if opts != nil { - sb = sb.OrderBy("ulid") - } - objectType, objectID := tupleUtils.SplitObject(tupleKey.GetObject()) - if objectType != "" { - sb = sb.Where(sq.Eq{"object_type": objectType}) - } - if objectID != "" { - sb = sb.Where(sq.Eq{"object_id": objectID}) - } - if tupleKey.GetRelation() != "" { - sb = sb.Where(sq.Eq{"relation": tupleKey.GetRelation()}) - } - if tupleKey.GetUser() != "" { - sb = sb.Where(sq.Eq{"_user": tupleKey.GetUser()}) - } - if opts != nil && opts.From != "" { - token, err := sqlcommon.UnmarshallContToken(opts.From) - if err != nil { - return nil, err - } - sb = sb.Where(sq.GtOrEq{"ulid": token.Ulid}) - } - if opts != nil && opts.PageSize != 0 { - sb = sb.Limit(uint64(opts.PageSize + 1)) // + 1 is used to determine whether to return a continuation token. - } - - rows, err := sb.QueryContext(ctx) - if err != nil { - return nil, handleSQLError(err) - } - - return sqlcommon.NewSQLTupleIterator(rows), nil -} - -// Write see [storage.RelationshipTupleWriter].Write. -func (m *SQLite) Write(ctx context.Context, store string, deletes storage.Deletes, writes storage.Writes) error { - ctx, span := tracer.Start(ctx, "sqlite.Write") - defer span.End() - - if len(deletes)+len(writes) > m.MaxTuplesPerWrite() { - return storage.ErrExceededWriteBatchLimit - } - - return m.busyRetry(func() error { - now := time.Now().UTC() - return write(ctx, m.db, m.stbl, m.sqlTime, store, deletes, writes, now) - }) -} - -// ReadUserTuple see [storage.RelationshipTupleReader].ReadUserTuple. -func (m *SQLite) ReadUserTuple(ctx context.Context, store string, tupleKey *openfgav1.TupleKey) (*openfgav1.Tuple, error) { - ctx, span := tracer.Start(ctx, "sqlite.ReadUserTuple") - defer span.End() - - objectType, objectID := tupleUtils.SplitObject(tupleKey.GetObject()) - userType := tupleUtils.GetUserTypeFromUser(tupleKey.GetUser()) - - var conditionName sql.NullString - var conditionContext []byte - var record storage.TupleRecord - err := m.stbl. - Select( - "object_type", "object_id", "relation", "_user", - "condition_name", "condition_context", - ). - From("tuple"). - Where(sq.Eq{ - "store": store, - "object_type": objectType, - "object_id": objectID, - "relation": tupleKey.GetRelation(), - "_user": tupleKey.GetUser(), - "user_type": userType, - }). - QueryRowContext(ctx). - Scan( - &record.ObjectType, - &record.ObjectID, - &record.Relation, - &record.User, - &conditionName, - &conditionContext, - ) - if err != nil { - return nil, handleSQLError(err) - } - - if conditionName.String != "" { - record.ConditionName = conditionName.String - - if conditionContext != nil { - var conditionContextStruct structpb.Struct - if err := proto.Unmarshal(conditionContext, &conditionContextStruct); err != nil { - return nil, err - } - record.ConditionContext = &conditionContextStruct - } - } - - return record.AsTuple(), nil -} - -// ReadUsersetTuples see [storage.RelationshipTupleReader].ReadUsersetTuples. -func (m *SQLite) ReadUsersetTuples( - ctx context.Context, - store string, - filter storage.ReadUsersetTuplesFilter, -) (storage.TupleIterator, error) { - ctx, span := tracer.Start(ctx, "sqlite.ReadUsersetTuples") - defer span.End() - - sb := m.stbl. - Select( - "store", "object_type", "object_id", "relation", "_user", - "condition_name", "condition_context", "ulid", "inserted_at", - ). - From("tuple"). - Where(sq.Eq{"store": store}). - Where(sq.Eq{"user_type": tupleUtils.UserSet}) - - objectType, objectID := tupleUtils.SplitObject(filter.Object) - if objectType != "" { - sb = sb.Where(sq.Eq{"object_type": objectType}) - } - if objectID != "" { - sb = sb.Where(sq.Eq{"object_id": objectID}) - } - if filter.Relation != "" { - sb = sb.Where(sq.Eq{"relation": filter.Relation}) - } - if len(filter.AllowedUserTypeRestrictions) > 0 { - orConditions := sq.Or{} - for _, userset := range filter.AllowedUserTypeRestrictions { - if _, ok := userset.GetRelationOrWildcard().(*openfgav1.RelationReference_Relation); ok { - orConditions = append(orConditions, sq.Like{"_user": userset.GetType() + ":%#" + userset.GetRelation()}) - } - if _, ok := userset.GetRelationOrWildcard().(*openfgav1.RelationReference_Wildcard); ok { - orConditions = append(orConditions, sq.Eq{"_user": userset.GetType() + ":*"}) - } - } - sb = sb.Where(orConditions) - } - rows, err := sb.QueryContext(ctx) - if err != nil { - return nil, handleSQLError(err) - } - - return sqlcommon.NewSQLTupleIterator(rows), nil -} - -// ReadStartingWithUser see [storage.RelationshipTupleReader].ReadStartingWithUser. -func (m *SQLite) ReadStartingWithUser( - ctx context.Context, - store string, - opts storage.ReadStartingWithUserFilter, -) (storage.TupleIterator, error) { - ctx, span := tracer.Start(ctx, "sqlite.ReadStartingWithUser") - defer span.End() - - targetUsersArg := make([]string, 0, len(opts.UserFilter)) - for _, u := range opts.UserFilter { - targetUser := u.GetObject() - if u.GetRelation() != "" { - targetUser = strings.Join([]string{u.GetObject(), u.GetRelation()}, "#") - } - targetUsersArg = append(targetUsersArg, targetUser) - } - - rows, err := m.stbl. - Select( - "store", "object_type", "object_id", "relation", "_user", - "condition_name", "condition_context", "ulid", "inserted_at", - ). - From("tuple"). - Where(sq.Eq{ - "store": store, - "object_type": opts.ObjectType, - "relation": opts.Relation, - "_user": targetUsersArg, - }).QueryContext(ctx) - if err != nil { - return nil, handleSQLError(err) - } - - return sqlcommon.NewSQLTupleIterator(rows), nil -} - -// MaxTuplesPerWrite see [storage.RelationshipTupleWriter].MaxTuplesPerWrite. -func (m *SQLite) MaxTuplesPerWrite() int { - return m.cfg.MaxTuplesPerWriteField -} - -// ReadAuthorizationModel see [storage.AuthorizationModelReadBackend].ReadAuthorizationModel. -func (m *SQLite) ReadAuthorizationModel(ctx context.Context, store string, modelID string) (*openfgav1.AuthorizationModel, error) { - ctx, span := tracer.Start(ctx, "sqlite.ReadAuthorizationModel") - defer span.End() - - return sqlcommon.ReadAuthorizationModel(ctx, m.dbInfo, store, modelID) -} - -// ReadAuthorizationModels see [storage.AuthorizationModelReadBackend].ReadAuthorizationModels. -func (m *SQLite) ReadAuthorizationModels( - ctx context.Context, - store string, - opts storage.PaginationOptions, -) ([]*openfgav1.AuthorizationModel, []byte, error) { - ctx, span := tracer.Start(ctx, "sqlite.ReadAuthorizationModels") - defer span.End() - - sb := m.stbl.Select("authorization_model_id"). - Distinct(). - From("authorization_model"). - Where(sq.Eq{"store": store}). - OrderBy("authorization_model_id desc") - - if opts.From != "" { - token, err := sqlcommon.UnmarshallContToken(opts.From) - if err != nil { - return nil, nil, err - } - sb = sb.Where(sq.LtOrEq{"authorization_model_id": token.Ulid}) - } - if opts.PageSize > 0 { - sb = sb.Limit(uint64(opts.PageSize + 1)) // + 1 is used to determine whether to return a continuation token. - } - - rows, err := sb.QueryContext(ctx) - if err != nil { - return nil, nil, handleSQLError(err) - } - defer func() { _ = rows.Close() }() - - var modelIDs []string - var modelID string - - for rows.Next() { - err = rows.Scan(&modelID) - if err != nil { - return nil, nil, handleSQLError(err) - } - - modelIDs = append(modelIDs, modelID) - } - - if err := rows.Err(); err != nil { - return nil, nil, handleSQLError(err) - } - - var token []byte - numModelIDs := len(modelIDs) - if len(modelIDs) > opts.PageSize { - numModelIDs = opts.PageSize - token, err = json.Marshal(sqlcommon.NewContToken(modelID, "")) - if err != nil { - return nil, nil, err - } - } - - // TODO: make this concurrent with a maximum of 5 goroutines. This may be helpful: - // https://stackoverflow.com/questions/25306073/always-have-x-number-of-goroutines-running-at-any-time - models := make([]*openfgav1.AuthorizationModel, 0, numModelIDs) - // We use numModelIDs here to avoid retrieving possibly one extra model. - for i := 0; i < numModelIDs; i++ { - model, err := m.ReadAuthorizationModel(ctx, store, modelIDs[i]) - if err != nil { - return nil, nil, err - } - models = append(models, model) - } - - return models, token, nil -} - -// FindLatestAuthorizationModel see [storage.AuthorizationModelReadBackend].FindLatestAuthorizationModel. -func (m *SQLite) FindLatestAuthorizationModel(ctx context.Context, store string) (*openfgav1.AuthorizationModel, error) { - ctx, span := tracer.Start(ctx, "sqlite.FindLatestAuthorizationModel") - defer span.End() - - return sqlcommon.FindLatestAuthorizationModel(ctx, m.dbInfo, store) -} - -// MaxTypesPerAuthorizationModel see [storage.TypeDefinitionWriteBackend].MaxTypesPerAuthorizationModel. -func (m *SQLite) MaxTypesPerAuthorizationModel() int { - return m.cfg.MaxTypesPerModelField -} - -// WriteAuthorizationModel see [storage.TypeDefinitionWriteBackend].WriteAuthorizationModel. -func (m *SQLite) WriteAuthorizationModel(ctx context.Context, store string, model *openfgav1.AuthorizationModel) error { - ctx, span := tracer.Start(ctx, "sqlite.WriteAuthorizationModel") - defer span.End() - - typeDefinitions := model.GetTypeDefinitions() - - if len(typeDefinitions) > m.MaxTypesPerAuthorizationModel() { - return storage.ExceededMaxTypeDefinitionsLimitError(m.MaxTypesPerAuthorizationModel()) - } - - return m.busyRetry(func() error { - return sqlcommon.WriteAuthorizationModel(ctx, m.dbInfo, store, model) - }) -} - -// CreateStore adds a new store to the SQLite storage. -func (m *SQLite) CreateStore(ctx context.Context, store *openfgav1.Store) (*openfgav1.Store, error) { - ctx, span := tracer.Start(ctx, "sqlite.CreateStore") - defer span.End() - - txn, err := m.db.BeginTx(ctx, &sql.TxOptions{}) - if err != nil { - return nil, handleSQLError(err) - } - defer func() { - _ = txn.Rollback() - }() - - _, err = m.stbl. - Insert("store"). - Columns("id", "name", "created_at", "updated_at"). - Values(store.GetId(), store.GetName(), sq.Expr("datetime('subsec')"), sq.Expr("datetime('subsec')")). - RunWith(txn). - ExecContext(ctx) - if err != nil { - return nil, handleSQLError(err) - } - - var createdAt time.Time - var id, name string - err = m.stbl. - Select("id", "name", "created_at"). - From("store"). - Where(sq.Eq{"id": store.GetId()}). - RunWith(txn). - QueryRowContext(ctx). - Scan(&id, &name, &createdAt) - if err != nil { - return nil, handleSQLError(err) - } - - err = txn.Commit() - if err != nil { - return nil, handleSQLError(err) - } - - return &openfgav1.Store{ - Id: id, - Name: name, - CreatedAt: timestamppb.New(createdAt), - UpdatedAt: timestamppb.New(createdAt), - }, nil -} - -// GetStore retrieves the details of a specific store from the SQLite using its storeID. -func (m *SQLite) GetStore(ctx context.Context, id string) (*openfgav1.Store, error) { - ctx, span := tracer.Start(ctx, "sqlite.GetStore") - defer span.End() - - row := m.stbl. - Select("id", "name", "created_at", "updated_at"). - From("store"). - Where(sq.Eq{ - "id": id, - "deleted_at": nil, - }). - QueryRowContext(ctx) - - var storeID, name string - var createdAt, updatedAt time.Time - err := row.Scan(&storeID, &name, &createdAt, &updatedAt) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, storage.ErrNotFound - } - return nil, handleSQLError(err) - } - - return &openfgav1.Store{ - Id: storeID, - Name: name, - CreatedAt: timestamppb.New(createdAt), - UpdatedAt: timestamppb.New(updatedAt), - }, nil -} - -// ListStores provides a paginated list of all stores present in the SQLite storage. -func (m *SQLite) ListStores(ctx context.Context, opts storage.PaginationOptions) ([]*openfgav1.Store, []byte, error) { - ctx, span := tracer.Start(ctx, "sqlite.ListStores") - defer span.End() - - sb := m.stbl. - Select("id", "name", "created_at", "updated_at"). - From("store"). - Where(sq.Eq{"deleted_at": nil}). - OrderBy("id") - - if opts.From != "" { - token, err := sqlcommon.UnmarshallContToken(opts.From) - if err != nil { - return nil, nil, err - } - sb = sb.Where(sq.GtOrEq{"id": token.Ulid}) - } - if opts.PageSize > 0 { - sb = sb.Limit(uint64(opts.PageSize + 1)) // + 1 is used to determine whether to return a continuation token. - } - - rows, err := sb.QueryContext(ctx) - if err != nil { - return nil, nil, handleSQLError(err) - } - defer func() { _ = rows.Close() }() - - var stores []*openfgav1.Store - var id string - for rows.Next() { - var name string - var createdAt, updatedAt time.Time - err := rows.Scan(&id, &name, &createdAt, &updatedAt) - if err != nil { - return nil, nil, handleSQLError(err) - } - - stores = append(stores, &openfgav1.Store{ - Id: id, - Name: name, - CreatedAt: timestamppb.New(createdAt), - UpdatedAt: timestamppb.New(updatedAt), - }) - } - - if err := rows.Err(); err != nil { - return nil, nil, handleSQLError(err) - } - - if len(stores) > opts.PageSize { - contToken, err := json.Marshal(sqlcommon.NewContToken(id, "")) - if err != nil { - return nil, nil, err - } - return stores[:opts.PageSize], contToken, nil - } - - return stores, nil, nil -} - -// DeleteStore removes a store from the SQLite storage. -func (m *SQLite) DeleteStore(ctx context.Context, id string) error { - ctx, span := tracer.Start(ctx, "sqlite.DeleteStore") - defer span.End() - - _, err := m.stbl. - Update("store"). - Set("deleted_at", sq.Expr("datetime('subsec')")). - Where(sq.Eq{"id": id}). - ExecContext(ctx) - if err != nil { - return handleSQLError(err) - } - - return nil -} - -// WriteAssertions see [storage.AssertionsBackend].WriteAssertions. -func (m *SQLite) WriteAssertions(ctx context.Context, store, modelID string, assertions []*openfgav1.Assertion) error { - ctx, span := tracer.Start(ctx, "sqlite.WriteAssertions") - defer span.End() - - marshalledAssertions, err := proto.Marshal(&openfgav1.Assertions{Assertions: assertions}) - if err != nil { - return err - } - - return m.busyRetry(func() error { - _, err = m.stbl. - Insert("assertion"). - Columns("store", "authorization_model_id", "assertions"). - Values(store, modelID, marshalledAssertions). - Suffix("ON CONFLICT(store,authorization_model_id) DO UPDATE SET assertions = ?", marshalledAssertions). - ExecContext(ctx) - if err != nil { - return handleSQLError(err) - } - - return nil - }) -} - -// ReadAssertions see [storage.AssertionsBackend].ReadAssertions. -func (m *SQLite) ReadAssertions(ctx context.Context, store, modelID string) ([]*openfgav1.Assertion, error) { - ctx, span := tracer.Start(ctx, "sqlite.ReadAssertions") - defer span.End() - - var marshalledAssertions []byte - err := m.stbl. - Select("assertions"). - From("assertion"). - Where(sq.Eq{ - "store": store, - "authorization_model_id": modelID, - }). - QueryRowContext(ctx). - Scan(&marshalledAssertions) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return []*openfgav1.Assertion{}, nil - } - return nil, handleSQLError(err) - } - - var assertions openfgav1.Assertions - err = proto.Unmarshal(marshalledAssertions, &assertions) - if err != nil { - return nil, err - } - - return assertions.GetAssertions(), nil -} - -// ReadChanges see [storage.ChangelogBackend].ReadChanges. -func (m *SQLite) ReadChanges( - ctx context.Context, - store, objectTypeFilter string, - opts storage.PaginationOptions, - horizonOffset time.Duration, -) ([]*openfgav1.TupleChange, []byte, error) { - ctx, span := tracer.Start(ctx, "sqlite.ReadChanges") - defer span.End() - - sb := m.stbl. - Select( - "ulid", "object_type", "object_id", "relation", "_user", "operation", - "condition_name", "condition_context", "inserted_at", - ). - From("changelog"). - Where(sq.Eq{"store": store}). - Where(fmt.Sprintf("inserted_at <= datetime('subsec','-%f seconds')", horizonOffset.Seconds())). - OrderBy("ulid asc") - - if objectTypeFilter != "" { - sb = sb.Where(sq.Eq{"object_type": objectTypeFilter}) - } - if opts.From != "" { - token, err := sqlcommon.UnmarshallContToken(opts.From) - if err != nil { - return nil, nil, err - } - if token.ObjectType != objectTypeFilter { - return nil, nil, storage.ErrMismatchObjectType - } - - sb = sb.Where(sq.Gt{"ulid": token.Ulid}) // > as we always return a continuation token. - } - if opts.PageSize > 0 { - sb = sb.Limit(uint64(opts.PageSize)) // + 1 is NOT used here as we always return a continuation token. - } - - rows, err := sb.QueryContext(ctx) - if err != nil { - return nil, nil, handleSQLError(err) - } - defer func() { _ = rows.Close() }() - - var changes []*openfgav1.TupleChange - var ulid string - for rows.Next() { - var objectType, objectID, relation, user string - var operation int - var insertedAt time.Time - var conditionName sql.NullString - var conditionContext []byte - - err = rows.Scan( - &ulid, - &objectType, - &objectID, - &relation, - &user, - &operation, - &conditionName, - &conditionContext, - &insertedAt, - ) - if err != nil { - return nil, nil, handleSQLError(err) - } - - var conditionContextStruct structpb.Struct - if conditionName.String != "" { - if conditionContext != nil { - if err := proto.Unmarshal(conditionContext, &conditionContextStruct); err != nil { - return nil, nil, err - } - } - } - - tk := tupleUtils.NewTupleKeyWithCondition( - tupleUtils.BuildObject(objectType, objectID), - relation, - user, - conditionName.String, - &conditionContextStruct, - ) - - changes = append(changes, &openfgav1.TupleChange{ - TupleKey: tk, - Operation: openfgav1.TupleOperation(operation), - Timestamp: timestamppb.New(insertedAt.UTC()), - }) - } - - if len(changes) == 0 { - return nil, nil, storage.ErrNotFound - } - - contToken, err := json.Marshal(sqlcommon.NewContToken(ulid, objectTypeFilter)) - if err != nil { - return nil, nil, err - } - - return changes, contToken, nil -} - -func (m *SQLite) IsReady(ctx context.Context) (storage.ReadinessStatus, error) { - if err := m.db.PingContext(ctx); err != nil { - return storage.ReadinessStatus{}, err - } - return storage.ReadinessStatus{ - IsReady: true, - }, nil -} - -// SQLite will return an SQLITE_BUSY error when the database is locked rather than waiting for the lock. -// This function retries the operation up to 5 times before returning the error. -func (m *SQLite) busyRetry(fn func() error) error { - for retries := 0; ; retries++ { - err := fn() - if err == nil || retries == m.cfg.QueryRetries { - return err - } - - var sqliteErr *sqlite3.Error - if errors.As(err, &sqliteErr) && (sqliteErr.Code == sqlite3.ErrLocked || sqliteErr.Code == sqlite3.ErrBusy) { - time.Sleep(10 * time.Millisecond) - continue - } - - return err - } -} - -func handleSQLError(err error, args ...any) error { - if strings.Contains(err.Error(), "UNIQUE constraint failed:") { - if len(args) > 0 { - if tk, ok := args[0].(*openfgav1.TupleKey); ok { - return storage.InvalidWriteInputError(tk, openfgav1.TupleOperation_TUPLE_OPERATION_WRITE) - } - } - return storage.ErrCollision - } - - return sqlcommon.HandleSQLError(err, args...) -} diff --git a/pkg/services/authz/zanzana/store/sqlite/store_test.go b/pkg/services/authz/zanzana/store/sqlite/store_test.go deleted file mode 100644 index 5388114d188..00000000000 --- a/pkg/services/authz/zanzana/store/sqlite/store_test.go +++ /dev/null @@ -1,295 +0,0 @@ -package sqlite - -import ( - "context" - "database/sql" - "testing" - "time" - - sq "github.com/Masterminds/squirrel" - "github.com/oklog/ulid/v2" - openfgav1 "github.com/openfga/api/proto/openfga/v1" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/openfga/openfga/pkg/storage" - "github.com/openfga/openfga/pkg/storage/sqlcommon" - "github.com/openfga/openfga/pkg/storage/test" - "github.com/openfga/openfga/pkg/tuple" - "github.com/openfga/openfga/pkg/typesystem" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/tests/testsuite" - - zassets "github.com/grafana/grafana/pkg/services/authz/zanzana/store/assets" - "github.com/grafana/grafana/pkg/services/authz/zanzana/store/migration" -) - -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -// TestIntegrationDatastore runs open fga default datastore test suite -func TestIntegrationDatastore(t *testing.T) { - db := sqliteIntegrationTest(t) - ds, err := NewWithDB(db, NewConfig()) - require.NoError(t, err) - test.RunAllTests(t, ds) -} - -// TestIntegrationReadEnsureNoOrder asserts that the read response is not ordered by ulid. -func TestIntegrationReadEnsureNoOrder(t *testing.T) { - db := sqliteIntegrationTest(t) - - ds, err := NewWithDB(db, NewConfig()) - require.NoError(t, err) - - ctx := context.Background() - store := "store" - firstTuple := tuple.NewTupleKey("doc:object_id_1", "relation", "user:user_1") - secondTuple := tuple.NewTupleKey("doc:object_id_2", "relation", "user:user_2") - - err = sqlcommon.Write(ctx, - sqlcommon.NewDBInfo(ds.db, ds.stbl, sq.Expr("datetime('subsec')")), - store, - []*openfgav1.TupleKeyWithoutCondition{}, - []*openfgav1.TupleKey{firstTuple}, - time.Now()) - require.NoError(t, err) - - // Tweak time so that ULID is smaller. - err = sqlcommon.Write(ctx, - sqlcommon.NewDBInfo(ds.db, ds.stbl, sq.Expr("datetime('subsec')")), - store, - []*openfgav1.TupleKeyWithoutCondition{}, - []*openfgav1.TupleKey{secondTuple}, - time.Now().Add(time.Minute*-1)) - require.NoError(t, err) - - iter, err := ds.Read(ctx, - store, - tuple.NewTupleKey("doc:", "relation", "")) - defer iter.Stop() - - require.NoError(t, err) - - // We expect that objectID1 will return first because it is inserted first. - curTuple, err := iter.Next(ctx) - require.NoError(t, err) - require.Equal(t, firstTuple, curTuple.GetKey()) - - curTuple, err = iter.Next(ctx) - require.NoError(t, err) - require.Equal(t, secondTuple, curTuple.GetKey()) -} - -// TestIntegrationReadPageEnsureNoOrder asserts that the read page is ordered by ulid. -func TestIntegrationReadPageEnsureOrder(t *testing.T) { - db := sqliteIntegrationTest(t) - - ds, err := NewWithDB(db, NewConfig()) - require.NoError(t, err) - - ctx := context.Background() - - store := "store" - firstTuple := tuple.NewTupleKey("doc:object_id_1", "relation", "user:user_1") - secondTuple := tuple.NewTupleKey("doc:object_id_2", "relation", "user:user_2") - - err = sqlcommon.Write(ctx, - sqlcommon.NewDBInfo(ds.db, ds.stbl, sq.Expr("datetime('subsec')")), - store, - []*openfgav1.TupleKeyWithoutCondition{}, - []*openfgav1.TupleKey{firstTuple}, - time.Now()) - require.NoError(t, err) - - // Tweak time so that ULID is smaller. - err = sqlcommon.Write(ctx, - sqlcommon.NewDBInfo(ds.db, ds.stbl, sq.Expr("datetime('subsec')")), - store, - []*openfgav1.TupleKeyWithoutCondition{}, - []*openfgav1.TupleKey{secondTuple}, - time.Now().Add(time.Minute*-1)) - require.NoError(t, err) - - tuples, _, err := ds.ReadPage(ctx, - store, - tuple.NewTupleKey("doc:", "relation", ""), - storage.NewPaginationOptions(0, "")) - require.NoError(t, err) - - require.Len(t, tuples, 2) - // We expect that objectID2 will return first because it has a smaller ulid. - require.Equal(t, secondTuple, tuples[0].GetKey()) - require.Equal(t, firstTuple, tuples[1].GetKey()) -} - -func TestIntegrationReadAuthorizationModelUnmarshallError(t *testing.T) { - db := sqliteIntegrationTest(t) - - ds, err := NewWithDB(db, NewConfig()) - require.NoError(t, err) - - ctx := context.Background() - store := "store" - modelID := "foo" - schemaVersion := typesystem.SchemaVersion1_0 - - bytes, err := proto.Marshal(&openfgav1.TypeDefinition{Type: "document"}) - require.NoError(t, err) - pbdata := []byte{0x01, 0x02, 0x03} - - _, err = ds.db.ExecContext(ctx, "INSERT INTO authorization_model (store, authorization_model_id, schema_version, type, type_definition, serialized_protobuf) VALUES (?, ?, ?, ?, ?, ?)", store, modelID, schemaVersion, "document", bytes, pbdata) - require.NoError(t, err) - - _, err = ds.ReadAuthorizationModel(ctx, store, modelID) - require.Error(t, err) - require.Contains(t, err.Error(), "cannot parse invalid wire-format data") -} - -// TestIntegrationAllowNullCondition tests that tuple and changelog rows existing before -// migration 005_add_conditions_to_tuples can be successfully read. -func TestIntegrationAllowNullCondition(t *testing.T) { - db := sqliteIntegrationTest(t) - - ds, err := NewWithDB(db, NewConfig()) - require.NoError(t, err) - - ctx := context.Background() - - stmt := ` - INSERT INTO tuple ( - store, object_type, object_id, relation, _user, user_type, ulid, - condition_name, condition_context, inserted_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('subsec')); - ` - _, err = ds.db.ExecContext( - ctx, stmt, "store", "folder", "2021-budget", "owner", "user:anne", "user", - ulid.Make().String(), nil, nil, - ) - require.NoError(t, err) - - tk := tuple.NewTupleKey("folder:2021-budget", "owner", "user:anne") - iter, err := ds.Read(ctx, "store", tk) - require.NoError(t, err) - defer iter.Stop() - - curTuple, err := iter.Next(ctx) - require.NoError(t, err) - require.Equal(t, tk, curTuple.GetKey()) - - tuples, _, err := ds.ReadPage(ctx, "store", &openfgav1.TupleKey{}, storage.PaginationOptions{ - PageSize: 2, - }) - require.NoError(t, err) - require.Len(t, tuples, 1) - require.Equal(t, tk, tuples[0].GetKey()) - - userTuple, err := ds.ReadUserTuple(ctx, "store", tk) - require.NoError(t, err) - require.Equal(t, tk, userTuple.GetKey()) - - tk2 := tuple.NewTupleKey("folder:2022-budget", "viewer", "user:anne") - _, err = ds.db.ExecContext( - ctx, stmt, "store", "folder", "2022-budget", "viewer", "user:anne", "userset", - ulid.Make().String(), nil, nil, - ) - - require.NoError(t, err) - iter, err = ds.ReadUsersetTuples(ctx, "store", storage.ReadUsersetTuplesFilter{Object: "folder:2022-budget"}) - require.NoError(t, err) - defer iter.Stop() - - curTuple, err = iter.Next(ctx) - require.NoError(t, err) - require.Equal(t, tk2, curTuple.GetKey()) - - iter, err = ds.ReadStartingWithUser(ctx, "store", storage.ReadStartingWithUserFilter{ - ObjectType: "folder", - Relation: "owner", - UserFilter: []*openfgav1.ObjectRelation{ - {Object: "user:anne"}, - }, - }) - require.NoError(t, err) - defer iter.Stop() - - curTuple, err = iter.Next(ctx) - require.NoError(t, err) - require.Equal(t, tk, curTuple.GetKey()) - - stmt = ` - INSERT INTO changelog ( - store, object_type, object_id, relation, _user, ulid, - condition_name, condition_context, inserted_at, operation - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('subsec'), ?); -` - _, err = ds.db.ExecContext( - ctx, stmt, "store", "folder", "2021-budget", "owner", "user:anne", - ulid.Make().String(), nil, nil, openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, - ) - require.NoError(t, err) - - _, err = ds.db.ExecContext( - ctx, stmt, "store", "folder", "2021-budget", "owner", "user:anne", - ulid.Make().String(), nil, nil, openfgav1.TupleOperation_TUPLE_OPERATION_DELETE, - ) - require.NoError(t, err) - - changes, _, err := ds.ReadChanges(ctx, "store", "folder", storage.PaginationOptions{}, 0) - require.NoError(t, err) - require.Len(t, changes, 2) - require.Equal(t, tk, changes[0].GetTupleKey()) - require.Equal(t, tk, changes[1].GetTupleKey()) -} - -// TestIntegrationMarshalledAssertions tests that previously persisted marshalled -// assertions can be read back. In any case where the Assertions proto model -// needs to change, we'll likely need to introduce a series of data migrations. -func TestIntegrationMarshalledAssertions(t *testing.T) { - db := sqliteIntegrationTest(t) - - ds, err := NewWithDB(db, NewConfig()) - require.NoError(t, err) - - ctx := context.Background() - // Note: this represents an assertion written on v1.3.7. - stmt := ` - INSERT INTO assertion ( - store, authorization_model_id, assertions - ) VALUES (?, ?, UNHEX('0A2B0A270A12666F6C6465723A323032312D62756467657412056F776E65721A0A757365723A616E6E657A1001')); - ` - _, err = ds.db.ExecContext(ctx, stmt, "store", "model") - require.NoError(t, err) - - assertions, err := ds.ReadAssertions(ctx, "store", "model") - require.NoError(t, err) - - expectedAssertions := []*openfgav1.Assertion{ - { - TupleKey: &openfgav1.AssertionTupleKey{ - Object: "folder:2021-budget", - Relation: "owner", - User: "user:annez", - }, - Expectation: true, - }, - } - require.Equal(t, expectedAssertions, assertions) -} - -func sqliteIntegrationTest(tb testing.TB) *sql.DB { - if testing.Short() || !db.IsTestDbSQLite() { - tb.Skip("skipping integration test") - } - - db, cfg := db.InitTestDBWithCfg(tb) - - m := migrator.NewMigrator(db.GetEngine(), cfg) - err := migration.RunWithMigrator(m, cfg, zassets.EmbedMigrations, zassets.SQLiteMigrationDir) - require.NoError(tb, err) - - return db.GetEngine().DB().DB -} diff --git a/pkg/services/authz/zanzana/store/sqlite/write.go b/pkg/services/authz/zanzana/store/sqlite/write.go deleted file mode 100644 index 4e5dd79610b..00000000000 --- a/pkg/services/authz/zanzana/store/sqlite/write.go +++ /dev/null @@ -1,165 +0,0 @@ -package sqlite - -import ( - "context" - "database/sql" - "time" - - sq "github.com/Masterminds/squirrel" - "github.com/oklog/ulid/v2" - "google.golang.org/protobuf/proto" - - openfgav1 "github.com/openfga/api/proto/openfga/v1" - "github.com/openfga/openfga/pkg/storage" - tupleUtils "github.com/openfga/openfga/pkg/tuple" -) - -// write is copied from https://github.com/openfga/openfga/blob/main/pkg/storage/sqlcommon/sqlcommon.go#L330-L456 -// but uses custom handleSQLError. -func write( - ctx context.Context, - db *sql.DB, - stbl sq.StatementBuilderType, - sqlTime any, - store string, - deletes storage.Deletes, - writes storage.Writes, - now time.Time, -) error { - txn, err := db.BeginTx(ctx, nil) - if err != nil { - return handleSQLError(err) - } - defer func() { - _ = txn.Rollback() - }() - - changelogBuilder := stbl. - Insert("changelog"). - Columns( - "store", "object_type", "object_id", "relation", "_user", - "condition_name", "condition_context", "operation", "ulid", "inserted_at", - ) - - deleteBuilder := stbl.Delete("tuple") - - for _, tk := range deletes { - id := ulid.MustNew(ulid.Timestamp(now), ulid.DefaultEntropy()).String() - objectType, objectID := tupleUtils.SplitObject(tk.GetObject()) - - res, err := deleteBuilder. - Where(sq.Eq{ - "store": store, - "object_type": objectType, - "object_id": objectID, - "relation": tk.GetRelation(), - "_user": tk.GetUser(), - "user_type": tupleUtils.GetUserTypeFromUser(tk.GetUser()), - }). - RunWith(txn). // Part of a txn. - ExecContext(ctx) - if err != nil { - return handleSQLError(err, tk) - } - - rowsAffected, err := res.RowsAffected() - if err != nil { - return handleSQLError(err) - } - - if rowsAffected != 1 { - return storage.InvalidWriteInputError( - tk, - openfgav1.TupleOperation_TUPLE_OPERATION_DELETE, - ) - } - - changelogBuilder = changelogBuilder.Values( - store, objectType, objectID, - tk.GetRelation(), tk.GetUser(), - "", nil, // Redact condition info for deletes since we only need the base triplet (object, relation, user). - openfgav1.TupleOperation_TUPLE_OPERATION_DELETE, - id, sqlTime, - ) - } - - insertBuilder := stbl. - Insert("tuple"). - Columns( - "store", "object_type", "object_id", "relation", "_user", "user_type", - "condition_name", "condition_context", "ulid", "inserted_at", - ) - - for _, tk := range writes { - id := ulid.MustNew(ulid.Timestamp(now), ulid.DefaultEntropy()).String() - objectType, objectID := tupleUtils.SplitObject(tk.GetObject()) - - conditionName, conditionContext, err := marshalRelationshipCondition(tk.GetCondition()) - if err != nil { - return err - } - - _, err = insertBuilder. - Values( - store, - objectType, - objectID, - tk.GetRelation(), - tk.GetUser(), - tupleUtils.GetUserTypeFromUser(tk.GetUser()), - conditionName, - conditionContext, - id, - sqlTime, - ). - RunWith(txn). // Part of a txn. - ExecContext(ctx) - if err != nil { - return handleSQLError(err, tk) - } - - changelogBuilder = changelogBuilder.Values( - store, - objectType, - objectID, - tk.GetRelation(), - tk.GetUser(), - conditionName, - conditionContext, - openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, - id, - sqlTime, - ) - } - - if len(writes) > 0 || len(deletes) > 0 { - _, err := changelogBuilder.RunWith(txn).ExecContext(ctx) // Part of a txn. - if err != nil { - return handleSQLError(err) - } - } - - if err := txn.Commit(); err != nil { - return handleSQLError(err) - } - - return nil -} - -// copied from https://github.com/openfga/openfga/blob/main/pkg/storage/sqlcommon/encoding.go#L8-L24 -func marshalRelationshipCondition( - rel *openfgav1.RelationshipCondition, -) (name string, context []byte, err error) { - if rel != nil { - if rel.GetContext() != nil && len(rel.GetContext().GetFields()) > 0 { - context, err = proto.Marshal(rel.GetContext()) - if err != nil { - return name, context, err - } - } - - return rel.GetName(), context, err - } - - return name, context, err -} diff --git a/pkg/services/authz/zanzana/store/store.go b/pkg/services/authz/zanzana/store/store.go index fe7aadc0cee..360e75c2dd4 100644 --- a/pkg/services/authz/zanzana/store/store.go +++ b/pkg/services/authz/zanzana/store/store.go @@ -2,15 +2,15 @@ package store import ( "fmt" + "strings" "time" - "xorm.io/xorm" - "github.com/openfga/openfga/assets" "github.com/openfga/openfga/pkg/storage" "github.com/openfga/openfga/pkg/storage/mysql" "github.com/openfga/openfga/pkg/storage/postgres" "github.com/openfga/openfga/pkg/storage/sqlcommon" + "github.com/openfga/openfga/pkg/storage/sqlite" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -19,9 +19,7 @@ import ( "github.com/grafana/grafana/pkg/setting" zlogger "github.com/grafana/grafana/pkg/services/authz/zanzana/logger" - zassets "github.com/grafana/grafana/pkg/services/authz/zanzana/store/assets" "github.com/grafana/grafana/pkg/services/authz/zanzana/store/migration" - "github.com/grafana/grafana/pkg/services/authz/zanzana/store/sqlite" ) func NewStore(cfg *setting.Cfg, logger log.Logger) (storage.OpenFGADatastore, error) { @@ -32,22 +30,12 @@ func NewStore(cfg *setting.Cfg, logger log.Logger) (storage.OpenFGADatastore, er switch grafanaDBCfg.Type { case migrator.SQLite: - connStr := grafanaDBCfg.ConnectionString - // Initilize connection using xorm engine so we can reuse it for both migrations and data store - engine, err := xorm.NewEngine(grafanaDBCfg.Type, connStr) - if err != nil { - return nil, fmt.Errorf("failed to connect to database: %w", err) - } - - m := migrator.NewMigrator(engine, cfg) - if err := migration.RunWithMigrator(m, cfg, zassets.EmbedMigrations, zassets.SQLiteMigrationDir); err != nil { + connStr := sqliteConnectionString(grafanaDBCfg.ConnectionString) + if err := migration.Run(cfg, migrator.SQLite, connStr, assets.EmbedMigrations, assets.SqliteMigrationDir); err != nil { return nil, fmt.Errorf("failed to run migrations: %w", err) } - return sqlite.NewWithDB(engine.DB().DB, &sqlite.Config{ - Config: zanzanaDBCfg, - QueryRetries: grafanaDBCfg.QueryRetries, - }) + return sqlite.New(connStr, zanzanaDBCfg) case migrator.MySQL: // For mysql we need to pass parseTime parameter in connection string connStr := grafanaDBCfg.ConnectionString + "&parseTime=true" @@ -75,20 +63,16 @@ func NewEmbeddedStore(cfg *setting.Cfg, db db.DB, logger log.Logger) (storage.Op return nil, fmt.Errorf("failed to parse database config: %w", err) } - m := migrator.NewMigrator(db.GetEngine(), cfg) - switch grafanaDBCfg.Type { case migrator.SQLite: - if err := migration.RunWithMigrator(m, cfg, zassets.EmbedMigrations, zassets.SQLiteMigrationDir); err != nil { + grafanaDBCfg.ConnectionString = sqliteConnectionString(grafanaDBCfg.ConnectionString) + if err := migration.Run(cfg, migrator.SQLite, grafanaDBCfg.ConnectionString, assets.EmbedMigrations, assets.SqliteMigrationDir); err != nil { return nil, fmt.Errorf("failed to run migrations: %w", err) } - // FIXME(kalleep): We should work on getting sqlite implemtation merged upstream and replace this one - return sqlite.NewWithDB(db.GetEngine().DB().DB, &sqlite.Config{ - Config: zanzanaDBCfg, - QueryRetries: grafanaDBCfg.QueryRetries, - }) + return sqlite.New(grafanaDBCfg.ConnectionString, zanzanaDBCfg) case migrator.MySQL: + m := migrator.NewMigrator(db.GetEngine(), cfg) if err := migration.RunWithMigrator(m, cfg, assets.EmbedMigrations, assets.MySQLMigrationDir); err != nil { return nil, fmt.Errorf("failed to run migrations: %w", err) } @@ -96,6 +80,7 @@ func NewEmbeddedStore(cfg *setting.Cfg, db db.DB, logger log.Logger) (storage.Op // For mysql we need to pass parseTime parameter in connection string return mysql.New(grafanaDBCfg.ConnectionString+"&parseTime=true", zanzanaDBCfg) case migrator.Postgres: + m := migrator.NewMigrator(db.GetEngine(), cfg) if err := migration.RunWithMigrator(m, cfg, assets.EmbedMigrations, assets.PostgresMigrationDir); err != nil { return nil, fmt.Errorf("failed to run migrations: %w", err) } @@ -126,3 +111,8 @@ func parseConfig(cfg *setting.Cfg, logger log.Logger) (*sqlstore.DatabaseConfig, return grafanaDBCfg, zanzanaDBCfg, nil } + +func sqliteConnectionString(v string) string { + // hardcode zanzana.db for now + return v[0:strings.LastIndex(v, "/")+1] + "zanzana.db" +} diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index 03bfe9f36ed..ece56e98179 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -118,3 +118,8 @@ func TranslateFixedRole(role string) string { role = strings.ReplaceAll(role, ".", "_") return role } + +// Translate "read" for the dashboard into "dashboard_read" for folder +func TranslateToFolderRelation(relation, objectType string) string { + return fmt.Sprintf("%s_%s", objectType, relation) +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 74944e407fe..d087684946b 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -782,7 +782,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi fakeAccessControlService := actest.FakeService{} alertMetrics := metrics.NewNGAlert(prometheus.NewRegistry()) - ruleStore, err := ngalertstore.ProvideDBStore(cfg, featureToggles, sqlStore, mockFolder, dashboardService, fakeAccessControl) + ruleStore, err := ngalertstore.ProvideDBStore(cfg, featureToggles, sqlStore, mockFolder, dashboardService, fakeAccessControl, bus) require.NoError(t, err) ng, err := ngalert.ProvideService( diff --git a/pkg/services/dashboards/service/zanzana.go b/pkg/services/dashboards/service/zanzana.go index f7bfb4d23f6..7441df302cf 100644 --- a/pkg/services/dashboards/service/zanzana.go +++ b/pkg/services/dashboards/service/zanzana.go @@ -10,10 +10,11 @@ import ( "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" ) const ( @@ -193,9 +194,16 @@ func (dr *DashboardServiceImpl) checkDashboards(ctx context.Context, query dashb } req := accesscontrol.CheckRequest{ - User: query.SignedInUser.GetUID(), - Relation: "read", - Object: zanzana.NewScopedTupleEntry(objectType, d.UID, "", strconv.FormatInt(orgId, 10)), + Namespace: claims.OrgNamespaceFormatter(orgId), + User: query.SignedInUser.GetUID(), + Relation: "read", + Object: zanzana.NewScopedTupleEntry(objectType, d.UID, "", strconv.FormatInt(orgId, 10)), + } + + if objectType != zanzana.TypeFolder { + // Pass parentn folder for the correct check + req.Parent = d.FolderUID + req.ObjectType = objectType } allowed, err := dr.ac.Check(ctx, req) @@ -238,45 +246,47 @@ func (dr *DashboardServiceImpl) findDashboardsZanzanaList(ctx context.Context, q ctx, span := tracer.Start(ctx, "dashboards.service.findDashboardsZanzanaList") defer span.End() - resourceUIDs, err := dr.listUserResources(ctx, query) - if err != nil { - return nil, err - } - if len(resourceUIDs) == 0 { - return []dashboards.DashboardSearchProjection{}, nil - } + var result []dashboards.DashboardSearchProjection - query.DashboardUIDs = resourceUIDs - query.SkipAccessControlFilter = true - return dr.dashboardStore.FindDashboards(ctx, &query) -} - -func (dr *DashboardServiceImpl) listUserResources(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]string, error) { - tasks := make([]func() ([]string, error), 0) - var resourceTypes []string - - // For some search types we need dashboards or folders only - switch query.Type { - case searchstore.TypeDashboard: - resourceTypes = []string{zanzana.TypeDashboard} - case searchstore.TypeFolder, searchstore.TypeAlertFolder: - resourceTypes = []string{zanzana.TypeFolder} - default: - resourceTypes = []string{zanzana.TypeDashboard, zanzana.TypeFolder} - } - - for _, resourceType := range resourceTypes { - tasks = append(tasks, func() ([]string, error) { - return dr.listAllowedResources(ctx, query, resourceType) - }) - } - - uids, err := runBatch(tasks) + allowedFolders, err := dr.listAllowedResources(ctx, query, zanzana.TypeFolder) if err != nil { return nil, err } - return uids, nil + if len(allowedFolders) > 0 { + // Find dashboards in folders that user has access to + query.SkipAccessControlFilter = true + query.FolderUIDs = allowedFolders + result, err = dr.dashboardStore.FindDashboards(ctx, &query) + if err != nil { + return nil, err + } + } + + // skip if limit reached + rest := query.Limit - int64(len(result)) + if rest <= 0 { + return result, nil + } + + // Run second query to find dashboards with direct permission assignments + allowedDashboards, err := dr.listAllowedResources(ctx, query, zanzana.TypeDashboard) + if err != nil { + return nil, err + } + + if len(allowedDashboards) > 0 { + query.FolderUIDs = []string{} + query.DashboardUIDs = allowedDashboards + query.Limit = rest + dashboardRes, err := dr.dashboardStore.FindDashboards(ctx, &query) + if err != nil { + return nil, err + } + result = append(result, dashboardRes...) + } + + return result, err } func (dr *DashboardServiceImpl) listAllowedResources(ctx context.Context, query dashboards.FindPersistedDashboardsQuery, resourceType string) ([]string, error) { @@ -307,36 +317,3 @@ func (dr *DashboardServiceImpl) listAllowedResources(ctx context.Context, query return resourceUIDs, nil } - -func runBatch(tasks []func() ([]string, error)) ([]string, error) { - var wg sync.WaitGroup - tasksNum := len(tasks) - resChan := make(chan []string, tasksNum) - errChan := make(chan error, tasksNum) - - for _, task := range tasks { - wg.Add(1) - go func() { - defer wg.Done() - res, err := task() - resChan <- res - errChan <- err - }() - } - - wg.Wait() - close(resChan) - close(errChan) - - for err := range errChan { - if err != nil { - return nil, err - } - } - - result := make([]string, 0) - for res := range resChan { - result = append(result, res...) - } - return result, nil -} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index ba68086a0af..9034ce2fb89 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1255,10 +1255,11 @@ var ( }, { Name: "dashboardRestore", - Description: "Enables deleted dashboard restore feature (backend only)", + Description: "Enables deleted dashboard restore feature", Stage: FeatureStageExperimental, Owner: grafanaSearchAndStorageSquad, HideFromAdminPage: true, + Expression: "false", // enabled by default }, { Name: "datasourceProxyDisableRBAC", @@ -1367,13 +1368,6 @@ var ( Owner: grafanaAlertingSquad, RequiresRestart: true, }, - { - Name: "dashboardRestoreUI", - Description: "Enables the frontend to be able to restore a recently deleted dashboard", - Stage: FeatureStageExperimental, - Owner: grafanaFrontendPlatformSquad, - Expression: "false", // enabled by default - }, { Name: "cloudWatchRoundUpEndTime", Description: "Round up end time for metric queries to the next minute to avoid missing data", @@ -1463,8 +1457,8 @@ var ( Owner: growthAndOnboarding, }, { - Name: "appPlatformAccessTokens", - Description: "Enables the use of access tokens for the App Platform", + Name: "appPlatformGrpcClientAuth", + Description: "Enables the gRPC client to authenticate with the App Platform by using ID & access tokens", Stage: FeatureStageExperimental, Owner: identityAccessTeam, HideFromDocs: true, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 27dc4bcdf2a..e65a21dd9b5 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -179,7 +179,6 @@ failWrongDSUID,experimental,@grafana/plugins-platform-backend,false,false,false zanzana,experimental,@grafana/identity-access-team,false,false,false passScopeToDashboardApi,experimental,@grafana/dashboards-squad,false,false,false alertingApiServer,experimental,@grafana/alerting-squad,false,true,false -dashboardRestoreUI,experimental,@grafana/grafana-frontend-platform,false,false,false cloudWatchRoundUpEndTime,GA,@grafana/aws-datasources,false,false,false cloudwatchMetricInsightsCrossAccount,preview,@grafana/aws-datasources,false,false,true prometheusAzureOverrideAudience,deprecated,@grafana/partner-datasources,false,false,false @@ -193,7 +192,7 @@ exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,t exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false,true homeSetupGuide,experimental,@grafana/growth-and-onboarding,false,false,true -appPlatformAccessTokens,experimental,@grafana/identity-access-team,false,false,false +appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false appSidecar,experimental,@grafana/explore-squad,false,false,false groupAttributeSync,experimental,@grafana/identity-access-team,false,false,false alertingQueryAndExpressionsStepMode,experimental,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 147d0f4681e..31fe284f57f 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -668,7 +668,7 @@ const ( FlagNotificationBanner = "notificationBanner" // FlagDashboardRestore - // Enables deleted dashboard restore feature (backend only) + // Enables deleted dashboard restore feature FlagDashboardRestore = "dashboardRestore" // FlagDatasourceProxyDisableRBAC @@ -727,10 +727,6 @@ const ( // Register Alerting APIs with the K8s API server FlagAlertingApiServer = "alertingApiServer" - // FlagDashboardRestoreUI - // Enables the frontend to be able to restore a recently deleted dashboard - FlagDashboardRestoreUI = "dashboardRestoreUI" - // FlagCloudWatchRoundUpEndTime // Round up end time for metric queries to the next minute to avoid missing data FlagCloudWatchRoundUpEndTime = "cloudWatchRoundUpEndTime" @@ -783,9 +779,9 @@ const ( // Used in Home for users who want to return to the onboarding flow or quickly find popular config pages FlagHomeSetupGuide = "homeSetupGuide" - // FlagAppPlatformAccessTokens - // Enables the use of access tokens for the App Platform - FlagAppPlatformAccessTokens = "appPlatformAccessTokens" + // FlagAppPlatformGrpcClientAuth + // Enables the gRPC client to authenticate with the App Platform by using ID & access tokens + FlagAppPlatformGrpcClientAuth = "appPlatformGrpcClientAuth" // FlagAppSidecar // Enable the app sidecar feature that allows rendering 2 apps at the same time diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index fbd7b6e335a..40e0388ab28 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -386,7 +386,8 @@ "metadata": { "name": "appPlatformAccessTokens", "resourceVersion": "1725549369316", - "creationTimestamp": "2024-09-05T15:16:09Z" + "creationTimestamp": "2024-09-05T15:16:09Z", + "deletionTimestamp": "2024-10-11T15:54:21Z" }, "spec": { "description": "Enables the use of access tokens for the App Platform", @@ -396,6 +397,20 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "appPlatformGrpcClientAuth", + "resourceVersion": "1728662061076", + "creationTimestamp": "2024-10-11T15:54:21Z" + }, + "spec": { + "description": "Enables the gRPC client to authenticate with the App Platform by using ID \u0026 access tokens", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "appSidecar", @@ -808,17 +823,18 @@ { "metadata": { "name": "dashboardRestore", - "resourceVersion": "1719321877748", + "resourceVersion": "1728397491294", "creationTimestamp": "2024-05-16T17:36:26Z", "annotations": { - "grafana.app/updatedTimestamp": "2024-06-25 13:24:37.748284 +0000 UTC" + "grafana.app/updatedTimestamp": "2024-10-08 14:24:51.294668 +0000 UTC" } }, "spec": { - "description": "Enables deleted dashboard restore feature (backend only)", + "description": "Enables deleted dashboard restore feature", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true + "hideFromAdminPage": true, + "expression": "false" } }, { @@ -826,6 +842,7 @@ "name": "dashboardRestoreUI", "resourceVersion": "1720021873452", "creationTimestamp": "2024-06-25T14:43:13Z", + "deletionTimestamp": "2024-10-08T14:24:51Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -3062,6 +3079,21 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "unifiedStorageSearch", + "resourceVersion": "1726771421439", + "creationTimestamp": "2024-09-19T18:43:41Z", + "deletionTimestamp": "2024-10-11T14:56:04Z" + }, + "spec": { + "description": "Enable unified storage search", + "stage": "experimental", + "codeowner": "@grafana/search-and-storage", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "useSeessionStorageForRedirection", diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 36f21e37170..a1608f85e71 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -260,6 +260,7 @@ func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Fo if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { dashFolder.Fullpath = dashFolder.Title + dashFolder.FullpathUIDs = dashFolder.UID return dashFolder, nil } @@ -282,7 +283,8 @@ func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Fo f.Version = dashFolder.Version if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { - f.Fullpath = f.Title // set full path to the folder title (unescaped) + f.Fullpath = f.Title // set full path to the folder title (unescaped) + f.FullpathUIDs = f.UID // set full path to the folder UID } return f, err @@ -671,6 +673,30 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( return nil, err } + if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesFolders) { + if f.ParentUID == "" { + return f, nil + } + + // Fetch the parent since the permissions for fetching the newly created folder + // are not yet present for the user--this requires a call to ClearUserPermissionCache + parent, err := s.Get(ctx, &folder.GetFolderQuery{ + UID: &f.ParentUID, + OrgID: f.OrgID, + WithFullpath: true, + WithFullpathUIDs: true, + SignedInUser: user, + }) + if err != nil { + return nil, err + } + // #TODO revisit setting permissions so that we can centralise the logic for escaping slashes in titles + // Escape forward slashes in the title + title := strings.Replace(f.Title, "/", "\\/", -1) + f.Fullpath = title + "/" + parent.Fullpath + f.FullpathUIDs = f.UID + "/" + parent.FullpathUIDs + } + return f, nil } diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 283dcb668c1..32bf156e55c 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -494,14 +494,14 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil) require.NoError(t, err) - alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac) + alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac, b) require.NoError(t, err) elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, serviceWithFlagOn.store, featuresFlagOn, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn) require.NoError(t, err) - ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd) + ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd, true) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID) require.NoError(t, err) @@ -577,14 +577,14 @@ func TestIntegrationNestedFolderService(t *testing.T) { folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil) require.NoError(t, err) - alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac) + alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b) require.NoError(t, err) elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, serviceWithFlagOff.store, featuresFlagOff, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff) require.NoError(t, err) - ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd) + ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd, true) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID) require.NoError(t, err) @@ -722,10 +722,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil) require.NoError(t, err) - alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac) + alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac, b) require.NoError(t, err) - ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd) + ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd, true) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID) require.NoError(t, err) @@ -1536,8 +1536,8 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { t.Run("Should get folders shared with given user", func(t *testing.T) { depth := 3 - ancestorFoldersWithPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withPermissions", createCmd) - ancestorFoldersWithoutPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withoutPermissions", createCmd) + ancestorFoldersWithPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withPermissions", createCmd, true) + ancestorFoldersWithoutPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withoutPermissions", createCmd, true) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorFoldersWithoutPermissions[0].UID) require.NoError(t, err) @@ -1661,8 +1661,8 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { // tree2-folder-0 // └──tree2-folder-1 // └──tree2-folder-2 - tree1 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree1-", createCmd) - tree2 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree2-", createCmd) + tree1 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree1-", createCmd, true) + tree2 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree2-", createCmd, true) signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = []string{ // Add permission to tree1-folder-0 @@ -1928,14 +1928,16 @@ func TestFolderServiceGetFolder(t *testing.T) { } depth := 3 - folders := CreateSubtreeInStore(t, folderSvcOn.store, &folderSvcOn, depth, "get/folder-", createCmd) + folders := CreateSubtreeInStore(t, folderSvcOn.store, &folderSvcOn, depth, "get/folder-", createCmd, false) f := folders[1] testCases := []struct { - name string - svc *Service - WithFullpath bool - expectedFullpath string + name string + svc *Service + WithFullpath bool + WithFullpathUIDs bool + expectedFullpath string + expectedFullpathUIDs string }{ { name: "when flag is off", @@ -1954,6 +1956,18 @@ func TestFolderServiceGetFolder(t *testing.T) { WithFullpath: true, expectedFullpath: "get\\/folder-folder-0/get\\/folder-folder-1", }, + { + name: "when flag is on and WithFullpathUIDs is false", + svc: &folderSvcOn, + WithFullpathUIDs: false, + expectedFullpathUIDs: "", + }, + { + name: "when flag is on and WithFullpathUIDs is true", + svc: &folderSvcOn, + WithFullpathUIDs: true, + expectedFullpathUIDs: "uidfor-0/uidfor-1", + }, } for _, tc := range testCases { @@ -2021,7 +2035,7 @@ func TestFolderServiceGetFolders(t *testing.T) { }) prefix := "getfolders/ff/off" - folders := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOff, 5, prefix, createCmd) + folders := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOff, 5, prefix, createCmd, true) f := folders[rand.Intn(len(folders))] t.Run("when flag is off", func(t *testing.T) { @@ -2510,7 +2524,7 @@ func TestSupportBundle(t *testing.T) { } } -func CreateSubtreeInStore(t *testing.T, store folder.Store, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder { +func CreateSubtreeInStore(t *testing.T, store folder.Store, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand, randomUID bool) []*folder.Folder { t.Helper() folders := make([]*folder.Folder, 0, depth) @@ -2518,6 +2532,9 @@ func CreateSubtreeInStore(t *testing.T, store folder.Store, service *Service, de title := fmt.Sprintf("%sfolder-%d", prefix, i) cmd.Title = title cmd.UID = util.GenerateShortUID() + if !randomUID { + cmd.UID = fmt.Sprintf("uidfor-%d", i) + } cmd.OrgID = orgID cmd.SignedInUser = &user.SignedInUser{OrgID: orgID, Permissions: map[int64]map[string][]string{orgID: {dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersAll}}}} diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 2e085fddccd..1428bd037c6 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -201,8 +201,11 @@ func (ss *FolderStoreImpl) Get(ctx context.Context, q folder.GetFolderQuery) (*f if q.WithFullpath { s.WriteString(fmt.Sprintf(`, %s AS fullpath`, getFullpathSQL(ss.db.GetDialect()))) } + if q.WithFullpathUIDs { + s.WriteString(fmt.Sprintf(`, %s AS fullpath_uids`, getFullapathUIDsSQL(ss.db.GetDialect()))) + } s.WriteString(" FROM folder f0") - if q.WithFullpath { + if q.WithFullpath || q.WithFullpathUIDs { s.WriteString(getFullpathJoinsSQL()) } switch { @@ -241,6 +244,7 @@ func (ss *FolderStoreImpl) Get(ctx context.Context, q folder.GetFolderQuery) (*f }) foldr.Fullpath = strings.TrimLeft(foldr.Fullpath, "/") + foldr.FullpathUIDs = strings.TrimLeft(foldr.FullpathUIDs, "/") return foldr.WithURL(), err } diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go index dc60826d618..7d639b92e8b 100644 --- a/pkg/services/folder/folderimpl/sqlstore_test.go +++ b/pkg/services/folder/folderimpl/sqlstore_test.go @@ -485,6 +485,24 @@ func TestIntegrationGet(t *testing.T) { assert.NotEmpty(t, ff.Updated) assert.NotEmpty(t, ff.URL) }) + + t.Run("get folder withFullpathUIDs should set fullpathUIDs as expected", func(t *testing.T) { + ff, err := folderStore.Get(context.Background(), folder.GetFolderQuery{ + UID: &subfolderWithSameName.UID, + OrgID: orgID, + WithFullpathUIDs: true, + }) + require.NoError(t, err) + assert.Equal(t, subfolderWithSameName.UID, ff.UID) + assert.Equal(t, subfolderWithSameName.OrgID, ff.OrgID) + assert.Equal(t, subfolderWithSameName.Title, ff.Title) + assert.Equal(t, subfolderWithSameName.Description, ff.Description) + assert.Equal(t, path.Join(f.UID, subfolderWithSameName.UID), ff.FullpathUIDs) + assert.Equal(t, f.UID, ff.ParentUID) + assert.NotEmpty(t, ff.Created) + assert.NotEmpty(t, ff.Updated) + assert.NotEmpty(t, ff.URL) + }) } func TestIntegrationGetParents(t *testing.T) { diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index b0869713f30..378127516c0 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -148,11 +148,12 @@ type DeleteFolderCommand struct { type GetFolderQuery struct { UID *string // Deprecated: use FolderUID instead - ID *int64 - Title *string - ParentUID *string - OrgID int64 - WithFullpath bool + ID *int64 + Title *string + ParentUID *string + OrgID int64 + WithFullpath bool + WithFullpathUIDs bool SignedInUser identity.Requester `json:"-"` } diff --git a/pkg/services/login/model.go b/pkg/services/login/model.go index b11ec2161f3..3d755a80301 100644 --- a/pkg/services/login/model.go +++ b/pkg/services/login/model.go @@ -27,6 +27,7 @@ type UserAuth struct { type ExternalUserInfo struct { OAuthToken *oauth2.Token + SAMLSession *SAMLSession AuthModule string AuthId string UserId int64 @@ -40,6 +41,11 @@ type ExternalUserInfo struct { SkipTeamSync bool } +type SAMLSession struct { + NameID string + SessionIndex string +} + func (e *ExternalUserInfo) String() string { isGrafanaAdmin := "nil" if e.IsGrafanaAdmin != nil { diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 5a4d9cd2335..18a71f24dde 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -11,8 +11,10 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/setting" ) +// nolint: gocyclo func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink, error) { var configNodes []*navtree.NavLink ctx := c.Req.Context() @@ -103,6 +105,16 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink }) } + if s.cfg.Env == setting.Dev { + pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{ + Text: "Extensions", + Icon: "plug", + SubTitle: "Extend the UI of plugins and Grafana", + Id: "extensions", + Url: s.cfg.AppSubURL + "/admin/extensions", + }) + } + pluginsNode := &navtree.NavLink{ Text: "Plugins and data", SubTitle: "Install plugins and define the relationships between data", diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index e4bdfcf2bfc..4441b75d658 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -389,7 +389,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt }) } - if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagDashboardRestoreUI) && (c.SignedInUser.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) { + if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagDashboardRestore) && (c.SignedInUser.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) { dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ Text: "Recently deleted", SubTitle: "Any items listed here for more than 30 days will be automatically deleted.", diff --git a/pkg/services/ngalert/api/api_notifications_test.go b/pkg/services/ngalert/api/api_notifications_test.go index fc006e37c39..d4e97b3c6f3 100644 --- a/pkg/services/ngalert/api/api_notifications_test.go +++ b/pkg/services/ngalert/api/api_notifications_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" @@ -375,6 +376,7 @@ func createNotificationSrvSutFromEnv(t *testing.T, env *testEnvironment) Notific env.xact, env.log, fakes.NewFakeReceiverPermissionsService(), + tracing.InitializeTracerForTest(), ) return NotificationSrv{ logger: env.log, diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index f22cf445ce5..ccdfde76c75 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -53,7 +53,7 @@ type TemplateService interface { type NotificationPolicyService interface { GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance, version string) error - ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) + ResetPolicyTree(ctx context.Context, orgID int64, provenance alerting_models.Provenance) (definitions.Route, error) } type MuteTimingService interface { @@ -84,7 +84,7 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) respo return ErrResp(http.StatusNotFound, err, "") } if err != nil { - return ErrResp(http.StatusInternalServerError, err, "") + return response.ErrOrFallback(http.StatusInternalServerError, "failed to get notification policy tree", err) } return response.JSON(http.StatusOK, policies) @@ -117,16 +117,17 @@ func (srv *ProvisioningSrv) RoutePutPolicyTree(c *contextmodel.ReqContext, tree return ErrResp(http.StatusBadRequest, err, "") } if err != nil { - return response.ErrOrFallback(http.StatusInternalServerError, "", err) + return response.ErrOrFallback(http.StatusInternalServerError, "failed to update notification policy tree", err) } return response.JSON(http.StatusAccepted, util.DynMap{"message": "policies updated"}) } func (srv *ProvisioningSrv) RouteResetPolicyTree(c *contextmodel.ReqContext) response.Response { - tree, err := srv.policies.ResetPolicyTree(c.Req.Context(), c.SignedInUser.GetOrgID()) + provenance := determineProvenance(c) + tree, err := srv.policies.ResetPolicyTree(c.Req.Context(), c.SignedInUser.GetOrgID(), alerting_models.Provenance(provenance)) if err != nil { - return ErrResp(http.StatusInternalServerError, err, "") + return response.ErrOrFallback(http.StatusInternalServerError, "failed to reset notification policy tree", err) } return response.JSON(http.StatusAccepted, tree) } diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index e6747915ae6..081bc84fe2e 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -140,7 +140,6 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, 500, response.Status()) require.NotEmpty(t, response.Body()) - require.Contains(t, string(response.Body()), "something went wrong") }) t.Run("PUT returns 500", func(t *testing.T) { @@ -164,7 +163,6 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, 500, response.Status()) require.NotEmpty(t, response.Body()) - require.Contains(t, string(response.Body()), "something went wrong") }) }) }) @@ -1832,6 +1830,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { BaseInterval: time.Second * 10, }, FolderService: folderService, + Bus: bus.ProvideBus(tracing.InitializeTracerForTest()), } user := &user.SignedInUser{ OrgID: 1, @@ -1894,7 +1893,7 @@ func createProvisioningSrvSut(t *testing.T) ProvisioningSrv { func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) ProvisioningSrv { t.Helper() - + tracer := tracing.InitializeTracerForTest() configStore := legacy_storage.NewAlertmanagerConfigStore(env.configs) receiverSvc := notifier.NewReceiverService( ac.NewReceiverAccess[*models.Receiver](env.ac, true), @@ -1905,6 +1904,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi env.xact, env.log, ngalertfakes.NewFakeReceiverPermissionsService(), + tracer, ) return ProvisioningSrv{ log: env.log, @@ -2002,7 +2002,7 @@ func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, or return nil } -func (f *fakeNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { +func (f *fakeNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) { f.tree = definitions.Route{} // TODO return f.tree, nil } @@ -2017,7 +2017,7 @@ func (f *fakeFailingNotificationPolicyService) UpdatePolicyTree(ctx context.Cont return fmt.Errorf("something went wrong") } -func (f *fakeFailingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { +func (f *fakeFailingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) { return definitions.Route{}, fmt.Errorf("something went wrong") } @@ -2031,7 +2031,7 @@ func (f *fakeRejectingNotificationPolicyService) UpdatePolicyTree(ctx context.Co return fmt.Errorf("%w: invalid policy tree", provisioning.ErrValidation) } -func (f *fakeRejectingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { +func (f *fakeRejectingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) { return definitions.Route{}, nil } diff --git a/pkg/services/ngalert/api/tooling/definitions/contact_points.go b/pkg/services/ngalert/api/tooling/definitions/contact_points.go index 0cd8f792e4e..4aec132e837 100644 --- a/pkg/services/ngalert/api/tooling/definitions/contact_points.go +++ b/pkg/services/ngalert/api/tooling/definitions/contact_points.go @@ -56,7 +56,7 @@ type EmailIntegration struct { type GooglechatIntegration struct { DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` - URL string `json:"url" yaml:"url" hcl:"url"` + URL Secret `json:"url" yaml:"url" hcl:"url"` Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 84cf3f4d4b3..6ce118341d0 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -464,6 +464,11 @@ type AlertRuleKeyWithVersion struct { AlertRuleKey `xorm:"extends"` } +type AlertRuleKeyWithGroup struct { + RuleGroup string + AlertRuleKey `xorm:"extends"` +} + type AlertRuleKeyWithId struct { AlertRuleKey ID int64 @@ -517,6 +522,11 @@ func (alertRule *AlertRule) GetKey() AlertRuleKey { return AlertRuleKey{OrgID: alertRule.OrgID, UID: alertRule.UID} } +// GetKeyWithGroup returns the alert definitions identifier +func (alertRule *AlertRule) GetKeyWithGroup() AlertRuleKeyWithGroup { + return AlertRuleKeyWithGroup{AlertRuleKey: alertRule.GetKey(), RuleGroup: alertRule.RuleGroup} +} + // GetGroupKey returns the identifier of a group the rule belongs to func (alertRule *AlertRule) GetGroupKey() AlertRuleGroupKey { return AlertRuleGroupKey{OrgID: alertRule.OrgID, NamespaceUID: alertRule.NamespaceUID, RuleGroup: alertRule.RuleGroup} diff --git a/pkg/services/ngalert/models/alert_rule_test.go b/pkg/services/ngalert/models/alert_rule_test.go index 652e0c23d41..5aa76c513d1 100644 --- a/pkg/services/ngalert/models/alert_rule_test.go +++ b/pkg/services/ngalert/models/alert_rule_test.go @@ -884,3 +884,27 @@ func TestTimeRangeYAML(t *testing.T) { require.NoError(t, err) require.Equal(t, yamlRaw, string(serialized)) } + +func TestAlertRuleGetKey(t *testing.T) { + t.Run("should return correct key", func(t *testing.T) { + rule := RuleGen.GenerateRef() + expected := AlertRuleKey{ + OrgID: rule.OrgID, + UID: rule.UID, + } + require.Equal(t, expected, rule.GetKey()) + }) +} + +func TestAlertRuleGetKeyWithGroup(t *testing.T) { + t.Run("should return correct key", func(t *testing.T) { + rule := RuleGen.With( + RuleMuts.WithUniqueGroupIndex(), + ).GenerateRef() + expected := AlertRuleKeyWithGroup{ + AlertRuleKey: rule.GetKey(), + RuleGroup: rule.RuleGroup, + } + require.Equal(t, expected, rule.GetKeyWithGroup()) + }) +} diff --git a/pkg/services/ngalert/models/receivers.go b/pkg/services/ngalert/models/receivers.go index 31ffe697e70..82f34e623b4 100644 --- a/pkg/services/ngalert/models/receivers.go +++ b/pkg/services/ngalert/models/receivers.go @@ -133,6 +133,14 @@ func (r *Receiver) Validate(decryptFn DecryptFn) error { return errors.Join(errs...) } +func (r *Receiver) GetIntegrationTypes() []string { + result := make([]string, 0, len(r.Integrations)) + for _, i := range r.Integrations { + result = append(result, i.Config.Type) + } + return result +} + // Integration is the domain model representation of an integration. type Integration struct { UID string diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index af53eca7e82..b8b2b5ceed2 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -587,6 +587,14 @@ func GenerateRuleKey(orgID int64) AlertRuleKey { } } +// GenerateRuleKeyWithGroup generates a random alert rule key with group +func GenerateRuleKeyWithGroup(orgID int64) AlertRuleKeyWithGroup { + return AlertRuleKeyWithGroup{ + AlertRuleKey: GenerateRuleKey(orgID), + RuleGroup: util.GenerateShortUID(), + } +} + // GenerateGroupKey generates a random group key func GenerateGroupKey(orgID int64) AlertRuleGroupKey { return AlertRuleGroupKey{ diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index ab2c1f4d9b7..37c169130fb 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -441,6 +441,7 @@ func (ng *AlertNG) init() error { ng.store, ng.Log, ng.ResourcePermissions, + ng.tracer, ) provisioningReceiverService := notifier.NewReceiverService( ac.NewReceiverAccess[*models.Receiver](ng.accesscontrol, true), @@ -451,6 +452,7 @@ func (ng *AlertNG) init() error { ng.store, ng.Log, ng.ResourcePermissions, + ng.tracer, ) // Provisioning diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index 4a6433ebb5e..ffe7b757edc 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -1141,6 +1141,7 @@ func GetAvailableNotifiers() []*NotifierPlugin { Placeholder: "Google Chat incoming webhook url", PropertyName: "url", Required: true, + Secure: true, }, { Label: "Title", diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels_test.go b/pkg/services/ngalert/notifier/channels_config/available_channels_test.go index f3290b3956a..2171b2c17fe 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels_test.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels_test.go @@ -26,7 +26,7 @@ func TestGetSecretKeysForContactPointType(t *testing.T) { {receiverType: "wecom", expectedSecretFields: []string{"url", "secret"}}, {receiverType: "prometheus-alertmanager", expectedSecretFields: []string{"basicAuthPassword"}}, {receiverType: "discord", expectedSecretFields: []string{"url"}}, - {receiverType: "googlechat", expectedSecretFields: []string{}}, + {receiverType: "googlechat", expectedSecretFields: []string{"url"}}, {receiverType: "line", expectedSecretFields: []string{"token"}}, {receiverType: "threema", expectedSecretFields: []string{"api_secret"}}, {receiverType: "opsgenie", expectedSecretFields: []string{"apiKey"}}, diff --git a/pkg/services/ngalert/notifier/receiver_svc.go b/pkg/services/ngalert/notifier/receiver_svc.go index 1ecf12594cc..bd4852660aa 100644 --- a/pkg/services/ngalert/notifier/receiver_svc.go +++ b/pkg/services/ngalert/notifier/receiver_svc.go @@ -7,11 +7,14 @@ import ( "fmt" "strings" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "golang.org/x/exp/maps" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -47,6 +50,7 @@ type ReceiverService struct { log log.Logger provenanceValidator validation.ProvenanceStatusTransitionValidator resourcePermissions ac.ReceiverPermissionsService + tracer tracing.Tracer } type alertRuleNotificationSettingsStore interface { @@ -99,6 +103,7 @@ func NewReceiverService( xact transactionManager, log log.Logger, resourcePermissions ac.ReceiverPermissionsService, + tracer tracing.Tracer, ) *ReceiverService { return &ReceiverService{ authz: authz, @@ -110,12 +115,20 @@ func NewReceiverService( log: log, provenanceValidator: validation.ValidateProvenanceRelaxed, resourcePermissions: resourcePermissions, + tracer: tracer, } } // GetReceiver returns a receiver by name. // The receiver's secure settings are decrypted if requested and the user has access to do so. func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiverQuery, user identity.Requester) (*models.Receiver, error) { + ctx, span := rs.tracer.Start(ctx, "alerting.receivers.get", trace.WithAttributes( + attribute.Int64("query_org_id", q.OrgID), + attribute.String("query_name", q.Name), + attribute.Bool("query_decrypt", q.Decrypt), + )) + defer span.End() + revision, err := rs.cfgStore.Get(ctx, q.OrgID) if err != nil { return nil, err @@ -125,6 +138,10 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver return nil, err } + span.AddEvent("Loaded receiver", trace.WithAttributes( + attribute.String("concurrency_token", revision.ConcurrencyToken), + )) + storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType()) if err != nil { return nil, err @@ -145,12 +162,12 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver if q.Decrypt { err := rcv.Decrypt(rs.decryptor(ctx)) if err != nil { - rs.log.Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err) + rs.log.FromContext(ctx).Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err) } } else { err := rcv.Encrypt(rs.encryptor(ctx)) if err != nil { - rs.log.Warn("Failed to encrypt secure settings", "name", rcv.Name, "error", err) + rs.log.FromContext(ctx).Warn("Failed to encrypt secure settings", "name", rcv.Name, "error", err) } } @@ -160,6 +177,15 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver // GetReceivers returns a list of receivers a user has access to. // Receivers can be filtered by name, and secure settings are decrypted if requested and the user has access to do so. func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceiversQuery, user identity.Requester) ([]*models.Receiver, error) { + ctx, span := rs.tracer.Start(ctx, "alerting.receivers.getMany", trace.WithAttributes( + attribute.Int64("query_org_id", q.OrgID), + attribute.StringSlice("query_names", q.Names), + attribute.Int("query_limit", q.Limit), + attribute.Int("query_offset", q.Offset), + attribute.Bool("query_decrypt", q.Decrypt), + )) + defer span.End() + uids := make([]string, 0, len(q.Names)) for _, name := range q.Names { uids = append(uids, legacy_storage.NameToUid(name)) @@ -171,6 +197,11 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive } postables := revision.GetReceivers(uids) + span.AddEvent("Loaded receivers", trace.WithAttributes( + attribute.String("concurrency_token", revision.ConcurrencyToken), + attribute.Int("count", len(postables)), + )) + storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType()) if err != nil { return nil, err @@ -189,16 +220,20 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive return nil, err } + span.AddEvent("Applied access control filter", trace.WithAttributes( + attribute.Int("count", len(receivers)), + )) + for _, rcv := range filtered { if q.Decrypt { err := rcv.Decrypt(rs.decryptor(ctx)) if err != nil { - rs.log.Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err) + rs.log.FromContext(ctx).Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err) } } else { err := rcv.Encrypt(rs.encryptor(ctx)) if err != nil { - rs.log.Warn("Failed to encrypt secure settings", "name", rcv.Name, "error", err) + rs.log.FromContext(ctx).Warn("Failed to encrypt secure settings", "name", rcv.Name, "error", err) } } } @@ -212,6 +247,14 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive // If the users has list access, all receiver settings will be removed from the response. This option is for backwards compatibility with the v1/receivers endpoint // and should be removed when FGAC is fully implemented. func (rs *ReceiverService) ListReceivers(ctx context.Context, q models.ListReceiversQuery, user identity.Requester) ([]*models.Receiver, error) { // TODO: Remove this method with FGAC. + ctx, span := rs.tracer.Start(ctx, "alerting.receivers.list", trace.WithAttributes( + attribute.Int64("query_org_id", q.OrgID), + attribute.StringSlice("query_names", q.Names), + attribute.Int("query_limit", q.Limit), + attribute.Int("query_offset", q.Offset), + )) + defer span.End() + listAccess, err := rs.authz.HasList(ctx, user) if err != nil { return nil, err @@ -228,6 +271,11 @@ func (rs *ReceiverService) ListReceivers(ctx context.Context, q models.ListRecei } postables := revision.GetReceivers(uids) + span.AddEvent("Loaded receivers", trace.WithAttributes( + attribute.String("concurrency_token", revision.ConcurrencyToken), + attribute.Int("count", len(postables)), + )) + storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType()) if err != nil { return nil, err @@ -243,6 +291,10 @@ func (rs *ReceiverService) ListReceivers(ctx context.Context, q models.ListRecei if err != nil { return nil, err } + + span.AddEvent("Applied access control filter", trace.WithAttributes( + attribute.Int("count", len(receivers)), + )) } // Remove settings. @@ -260,6 +312,12 @@ func (rs *ReceiverService) ListReceivers(ctx context.Context, q models.ListRecei // DeleteReceiver deletes a receiver by uid. // UID field currently does not exist, we assume the uid is a particular hashed value of the receiver name. func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, callerProvenance definitions.Provenance, version string, orgID int64, user identity.Requester) error { + ctx, span := rs.tracer.Start(ctx, "alerting.receivers.delete", trace.WithAttributes( + attribute.String("receiver_uid", uid), + attribute.String("receiver_version", version), + )) + defer span.End() + if err := rs.authz.AuthorizeDeleteByUID(ctx, user, uid); err != nil { return err } @@ -284,6 +342,8 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, calle return err } + logger := rs.log.FromContext(ctx).New("receiver", existing.Name, "uid", uid, "version", version, "integrations", existing.GetIntegrationTypes()) + // Check optimistic concurrency. // Optimistic concurrency is optional for delete operations, but we still check it if a version is provided. if version != "" { @@ -292,7 +352,7 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, calle return err } } else { - rs.log.Debug("Ignoring optimistic concurrency check because version was not provided", "receiver", existing.Name, "operation", "delete") + logger.Debug("Ignoring optimistic concurrency check because version was not provided", "operation", "delete") } if err := rs.provenanceValidator(existing.Provenance, models.Provenance(callerProvenance)); err != nil { @@ -306,25 +366,37 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, calle } if usedByRoutes || len(usedByRules) > 0 { + logger.Warn("Cannot delete receiver because it is used", "used_by_routes", usedByRoutes, "used_by_rules", len(usedByRules)) return makeReceiverInUseErr(usedByRoutes, usedByRules) } revision.DeleteReceiver(uid) - return rs.xact.InTransaction(ctx, func(ctx context.Context) error { + err = rs.xact.InTransaction(ctx, func(ctx context.Context) error { err = rs.cfgStore.Save(ctx, revision, orgID) if err != nil { return err } err = rs.resourcePermissions.DeleteResourcePermissions(ctx, orgID, uid) if err != nil { - rs.log.Error("Could not delete receiver permissions", "receiver", existing.Name, "error", err) + logger.Error("Could not delete receiver permissions", "error", err) } return rs.deleteProvenances(ctx, orgID, existing.Integrations) }) + if err != nil { + return err + } + logger.Info("Deleted receiver") + return nil } -func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receiver, orgID int64, user identity.Requester) (*models.Receiver, error) { +func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receiver, orgID int64, user identity.Requester) (result *models.Receiver, err error) { + ctx, span := rs.tracer.Start(ctx, "alerting.receivers.create", trace.WithAttributes( + attribute.String("receiver", r.Name), + attribute.StringSlice("integrations", r.GetIntegrationTypes()), + )) + defer span.End() + if err := rs.authz.AuthorizeCreate(ctx, user); err != nil { return nil, err } @@ -334,6 +406,8 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive return nil, err } + span.AddEvent("Loaded Alertmanager configuration", trace.WithAttributes(attribute.String("concurrency_token", revision.ConcurrencyToken))) + createdReceiver := r.Clone() err = createdReceiver.Encrypt(rs.encryptor(ctx)) if err != nil { @@ -341,6 +415,7 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive } if err := createdReceiver.Validate(rs.decryptor(ctx)); err != nil { + span.RecordError(err) return nil, legacy_storage.MakeErrReceiverInvalid(err) } @@ -364,19 +439,34 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive return nil, err } - result, err := PostableApiReceiverToReceiver(created, createdReceiver.Provenance) + result, err = PostableApiReceiverToReceiver(created, createdReceiver.Provenance) if err != nil { return nil, err } + span.AddEvent("Created a new receiver", trace.WithAttributes( + attribute.String("uid", result.UID), + attribute.String("version", result.Version), + )) + rs.log.FromContext(ctx).Info("Created a new receiver", "receiver", result.Name, "uid", result.UID, "fingerprint", result.Version, "integrations", result.GetIntegrationTypes()) return result, nil } func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receiver, storedSecureFields map[string][]string, orgID int64, user identity.Requester) (*models.Receiver, error) { - // TODO: To support receiver renaming, we need to consider permissions on old and new UID since UIDs are tied to names. + ctx, span := rs.tracer.Start(ctx, "alerting.receivers.update", trace.WithAttributes( + attribute.String("receiver", r.Name), + attribute.String("uid", r.UID), + attribute.String("version", r.Version), + attribute.StringSlice("integrations", r.GetIntegrationTypes()), + )) + defer span.End() + if err := rs.authz.AuthorizeUpdate(ctx, user, r); err != nil { return nil, err } + logger := rs.log.FromContext(ctx).New("receiver", r.Name, "uid", r.UID, "version", r.Version, "integrations", r.GetIntegrationTypes()) + logger.Debug("Updating receiver") + revision, err := rs.cfgStore.Get(ctx, orgID) if err != nil { return nil, err @@ -395,6 +485,14 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive return nil, err } + span.AddEvent("Loaded current receiver", trace.WithAttributes( + attribute.String("concurrency_token", revision.ConcurrencyToken), + attribute.String("receiver", existing.Name), + attribute.String("uid", existing.UID), + attribute.String("version", existing.Version), + attribute.StringSlice("integrations", existing.GetIntegrationTypes()), + )) + // Check optimistic concurrency. err = rs.checkOptimisticConcurrency(existing, r.Version) if err != nil { @@ -440,7 +538,7 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive return err } if permissionsUpdated > 0 { - rs.log.FromContext(ctx).Info("Moved custom receiver permissions", "oldName", existing.Name, "newName", r.Name, "count", permissionsUpdated) + logger.Info("Moved custom receiver permissions", "oldName", existing.Name, "count", permissionsUpdated) } if err := rs.resourcePermissions.DeleteResourcePermissions(ctx, orgID, legacy_storage.NameToUid(existing.Name)); err != nil { return err @@ -465,6 +563,7 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive if err != nil { return nil, err } + logger.Info("Updated receiver", "new_version", result.Version) return result, nil } @@ -662,6 +761,13 @@ func makeErrReceiverDependentResourcesProvenance(usedByRoutes bool, rules []mode } func (rs *ReceiverService) RenameReceiverInDependentResources(ctx context.Context, orgID int64, route *definitions.Route, oldName, newName string, receiverProvenance models.Provenance) error { + ctx, span := rs.tracer.Start(ctx, "alerting.receivers.rename-dependent-resources", trace.WithAttributes( + attribute.String("oldName", oldName), + attribute.String("newName", newName), + attribute.String("receiver_provenance", string(receiverProvenance)), + )) + defer span.End() + validate := validation.ValidateProvenanceOfDependentResources(receiverProvenance) // if there are no references to the old time interval, exit updatedRoutes := legacy_storage.RenameReceiverInRoute(oldName, newName, route) @@ -679,7 +785,12 @@ func (rs *ReceiverService) RenameReceiverInDependentResources(ctx context.Contex return err } if !canUpdate || len(invalidProvenance) > 0 { - return makeErrReceiverDependentResourcesProvenance(updatedRoutes > 0, invalidProvenance) + err := makeErrReceiverDependentResourcesProvenance(updatedRoutes > 0, invalidProvenance) + span.RecordError(err, trace.WithAttributes( + attribute.Bool("invalid_route_provenance", canUpdate), + attribute.Int("invalid_rule_provenances", len(invalidProvenance)), + )) + return err } if len(affected) > 0 || updatedRoutes > 0 { rs.log.FromContext(ctx).Info("Updated rules and routes that use renamed receiver", "oldName", oldName, "newName", newName, "rules", len(affected), "routes", updatedRoutes) diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index b96f5594ad9..fd0df8142f0 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/authz/zanzana" @@ -1484,6 +1485,7 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secretService) *ReceiverS xact, log.NewNopLogger(), fakes.NewFakeReceiverPermissionsService(), + tracing.InitializeTracerForTest(), ) } diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 432dc0e1b31..98693e427d5 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -1647,6 +1647,7 @@ func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRul }, Logger: log.NewNopLogger(), FolderService: folderService, + Bus: bus.ProvideBus(tracing.InitializeTracerForTest()), } // store := fakes.NewRuleStore(t) quotas := MockQuotaChecker{} diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index 045b4030cdf..7f27a70496d 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/authz/zanzana" @@ -494,6 +495,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec xact, log.NewNopLogger(), fakes.NewFakeReceiverPermissionsService(), + tracing.InitializeTracerForTest(), ) return NewContactPointService( diff --git a/pkg/services/ngalert/provisioning/notification_policies.go b/pkg/services/ngalert/provisioning/notification_policies.go index 8eeeeff5211..afba5f0c0d9 100644 --- a/pkg/services/ngalert/provisioning/notification_policies.go +++ b/pkg/services/ngalert/provisioning/notification_policies.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" + "github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation" "github.com/grafana/grafana/pkg/setting" ) @@ -25,6 +26,7 @@ type NotificationPolicyService struct { xact TransactionManager log log.Logger settings setting.UnifiedAlertingSettings + validator validation.ProvenanceStatusTransitionValidator } func NewNotificationPolicyService(am alertmanagerConfigStore, prov ProvisioningStore, @@ -35,6 +37,7 @@ func NewNotificationPolicyService(am alertmanagerConfigStore, prov ProvisioningS xact: xact, log: log, settings: settings, + validator: validation.ValidateProvenanceRelaxed, } } @@ -69,11 +72,20 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI return err } - err = nps.checkOptimisticConcurrency(*revision.Config.AlertmanagerConfig.Route, models.Provenance(tree.Provenance), version, "update") + err = nps.checkOptimisticConcurrency(*revision.Config.AlertmanagerConfig.Route, p, version, "update") if err != nil { return err } + // check that provenance is not changed in an invalid way + storedProvenance, err := nps.provenanceStore.GetProvenance(ctx, &tree, orgID) + if err != nil { + return err + } + if err := nps.validator(storedProvenance, p); err != nil { + return err + } + receivers, err := nps.receiversToMap(revision.Config.AlertmanagerConfig.Receivers) if err != nil { return err @@ -107,7 +119,15 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI }) } -func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { +func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) { + storedProvenance, err := nps.provenanceStore.GetProvenance(ctx, &definitions.Route{}, orgID) + if err != nil { + return definitions.Route{}, err + } + if err := nps.validator(storedProvenance, provenance); err != nil { + return definitions.Route{}, err + } + defaultCfg, err := legacy_storage.DeserializeAlertmanagerConfig([]byte(nps.settings.DefaultConfiguration)) if err != nil { nps.log.Error("Failed to parse default alertmanager config: %w", err) diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index e67fb83be32..9fa49c8f5f1 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -2,6 +2,7 @@ package provisioning import ( "context" + "errors" "testing" "github.com/grafana/alerting/definition" @@ -102,7 +103,10 @@ func TestUpdatePolicyTree(t *testing.T) { t.Run("ErrValidation if referenced receiver does not exist", func(t *testing.T) { rev := getDefaultConfigRevision() - sut, store, _ := createNotificationPolicyServiceSut() + sut, store, prov := createNotificationPolicyServiceSut() + prov.GetProvenanceFunc = func(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) { + return models.ProvenanceNone, nil + } store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return &rev, nil } @@ -137,6 +141,35 @@ func TestUpdatePolicyTree(t *testing.T) { require.ErrorIs(t, err, ErrVersionConflict) }) + t.Run("Error if provenance validation fails", func(t *testing.T) { + sut, store, prov := createNotificationPolicyServiceSut() + prov.GetProvenanceFunc = func(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) { + return models.ProvenanceAPI, nil + } + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &rev, nil + } + expectedRev := getDefaultConfigRevision() + route := newRoute + expectedRev.ConcurrencyToken = rev.ConcurrencyToken + expectedRev.Config.AlertmanagerConfig.Route = &route + + expectedErr := errors.New("test") + sut.validator = func(from, to models.Provenance) error { + assert.Equal(t, models.ProvenanceAPI, from) + assert.Equal(t, models.ProvenanceNone, to) + return expectedErr + } + + err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) + require.ErrorIs(t, err, expectedErr) + + assert.Len(t, prov.Calls, 1) + assert.Equal(t, "GetProvenance", prov.Calls[0].MethodName) + assert.IsType(t, &definitions.Route{}, prov.Calls[0].Arguments[1]) + assert.Equal(t, orgID, prov.Calls[0].Arguments[2].(int64)) + }) + t.Run("updates Route and sets provenance in transaction if route is valid and version matches", func(t *testing.T) { sut, store, prov := createNotificationPolicyServiceSut() store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { @@ -155,12 +188,16 @@ func TestUpdatePolicyTree(t *testing.T) { assertInTransaction(t, store.Calls[1].Args[0].(context.Context)) assert.Equal(t, &expectedRev, store.Calls[1].Args[1]) - assert.Len(t, prov.Calls, 1) - assert.Equal(t, "SetProvenance", prov.Calls[0].MethodName) - assertInTransaction(t, prov.Calls[0].Arguments[0].(context.Context)) - assert.IsType(t, &definitions.Route{}, prov.Calls[0].Arguments[1]) - assert.Equal(t, orgID, prov.Calls[0].Arguments[2].(int64)) - assert.Equal(t, models.ProvenanceAPI, prov.Calls[0].Arguments[3].(models.Provenance)) + c := prov.Calls[0] + assert.Equal(t, "GetProvenance", c.MethodName) + assert.IsType(t, &definitions.Route{}, c.Arguments[1]) + assert.Equal(t, orgID, c.Arguments[2].(int64)) + c = prov.Calls[1] + assert.Equal(t, "SetProvenance", c.MethodName) + assertInTransaction(t, c.Arguments[0].(context.Context)) + assert.IsType(t, &definitions.Route{}, c.Arguments[1]) + assert.Equal(t, orgID, c.Arguments[2].(int64)) + assert.Equal(t, models.ProvenanceAPI, c.Arguments[3].(models.Provenance)) }) t.Run("bypasses optimistic concurrency if provided version is empty", func(t *testing.T) { @@ -181,12 +218,13 @@ func TestUpdatePolicyTree(t *testing.T) { assertInTransaction(t, store.Calls[1].Args[0].(context.Context)) assert.Equal(t, &expectedRev, store.Calls[1].Args[1]) - assert.Len(t, prov.Calls, 1) - assert.Equal(t, "SetProvenance", prov.Calls[0].MethodName) - assertInTransaction(t, prov.Calls[0].Arguments[0].(context.Context)) - assert.IsType(t, &definitions.Route{}, prov.Calls[0].Arguments[1]) - assert.Equal(t, orgID, prov.Calls[0].Arguments[2].(int64)) - assert.Equal(t, models.ProvenanceAPI, prov.Calls[0].Arguments[3].(models.Provenance)) + assert.Len(t, prov.Calls, 2) + c := prov.Calls[1] + assert.Equal(t, "SetProvenance", c.MethodName) + assertInTransaction(t, c.Arguments[0].(context.Context)) + assert.IsType(t, &definitions.Route{}, c.Arguments[1]) + assert.Equal(t, orgID, c.Arguments[2].(int64)) + assert.Equal(t, models.ProvenanceAPI, c.Arguments[3].(models.Provenance)) }) } @@ -223,10 +261,27 @@ func TestResetPolicyTree(t *testing.T) { sut.settings = setting.UnifiedAlertingSettings{ DefaultConfiguration: "{", } - _, err := sut.ResetPolicyTree(context.Background(), orgID) + _, err := sut.ResetPolicyTree(context.Background(), orgID, models.ProvenanceNone) require.ErrorContains(t, err, "failed to parse default alertmanager config") }) + t.Run("Error if provenance validation fails", func(t *testing.T) { + sut, _, prov := createNotificationPolicyServiceSut() + prov.GetProvenanceFunc = func(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) { + return models.ProvenanceAPI, nil + } + + expectedErr := errors.New("test") + sut.validator = func(from, to models.Provenance) error { + assert.Equal(t, models.ProvenanceAPI, from) + assert.Equal(t, models.ProvenanceNone, to) + return expectedErr + } + + _, err := sut.ResetPolicyTree(context.Background(), orgID, models.ProvenanceNone) + require.ErrorIs(t, err, expectedErr) + }) + t.Run("replaces route with one from the default config and copies receivers if do not exist", func(t *testing.T) { defaultConfig := getDefaultConfigRevision().Config data, err := legacy_storage.SerializeAlertmanagerConfig(*defaultConfig) @@ -252,7 +307,7 @@ func TestResetPolicyTree(t *testing.T) { expectedRev.Config.AlertmanagerConfig.Route = getDefaultConfigRevision().Config.AlertmanagerConfig.Route expectedRev.Config.AlertmanagerConfig.Receivers = append(expectedRev.Config.AlertmanagerConfig.Receivers, getDefaultConfigRevision().Config.AlertmanagerConfig.Receivers[0]) - tree, err := sut.ResetPolicyTree(context.Background(), orgID) + tree, err := sut.ResetPolicyTree(context.Background(), orgID, models.ProvenanceNone) require.NoError(t, err) assert.Equal(t, *defaultConfig.AlertmanagerConfig.Route, tree) @@ -262,11 +317,16 @@ func TestResetPolicyTree(t *testing.T) { resetRev := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) assert.Equal(t, expectedRev.Config.AlertmanagerConfig, resetRev.Config.AlertmanagerConfig) - assert.Len(t, prov.Calls, 1) - assert.Equal(t, "DeleteProvenance", prov.Calls[0].MethodName) - assertInTransaction(t, prov.Calls[0].Arguments[0].(context.Context)) - assert.IsType(t, &definitions.Route{}, prov.Calls[0].Arguments[1]) - assert.Equal(t, orgID, prov.Calls[0].Arguments[2]) + assert.Len(t, prov.Calls, 2) + c := prov.Calls[0] + assert.Equal(t, "GetProvenance", c.MethodName) + assert.IsType(t, &definitions.Route{}, c.Arguments[1]) + assert.Equal(t, orgID, c.Arguments[2].(int64)) + c = prov.Calls[1] + assert.Equal(t, "DeleteProvenance", c.MethodName) + assertInTransaction(t, c.Arguments[0].(context.Context)) + assert.IsType(t, &definitions.Route{}, c.Arguments[1]) + assert.Equal(t, orgID, c.Arguments[2]) }) } @@ -286,6 +346,9 @@ func createNotificationPolicyServiceSut() (*NotificationPolicyService, *legacy_s settings: setting.UnifiedAlertingSettings{ DefaultConfiguration: setting.GetAlertmanagerDefaultConfiguration(), }, + validator: func(from, to models.Provenance) error { + return nil + }, }, configStore, prov } diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 25ceaf47783..9fe802d3dd1 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -86,7 +86,7 @@ func newRuleFactory( } return newAlertRule( ctx, - rule.GetKey(), + rule.GetKeyWithGroup(), appURL, disableGrafanaFolder, maxAttempts, @@ -112,7 +112,7 @@ type ruleProvider interface { } type alertRule struct { - key ngmodels.AlertRuleKey + key ngmodels.AlertRuleKeyWithGroup evalCh chan *Evaluation updateCh chan RuleVersionAndPauseStatus @@ -140,7 +140,7 @@ type alertRule struct { func newAlertRule( parent context.Context, - key ngmodels.AlertRuleKey, + key ngmodels.AlertRuleKeyWithGroup, appURL *url.URL, disableGrafanaFolder bool, maxAttempts int64, @@ -155,7 +155,7 @@ func newAlertRule( evalAppliedHook func(ngmodels.AlertRuleKey, time.Time), stopAppliedHook func(ngmodels.AlertRuleKey), ) *alertRule { - ctx, stop := util.WithCancelCause(ngmodels.WithRuleKey(parent, key)) + ctx, stop := util.WithCancelCause(ngmodels.WithRuleKey(parent, key.AlertRuleKey)) return &alertRule{ key: key, evalCh: make(chan *Evaluation), @@ -194,7 +194,7 @@ func (a *alertRule) Status() ngmodels.RuleStatus { // // the second element contains a dropped message that was sent by a concurrent sender. func (a *alertRule) Eval(eval *Evaluation) (bool, *Evaluation) { - if a.key != eval.rule.GetKey() { + if a.key != eval.rule.GetKeyWithGroup() { // Make sure that rule has the same key. This should not happen a.logger.Error("Invalid rule sent for evaluating. Skipping", "ruleKeyToEvaluate", eval.rule.GetKey().String()) return false, eval @@ -352,7 +352,7 @@ func (a *alertRule) Run() error { // cases. ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) defer cancelFunc() - states := a.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, a.key), a.key, ngmodels.StateReasonRuleDeleted) + states := a.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, a.key.AlertRuleKey), a.key, ngmodels.StateReasonRuleDeleted) a.expireAndSend(grafanaCtx, states) } a.logger.Debug("Stopping alert rule routine") @@ -467,7 +467,7 @@ func (a *alertRule) send(ctx context.Context, logger log.Logger, states state.St if len(alerts.PostableAlerts) > 0 { logger.Debug("Sending transitions to notifier", "transitions", len(alerts.PostableAlerts)) - a.sender.Send(ctx, a.key, alerts) + a.sender.Send(ctx, a.key.AlertRuleKey, alerts) } return alerts } @@ -476,12 +476,12 @@ func (a *alertRule) send(ctx context.Context, logger log.Logger, states state.St func (a *alertRule) expireAndSend(ctx context.Context, states []state.StateTransition) { expiredAlerts := state.FromAlertsStateToStoppedAlert(states, a.appURL, a.clock) if len(expiredAlerts.PostableAlerts) > 0 { - a.sender.Send(ctx, a.key, expiredAlerts) + a.sender.Send(ctx, a.key.AlertRuleKey, expiredAlerts) } } func (a *alertRule) resetState(ctx context.Context, isPaused bool) { - rule := a.ruleProvider.get(a.key) + rule := a.ruleProvider.get(a.key.AlertRuleKey) reason := ngmodels.StateReasonUpdated if isPaused { reason = ngmodels.StateReasonPaused @@ -496,7 +496,7 @@ func (a *alertRule) evalApplied(now time.Time) { return } - a.evalAppliedHook(a.key, now) + a.evalAppliedHook(a.key.AlertRuleKey, now) } // stopApplied is only used on tests. @@ -505,7 +505,7 @@ func (a *alertRule) stopApplied() { return } - a.stopAppliedHook(a.key) + a.stopAppliedHook(a.key.AlertRuleKey) } func SchedulerUserFor(orgID int64) *user.SignedInUser { diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index 0621204481d..7433d62ec2f 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -39,7 +39,7 @@ func TestAlertRule(t *testing.T) { t.Run("when rule evaluation is not stopped", func(t *testing.T) { t.Run("update should send to updateCh", func(t *testing.T) { - r := blankRuleForTests(context.Background(), models.GenerateRuleKey(1)) + r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) resultCh := make(chan bool) go func() { resultCh <- r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) @@ -52,7 +52,7 @@ func TestAlertRule(t *testing.T) { } }) t.Run("update should drop any concurrent sending to updateCh", func(t *testing.T) { - r := blankRuleForTests(context.Background(), models.GenerateRuleKey(1)) + r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) version1 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} version2 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} @@ -79,7 +79,7 @@ func TestAlertRule(t *testing.T) { }) t.Run("eval should send to evalCh", func(t *testing.T) { ruleSpec := gen.GenerateRef() - r := blankRuleForTests(context.Background(), ruleSpec.GetKey()) + r := blankRuleForTests(context.Background(), ruleSpec.GetKeyWithGroup()) expected := time.Now() resultCh := make(chan evalResponse) data := &Evaluation{ @@ -103,7 +103,7 @@ func TestAlertRule(t *testing.T) { }) t.Run("eval should drop any concurrent sending to evalCh", func(t *testing.T) { ruleSpec := gen.GenerateRef() - r := blankRuleForTests(context.Background(), ruleSpec.GetKey()) + r := blankRuleForTests(context.Background(), ruleSpec.GetKeyWithGroup()) time1 := time.UnixMilli(rand.Int63n(math.MaxInt64)) time2 := time.UnixMilli(rand.Int63n(math.MaxInt64)) resultCh1 := make(chan evalResponse) @@ -150,7 +150,7 @@ func TestAlertRule(t *testing.T) { }) t.Run("eval should exit when context is cancelled", func(t *testing.T) { ruleSpec := gen.GenerateRef() - r := blankRuleForTests(context.Background(), ruleSpec.GetKey()) + r := blankRuleForTests(context.Background(), ruleSpec.GetKeyWithGroup()) resultCh := make(chan evalResponse) data := &Evaluation{ scheduledAt: time.Now(), @@ -174,14 +174,14 @@ func TestAlertRule(t *testing.T) { }) t.Run("when rule evaluation is stopped", func(t *testing.T) { t.Run("Update should do nothing", func(t *testing.T) { - r := blankRuleForTests(context.Background(), models.GenerateRuleKey(1)) + r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) r.Stop(errRuleDeleted) require.ErrorIs(t, r.ctx.Err(), errRuleDeleted) require.False(t, r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})) }) t.Run("eval should do nothing", func(t *testing.T) { ruleSpec := gen.GenerateRef() - r := blankRuleForTests(context.Background(), ruleSpec.GetKey()) + r := blankRuleForTests(context.Background(), ruleSpec.GetKeyWithGroup()) r.Stop(nil) data := &Evaluation{ scheduledAt: time.Now(), @@ -193,19 +193,19 @@ func TestAlertRule(t *testing.T) { require.Nilf(t, dropped, "expected no dropped evaluations but got one") }) t.Run("calling stop multiple times should not panic", func(t *testing.T) { - r := blankRuleForTests(context.Background(), models.GenerateRuleKey(1)) + r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) r.Stop(nil) r.Stop(nil) }) t.Run("stop should not panic if parent context stopped", func(t *testing.T) { ctx, cancelFn := context.WithCancel(context.Background()) - r := blankRuleForTests(ctx, models.GenerateRuleKey(1)) + r := blankRuleForTests(ctx, models.GenerateRuleKeyWithGroup(1)) cancelFn() r.Stop(nil) }) }) t.Run("should be thread-safe", func(t *testing.T) { - r := blankRuleForTests(context.Background(), models.GenerateRuleKey(1)) + r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) wg := sync.WaitGroup{} go func() { for { @@ -249,7 +249,7 @@ func TestAlertRule(t *testing.T) { }) t.Run("Run should exit if idle when Stop is called", func(t *testing.T) { - rule := blankRuleForTests(context.Background(), models.GenerateRuleKey(1)) + rule := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) runResult := make(chan error) go func() { runResult <- rule.Run() @@ -266,7 +266,7 @@ func TestAlertRule(t *testing.T) { }) } -func blankRuleForTests(ctx context.Context, key models.AlertRuleKey) *alertRule { +func blankRuleForTests(ctx context.Context, key models.AlertRuleKeyWithGroup) *alertRule { return newAlertRule(ctx, key, nil, false, 0, nil, nil, nil, nil, nil, nil, log.NewNopLogger(), nil, nil, nil) } diff --git a/pkg/services/ngalert/state/cache.go b/pkg/services/ngalert/state/cache.go index 28a5391f55d..0da09784d38 100644 --- a/pkg/services/ngalert/state/cache.go +++ b/pkg/services/ngalert/state/cache.go @@ -346,8 +346,8 @@ func (c *cache) removeByRuleUID(orgID int64, uid string) []*State { return states } -// asInstances returns the whole content of the cache as a slice of AlertInstance. -func (c *cache) asInstances(skipNormalState bool) []ngModels.AlertInstance { +// GetAlertInstances returns the whole content of the cache as a slice of AlertInstance. +func (c *cache) GetAlertInstances(skipNormalState bool) []ngModels.AlertInstance { var states []ngModels.AlertInstance c.mtxStates.RLock() defer c.mtxStates.RUnlock() diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index fc23cd85998..b351f6c274f 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -31,8 +31,8 @@ type AlertInstanceManager interface { } type StatePersister interface { - Async(ctx context.Context, cache *cache) - Sync(ctx context.Context, span trace.Span, states StateTransitions) + Async(ctx context.Context, instancesProvider AlertInstancesProvider) + Sync(ctx context.Context, span trace.Span, ruleKey ngModels.AlertRuleKeyWithGroup, states StateTransitions) } // Sender is an optional callback intended for sending the states to an alertmanager. @@ -240,7 +240,7 @@ func (st *Manager) Get(orgID int64, alertRuleUID string, stateId data.Fingerprin // DeleteStateByRuleUID removes the rule instances from cache and instanceStore. A closed channel is returned to be able // to gracefully handle the clear state step in scheduler in case we do not need to use the historian to save state // history. -func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.AlertRuleKey, reason string) []StateTransition { +func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.AlertRuleKeyWithGroup, reason string) []StateTransition { logger := st.log.FromContext(ctx) logger.Debug("Resetting state of the rule") @@ -290,7 +290,7 @@ func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.Al // ResetStateByRuleUID removes the rule instances from cache and instanceStore and saves state history. If the state // history has to be saved, rule must not be nil. func (st *Manager) ResetStateByRuleUID(ctx context.Context, rule *ngModels.AlertRule, reason string) []StateTransition { - ruleKey := rule.GetKey() + ruleKey := rule.GetKeyWithGroup() transitions := st.DeleteStateByRuleUID(ctx, ruleKey, reason) if rule == nil || st.historian == nil || len(transitions) == 0 { @@ -347,7 +347,7 @@ func (st *Manager) ProcessEvalResults( statesToSend = st.updateLastSentAt(allChanges, evaluatedAt) } - st.persister.Sync(ctx, span, allChanges) + st.persister.Sync(ctx, span, alertRule.GetKeyWithGroup(), allChanges) if st.historian != nil { st.historian.Record(ctx, history_model.NewRuleMeta(alertRule, logger), allChanges) } diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 75a25aeeb4b..a46e5357fe5 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -2083,7 +2083,7 @@ func TestDeleteStateByRuleUID(t *testing.T) { assert.Equal(t, tc.startingInstanceDBCount, len(alerts)) expectedReason := util.GenerateShortUID() - transitions := st.DeleteStateByRuleUID(ctx, rule.GetKey(), expectedReason) + transitions := st.DeleteStateByRuleUID(ctx, rule.GetKeyWithGroup(), expectedReason) // Check that the deleted states are the same as the ones that were in cache assert.Equal(t, tc.startingStateCacheCount, len(transitions)) diff --git a/pkg/services/ngalert/state/persist.go b/pkg/services/ngalert/state/persist.go index b3e237a4fb7..a6d9a74d500 100644 --- a/pkg/services/ngalert/state/persist.go +++ b/pkg/services/ngalert/state/persist.go @@ -13,7 +13,9 @@ type InstanceStore interface { ListAlertInstances(ctx context.Context, cmd *models.ListAlertInstancesQuery) ([]*models.AlertInstance, error) SaveAlertInstance(ctx context.Context, instance models.AlertInstance) error DeleteAlertInstances(ctx context.Context, keys ...models.AlertInstanceKey) error - DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKey) error + // SaveAlertInstancesForRule overwrites the state for the given rule. + SaveAlertInstancesForRule(ctx context.Context, key models.AlertRuleKeyWithGroup, instances []models.AlertInstance) error + DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKeyWithGroup) error FullSync(ctx context.Context, instances []models.AlertInstance) error } diff --git a/pkg/services/ngalert/state/persister_async.go b/pkg/services/ngalert/state/persister_async.go index 91807f26921..cc617866202 100644 --- a/pkg/services/ngalert/state/persister_async.go +++ b/pkg/services/ngalert/state/persister_async.go @@ -9,8 +9,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/metrics" + "github.com/grafana/grafana/pkg/services/ngalert/models" ) +type AlertInstancesProvider interface { + GetAlertInstances(skipNormalState bool) []models.AlertInstance +} + type AsyncStatePersister struct { log log.Logger // doNotSaveNormalState controls whether eval.Normal state is persisted to the database and returned by get methods. @@ -30,16 +35,16 @@ func NewAsyncStatePersister(log log.Logger, ticker *clock.Ticker, cfg ManagerCfg } } -func (a *AsyncStatePersister) Async(ctx context.Context, cache *cache) { +func (a *AsyncStatePersister) Async(ctx context.Context, instancesProvider AlertInstancesProvider) { for { select { case <-a.ticker.C: - if err := a.fullSync(ctx, cache); err != nil { + if err := a.fullSync(ctx, instancesProvider); err != nil { a.log.Error("Failed to do a full state sync to database", "err", err) } case <-ctx.Done(): a.log.Info("Scheduler is shutting down, doing a final state sync.") - if err := a.fullSync(context.Background(), cache); err != nil { + if err := a.fullSync(context.Background(), instancesProvider); err != nil { a.log.Error("Failed to do a full state sync to database", "err", err) } a.ticker.Stop() @@ -49,10 +54,10 @@ func (a *AsyncStatePersister) Async(ctx context.Context, cache *cache) { } } -func (a *AsyncStatePersister) fullSync(ctx context.Context, cache *cache) error { +func (a *AsyncStatePersister) fullSync(ctx context.Context, instancesProvider AlertInstancesProvider) error { startTime := time.Now() a.log.Debug("Full state sync start") - instances := cache.asInstances(a.doNotSaveNormalState) + instances := instancesProvider.GetAlertInstances(a.doNotSaveNormalState) if err := a.store.FullSync(ctx, instances); err != nil { a.log.Error("Full state sync failed", "duration", time.Since(startTime), "instances", len(instances)) return err @@ -64,6 +69,6 @@ func (a *AsyncStatePersister) fullSync(ctx context.Context, cache *cache) error return nil } -func (a *AsyncStatePersister) Sync(_ context.Context, _ trace.Span, _ StateTransitions) { +func (a *AsyncStatePersister) Sync(_ context.Context, _ trace.Span, _ models.AlertRuleKeyWithGroup, _ StateTransitions) { a.log.Debug("Sync: No-Op") } diff --git a/pkg/services/ngalert/state/persister_noop.go b/pkg/services/ngalert/state/persister_noop.go index 0275bc5f351..0e770bdc1c4 100644 --- a/pkg/services/ngalert/state/persister_noop.go +++ b/pkg/services/ngalert/state/persister_noop.go @@ -4,12 +4,15 @@ import ( "context" "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/services/ngalert/models" ) type NoopPersister struct{} -func (n *NoopPersister) Async(_ context.Context, _ *cache) {} -func (n *NoopPersister) Sync(_ context.Context, _ trace.Span, _ StateTransitions) {} +func (n *NoopPersister) Async(_ context.Context, _ AlertInstancesProvider) {} +func (n *NoopPersister) Sync(_ context.Context, _ trace.Span, _ models.AlertRuleKeyWithGroup, _ StateTransitions) { +} func NewNoopPersister() StatePersister { return &NoopPersister{} diff --git a/pkg/services/ngalert/state/persister_sync.go b/pkg/services/ngalert/state/persister_sync.go index 1aee0bed09e..5e0e653547b 100644 --- a/pkg/services/ngalert/state/persister_sync.go +++ b/pkg/services/ngalert/state/persister_sync.go @@ -30,12 +30,12 @@ func NewSyncStatePersisiter(log log.Logger, cfg ManagerCfg) StatePersister { } } -func (a *SyncStatePersister) Async(_ context.Context, _ *cache) { +func (a *SyncStatePersister) Async(_ context.Context, _ AlertInstancesProvider) { a.log.Debug("Async: No-Op") } // Sync persists the state transitions to the database. It deletes stale states and saves the current states. -func (a *SyncStatePersister) Sync(ctx context.Context, span trace.Span, allStates StateTransitions) { +func (a *SyncStatePersister) Sync(ctx context.Context, span trace.Span, _ ngModels.AlertRuleKeyWithGroup, allStates StateTransitions) { staleStates := allStates.StaleStates() if len(staleStates) > 0 { a.deleteAlertStates(ctx, staleStates) diff --git a/pkg/services/ngalert/state/persister_sync_test.go b/pkg/services/ngalert/state/persister_sync_test.go index 390e97a4137..e4e98b91401 100644 --- a/pkg/services/ngalert/state/persister_sync_test.go +++ b/pkg/services/ngalert/state/persister_sync_test.go @@ -40,6 +40,7 @@ func TestSyncPersister_saveAlertStates(t *testing.T) { create(eval.NoData, ""), create(eval.Error, ""), } + ruleKey := ngmodels.AlertRuleKeyWithGroup{} transitionToKey := map[ngmodels.AlertInstanceKey]StateTransition{} transitions := make([]StateTransition, 0) @@ -69,7 +70,7 @@ func TestSyncPersister_saveAlertStates(t *testing.T) { InstanceStore: st, MaxStateSaveConcurrency: 1, }) - syncStatePersister.Sync(context.Background(), span, transitions) + syncStatePersister.Sync(context.Background(), span, ruleKey, transitions) savedKeys := map[ngmodels.AlertInstanceKey]ngmodels.AlertInstance{} for _, op := range st.RecordedOps() { saved := op.(ngmodels.AlertInstance) @@ -90,7 +91,7 @@ func TestSyncPersister_saveAlertStates(t *testing.T) { InstanceStore: st, MaxStateSaveConcurrency: 1, }) - syncStatePersister.Sync(context.Background(), span, transitions) + syncStatePersister.Sync(context.Background(), span, ruleKey, transitions) savedKeys := map[ngmodels.AlertInstanceKey]ngmodels.AlertInstance{} for _, op := range st.RecordedOps() { @@ -160,7 +161,7 @@ func TestSyncPersister_saveAlertStates(t *testing.T) { PreviousStateReason: util.GenerateShortUID(), } - syncStatePersister.Sync(context.Background(), span, []StateTransition{transition}) + syncStatePersister.Sync(context.Background(), span, ruleKey, []StateTransition{transition}) require.Len(t, st.RecordedOps(), 1) saved := st.RecordedOps()[0].(ngmodels.AlertInstance) diff --git a/pkg/services/ngalert/state/testing.go b/pkg/services/ngalert/state/testing.go index 6ec84d0df8f..ebbc1bb3fc9 100644 --- a/pkg/services/ngalert/state/testing.go +++ b/pkg/services/ngalert/state/testing.go @@ -56,7 +56,11 @@ func (f *FakeInstanceStore) DeleteAlertInstances(ctx context.Context, q ...model return nil } -func (f *FakeInstanceStore) DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKey) error { +func (f *FakeInstanceStore) SaveAlertInstancesForRule(ctx context.Context, key models.AlertRuleKeyWithGroup, instances []models.AlertInstance) error { + return nil +} + +func (f *FakeInstanceStore) DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKeyWithGroup) error { return nil } diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index a09f1dcc144..637f5420124 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -46,6 +46,15 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI return err } logger.Debug("Deleted alert rules", "count", rows) + if rows > 0 { + keys := make([]ngmodels.AlertRuleKey, 0, len(ruleUID)) + for _, uid := range ruleUID { + keys = append(keys, ngmodels.AlertRuleKey{OrgID: orgID, UID: uid}) + } + _ = st.Bus.Publish(ctx, &RuleChangeEvent{ + RuleKeys: keys, + }) + } rows, err = sess.Table(alertRuleVersion{}).Where("rule_org_id = ?", orgID).In("rule_uid", ruleUID).Delete(alertRule{}) if err != nil { @@ -173,6 +182,7 @@ func (st DBstore) GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmode // Returns the UID and ID of rules that were created in the same order as the input rules. func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRule) ([]ngmodels.AlertRuleKeyWithId, error) { ids := make([]ngmodels.AlertRuleKeyWithId, 0, len(rules)) + keys := make([]ngmodels.AlertRuleKey, 0, len(rules)) return ids, st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { newRules := make([]alertRule, 0, len(rules)) ruleVersions := make([]alertRuleVersion, 0, len(rules)) @@ -211,13 +221,10 @@ func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRu return fmt.Errorf("failed to create new rules: %w", err) } r := newRules[i] - ids = append(ids, ngmodels.AlertRuleKeyWithId{ - AlertRuleKey: ngmodels.AlertRuleKey{ - OrgID: r.OrgID, - UID: r.UID, - }, - ID: r.ID, - }) + key := ngmodels.AlertRuleKey{OrgID: r.OrgID, UID: r.UID} + + ids = append(ids, ngmodels.AlertRuleKeyWithId{AlertRuleKey: key, ID: r.ID}) + keys = append(keys, key) } } @@ -226,6 +233,12 @@ func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRu return fmt.Errorf("failed to create new rule versions: %w", err) } } + + if len(keys) > 0 { + _ = st.Bus.Publish(ctx, &RuleChangeEvent{ + RuleKeys: keys, + }) + } return nil }) } @@ -239,6 +252,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []ngmodels.UpdateR } ruleVersions := make([]alertRuleVersion, 0, len(rules)) + keys := make([]ngmodels.AlertRuleKey, 0, len(rules)) for i := range rules { // We do indexed access way to avoid "G601: Implicit memory aliasing in for loop." // Doing this will be unnecessary with go 1.22 https://stackoverflow.com/a/68247837/767660 @@ -269,6 +283,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []ngmodels.UpdateR v.Version++ v.ParentVersion = r.Existing.Version ruleVersions = append(ruleVersions, v) + keys = append(keys, ngmodels.AlertRuleKey{OrgID: r.New.OrgID, UID: r.New.UID}) } if len(ruleVersions) > 0 { if _, err := sess.Insert(&ruleVersions); err != nil { @@ -283,7 +298,11 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []ngmodels.UpdateR } } } - + if len(keys) > 0 { + _ = st.Bus.Publish(ctx, &RuleChangeEvent{ + RuleKeys: keys, + }) + } return nil }) } diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index df2cb83ddbc..2ccfd9ee5f1 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -43,12 +43,11 @@ func TestIntegrationUpdateAlertRules(t *testing.T) { cfg := setting.NewCfg() cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{BaseInterval: time.Duration(rand.Int63n(100)+1) * time.Second} sqlStore := db.InitTestDB(t) - store := &DBstore{ - SQLStore: sqlStore, - Cfg: cfg.UnifiedAlerting, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: &logtest.Fake{}, - } + logger := &logtest.Fake{} + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + gen := models.RuleGen gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval)) recordingRuleGen := gen.With(gen.WithAllRecordingRules()) @@ -113,6 +112,29 @@ func TestIntegrationUpdateAlertRules(t *testing.T) { require.ErrorIs(t, err, ErrOptimisticLock) }) + + t.Run("should emit event when rules are updated", func(t *testing.T) { + rule := createRule(t, store, gen) + called := false + b.publishFn = func(ctx context.Context, msg bus.Msg) error { + event, ok := msg.(*RuleChangeEvent) + require.True(t, ok) + require.NotNil(t, event) + require.Len(t, event.RuleKeys, 1) + require.Equal(t, rule.GetKey(), event.RuleKeys[0]) + called = true + return nil + } + + newRule := models.CopyRule(rule) + newRule.Title = util.GenerateShortUID() + err := store.UpdateAlertRules(context.Background(), []models.UpdateRule{{ + Existing: rule, + New: *newRule, + }}) + require.NoError(t, err) + require.True(t, called) + }) } func TestIntegrationUpdateAlertRulesWithUniqueConstraintViolation(t *testing.T) { @@ -122,12 +144,9 @@ func TestIntegrationUpdateAlertRulesWithUniqueConstraintViolation(t *testing.T) cfg := setting.NewCfg() cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{BaseInterval: time.Duration(rand.Int63n(100)+1) * time.Second} sqlStore := db.InitTestDB(t) - store := &DBstore{ - SQLStore: sqlStore, - Cfg: cfg.UnifiedAlerting, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: &logtest.Fake{}, - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) gen := models.RuleGen createRuleInFolder := func(title string, orgID int64, namespaceUID string) *models.AlertRule { @@ -379,13 +398,11 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) { } sqlStore := db.InitTestDB(t) - store := &DBstore{ - Logger: &logtest.Fake{}, - SQLStore: sqlStore, - Cfg: cfg.UnifiedAlerting, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - FeatureToggles: featuremgmt.WithFeatures(), - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + logger := &logtest.Fake{} + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + store.FeatureToggles = featuremgmt.WithFeatures() gen := models.RuleGen gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval), gen.WithUniqueOrgID()) @@ -504,7 +521,9 @@ func TestIntegration_CountAlertRules(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() - store := &DBstore{SQLStore: sqlStore, FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())} + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) gen := models.RuleGen gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval), gen.WithRandomRecordingRules()) @@ -569,11 +588,11 @@ func TestIntegration_DeleteInFolder(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() - store := &DBstore{ - SQLStore: sqlStore, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: log.New("test-dbstore"), - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + logger := log.New("test-dbstore") + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + rule := createRule(t, store, nil) t.Run("should not be able to delete folder without permissions to delete rules", func(t *testing.T) { @@ -595,6 +614,38 @@ func TestIntegration_DeleteInFolder(t *testing.T) { }) } +func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + sqlStore := db.InitTestDB(t) + cfg := setting.NewCfg() + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + logger := log.New("test-dbstore") + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + + gen := models.RuleGen + + t.Run("should emit event when rules are deleted", func(t *testing.T) { + rule := createRule(t, store, gen) + called := false + b.publishFn = func(ctx context.Context, msg bus.Msg) error { + event, ok := msg.(*RuleChangeEvent) + require.True(t, ok) + require.NotNil(t, event) + require.Len(t, event.RuleKeys, 1) + require.Equal(t, rule.GetKey(), event.RuleKeys[0]) + called = true + return nil + } + err := store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, rule.UID) + require.NoError(t, err) + require.True(t, called) + }) +} + func TestIntegration_GetNamespaceByUID(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -602,11 +653,10 @@ func TestIntegration_GetNamespaceByUID(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() - store := &DBstore{ - SQLStore: sqlStore, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: log.New("test-dbstore"), - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + logger := log.New("test-dbstore") + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) u := &user.SignedInUser{ UserID: 1, @@ -657,12 +707,10 @@ func TestIntegrationInsertAlertRules(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() cfg.UnifiedAlerting.BaseInterval = 1 * time.Second - store := &DBstore{ - SQLStore: sqlStore, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: log.New("test-dbstore"), - Cfg: cfg.UnifiedAlerting, - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + logger := log.New("test-dbstore") + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) gen := models.RuleGen.With( models.RuleGen.WithOrgID(orgID), @@ -790,6 +838,25 @@ func TestIntegrationInsertAlertRules(t *testing.T) { require.ErrorIs(t, err, models.ErrAlertRuleConflictBase) require.ErrorContains(t, err, "rule UID under the same organisation should be unique") }) + + t.Run("should emit event when rules are inserted", func(t *testing.T) { + rule := gen.Generate() + called := false + b.publishFn = func(ctx context.Context, msg bus.Msg) error { + event, ok := msg.(*RuleChangeEvent) + require.True(t, ok) + require.NotNil(t, event) + require.Len(t, event.RuleKeys, 1) + require.Equal(t, rule.GetKey(), event.RuleKeys[0]) + called = true + return nil + } + + rules, err := store.InsertAlertRules(context.Background(), []models.AlertRule{rule}) + require.NoError(t, err) + require.Len(t, rules, 1) + require.True(t, called) + }) } func TestIntegrationAlertRulesNotificationSettings(t *testing.T) { @@ -808,12 +875,10 @@ func TestIntegrationAlertRulesNotificationSettings(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() cfg.UnifiedAlerting.BaseInterval = 1 * time.Second - store := &DBstore{ - SQLStore: sqlStore, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: log.New("test-dbstore"), - Cfg: cfg.UnifiedAlerting, - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + logger := log.New("test-dbstore") + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) receiverName := "receiver\"-" + uuid.NewString() timeIntervalName := "time-" + util.GenerateShortUID() @@ -1082,14 +1147,12 @@ func TestIntegrationListNotificationSettings(t *testing.T) { } sqlStore := db.InitTestDB(t) + folderService := setupFolderService(t, sqlStore, setting.NewCfg(), featuremgmt.WithFeatures()) + logger := log.New("test-dbstore") cfg := setting.NewCfg() cfg.UnifiedAlerting.BaseInterval = 1 * time.Second - store := &DBstore{ - SQLStore: sqlStore, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: log.New("test-dbstore"), - Cfg: cfg.UnifiedAlerting, - } + b := &fakeBus{} + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) searchName := `name-%"-👍'test` gen := models.RuleGen @@ -1207,12 +1270,10 @@ func TestIntegrationGetNamespacesByRuleUID(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() cfg.UnifiedAlerting.BaseInterval = 1 * time.Second - store := &DBstore{ - SQLStore: sqlStore, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: log.New("test-dbstore"), - Cfg: cfg.UnifiedAlerting, - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + logger := log.New("test-dbstore") + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) rules := models.RuleGen.With(models.RuleMuts.WithOrgID(1), models.RuleMuts.WithRandomRecordingRules()).GenerateMany(5) _, err := store.InsertAlertRules(context.Background(), rules) @@ -1258,13 +1319,11 @@ func TestIntegrationRuleGroupsCaseSensitive(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() cfg.UnifiedAlerting.BaseInterval = 1 * time.Second - store := &DBstore{ - SQLStore: sqlStore, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: log.New("test-dbstore"), - Cfg: cfg.UnifiedAlerting, - FeatureToggles: featuremgmt.WithFeatures(), - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + logger := log.New("test-dbstore") + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + store.FeatureToggles = featuremgmt.WithFeatures() gen := models.RuleGen.With(models.RuleMuts.WithOrgID(1)) misc := gen.GenerateMany(5, 10) @@ -1365,12 +1424,9 @@ func TestIncreaseVersionForAllRulesInNamespaces(t *testing.T) { cfg := setting.NewCfg() cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{BaseInterval: time.Duration(rand.Int63n(100)+1) * time.Second} sqlStore := db.InitTestDB(t) - store := &DBstore{ - SQLStore: sqlStore, - Cfg: cfg.UnifiedAlerting, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: &logtest.Fake{}, - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) orgID := int64(1) gen := models.RuleGen gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval)).With(gen.WithOrgID(orgID)) @@ -1482,12 +1538,9 @@ func TestIntegration_AlertRuleVersionsCleanup(t *testing.T) { BaseInterval: time.Duration(rand.Int63n(100)+1) * time.Second, } sqlStore := db.InitTestDB(t) - store := &DBstore{ - SQLStore: sqlStore, - Cfg: cfg.UnifiedAlerting, - FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()), - Logger: &logtest.Fake{}, - } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) generator := models.RuleGen generator = generator.With(generator.WithIntervalMatching(store.Cfg.BaseInterval), generator.WithUniqueOrgID()) @@ -1601,3 +1654,33 @@ func TestIntegration_AlertRuleVersionsCleanup(t *testing.T) { require.Error(t, err) }) } + +func createTestStore( + sqlStore db.DB, + folderService folder.Service, + logger log.Logger, + cfg setting.UnifiedAlertingSettings, + bus bus.Bus, +) *DBstore { + return &DBstore{ + SQLStore: sqlStore, + FolderService: folderService, + Logger: logger, + Cfg: cfg, + Bus: bus, + } +} + +type fakeBus struct { + publishFn func(ctx context.Context, msg bus.Msg) error +} + +func (f *fakeBus) AddEventListener(handler bus.HandlerFunc) {} + +func (f *fakeBus) Publish(ctx context.Context, msg bus.Msg) error { + if f.publishFn != nil { + return f.publishFn(ctx, msg) + } + + return nil +} diff --git a/pkg/services/ngalert/store/database.go b/pkg/services/ngalert/store/database.go index 0470cbb9251..8f9e752db89 100644 --- a/pkg/services/ngalert/store/database.go +++ b/pkg/services/ngalert/store/database.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -41,10 +42,18 @@ type DBstore struct { FolderService folder.Service DashboardService dashboards.DashboardService AccessControl accesscontrol.AccessControl + Bus bus.Bus } func ProvideDBStore( - cfg *setting.Cfg, featureToggles featuremgmt.FeatureToggles, sqlstore db.DB, folderService folder.Service, dashboards dashboards.DashboardService, ac accesscontrol.AccessControl) (*DBstore, error) { + cfg *setting.Cfg, + featureToggles featuremgmt.FeatureToggles, + sqlstore db.DB, + folderService folder.Service, + dashboards dashboards.DashboardService, + ac accesscontrol.AccessControl, + bus bus.Bus, +) (*DBstore, error) { store := DBstore{ Cfg: cfg.UnifiedAlerting, FeatureToggles: featureToggles, @@ -53,9 +62,14 @@ func ProvideDBStore( FolderService: folderService, DashboardService: dashboards, AccessControl: ac, + Bus: bus, } if err := folderService.RegisterService(store); err != nil { return nil, err } return &store, nil } + +type RuleChangeEvent struct { + RuleKeys []models.AlertRuleKey +} diff --git a/pkg/services/ngalert/store/instance_database.go b/pkg/services/ngalert/store/instance_database.go index c5f1b654bc4..4ede66e2a4a 100644 --- a/pkg/services/ngalert/store/instance_database.go +++ b/pkg/services/ngalert/store/instance_database.go @@ -2,6 +2,7 @@ package store import ( "context" + "errors" "fmt" "sort" "strings" @@ -32,6 +33,7 @@ func (st DBstore) ListAlertInstances(ctx context.Context, cmd *models.ListAlertI if cmd.RuleUID != "" { addToQuery(` AND rule_uid = ?`, cmd.RuleUID) } + if st.FeatureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingNoNormalState) { s.WriteString(fmt.Sprintf(" AND NOT (current_state = '%s' AND current_reason = '')", models.InstanceStateNormal)) } @@ -206,7 +208,13 @@ func (st DBstore) DeleteAlertInstances(ctx context.Context, keys ...models.Alert return err } -func (st DBstore) DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKey) error { +// SaveAlertInstancesForRule is not implemented for instance database store. +func (st DBstore) SaveAlertInstancesForRule(ctx context.Context, key models.AlertRuleKeyWithGroup, instances []models.AlertInstance) error { + st.Logger.Error("SaveAlertInstancesForRule is not implemented for instance database store.") + return errors.New("method SaveAlertInstancesForRule is not implemented for instance database store") +} + +func (st DBstore) DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKeyWithGroup) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { _, err := sess.Exec("DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid = ?", key.OrgID, key.UID) return err diff --git a/pkg/services/ngalert/store/testing.go b/pkg/services/ngalert/store/testing.go index 39f6f4ffb09..8e4c975b73b 100644 --- a/pkg/services/ngalert/store/testing.go +++ b/pkg/services/ngalert/store/testing.go @@ -7,8 +7,10 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/setting" @@ -146,6 +148,7 @@ func SetupStoreForTesting(t *testing.T, db db.DB) *DBstore { Cfg: cfg.UnifiedAlerting, FolderService: service, Logger: &logtest.Fake{}, + Bus: bus.ProvideBus(tracing.InitializeTracerForTest()), } return store } diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index ae5664adeec..4af0babc731 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -65,7 +65,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, dashboardService, dashboardStore := testutil.SetupDashboardService(tb, sqlStore, folderStore, cfg) features := featuremgmt.WithFeatures() folderService := testutil.SetupFolderService(tb, cfg, sqlStore, dashboardStore, folderStore, bus, features, ac) - ruleStore, err := store.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, folderService, &dashboards.FakeDashboardService{}, ac) + ruleStore, err := store.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, folderService, &dashboards.FakeDashboardService{}, ac, bus) require.NoError(tb, err) ng, err := ngalert.ProvideService( cfg, features, nil, nil, routing.NewRouteRegister(), sqlStore, kvstore.NewFakeKVStore(), nil, nil, quotatest.New(false, nil), @@ -82,6 +82,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, Logger: log.New("ngalert-test"), DashboardService: dashboardService, FolderService: folderService, + Bus: bus, } } diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 4eafd7db235..677b07bcb27 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" @@ -584,7 +585,7 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { backendFactoryProvider := fakes.NewFakeBackendProcessProvider() backendFactoryProvider.BackendFactoryFunc = func(ctx context.Context, plugin *plugins.Plugin) backendplugin.PluginFactoryFunc { - return func(pluginID string, logger log.Logger, env func() []string) (backendplugin.Plugin, error) { + return func(pluginID string, logger log.Logger, tracer trace.Tracer, env func() []string) (backendplugin.Plugin, error) { require.Equal(t, "grafana-test-datasource", pluginID) return &fakes.FakeBackendPlugin{}, nil } @@ -1133,7 +1134,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { procPrvdr := fakes.NewFakeBackendProcessProvider() // Cause an initialization error procPrvdr.BackendFactoryFunc = func(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { - return func(pluginID string, _ log.Logger, _ func() []string) (backendplugin.Plugin, error) { + return func(pluginID string, _ log.Logger, _ trace.Tracer, _ func() []string) (backendplugin.Plugin, error) { if pluginID == "test-datasource" { return nil, errors.New("failed to initialize") } diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index 6422c7ddd61..a590d8c810a 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -68,7 +68,7 @@ func ProvideInitializationStage(cfg *config.PluginManagementCfg, pr registry.Ser return initialization.New(cfg, initialization.Opts{ InitializeFuncs: []initialization.InitializeFunc{ ExternalServiceRegistrationStep(cfg, externalServiceRegistry, tracer), - initialization.BackendClientInitStep(pluginEnvProvider, bp), + initialization.BackendClientInitStep(pluginEnvProvider, bp, tracer), initialization.BackendProcessStartStep(pm), RegisterPluginRolesStep(roleRegistry), RegisterActionSetsStep(actionSetRegistry), diff --git a/pkg/services/pluginsintegration/renderer/renderer.go b/pkg/services/pluginsintegration/renderer/renderer.go index 9eb71fc7d7d..ff8003b957c 100644 --- a/pkg/services/pluginsintegration/renderer/renderer.go +++ b/pkg/services/pluginsintegration/renderer/renderer.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" @@ -22,11 +23,12 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" "github.com/grafana/grafana/pkg/services/rendering" + "go.opentelemetry.io/otel/trace" ) func ProvideService(cfg *config.PluginManagementCfg, pluginEnvProvider envvars.Provider, - registry registry.Service) (*Manager, error) { - l, err := createLoader(cfg, pluginEnvProvider, registry) + registry registry.Service, tracer tracing.Tracer) (*Manager, error) { + l, err := createLoader(cfg, pluginEnvProvider, registry, tracer) if err != nil { return nil, err } @@ -105,7 +107,7 @@ func (m *Manager) Renderer(ctx context.Context) (rendering.Plugin, bool) { } func createLoader(cfg *config.PluginManagementCfg, pluginEnvProvider envvars.Provider, - pr registry.Service) (loader.Service, error) { + pr registry.Service, tracer trace.Tracer) (loader.Service, error) { d := discovery.New(cfg, discovery.Opts{ FindFilterFuncs: []discovery.FindFilterFunc{ discovery.NewPermittedPluginTypesFilterStep([]plugins.Type{plugins.TypeRenderer}), @@ -124,7 +126,7 @@ func createLoader(cfg *config.PluginManagementCfg, pluginEnvProvider envvars.Pro }) i := initialization.New(cfg, initialization.Opts{ InitializeFuncs: []initialization.InitializeFunc{ - initialization.BackendClientInitStep(pluginEnvProvider, provider.New(provider.RendererProvider)), + initialization.BackendClientInitStep(pluginEnvProvider, provider.New(provider.RendererProvider), tracer), initialization.PluginRegistrationStep(pr), }, }) diff --git a/pkg/services/provisioning/alerting/notification_policy_provisioner.go b/pkg/services/provisioning/alerting/notification_policy_provisioner.go index 55f3dd2ae9d..63f6cbe9ce3 100644 --- a/pkg/services/provisioning/alerting/notification_policy_provisioner.go +++ b/pkg/services/provisioning/alerting/notification_policy_provisioner.go @@ -45,7 +45,7 @@ func (c *defaultNotificationPolicyProvisioner) Unprovision(ctx context.Context, files []*AlertingFile) error { for _, file := range files { for _, orgID := range file.ResetPolicies { - _, err := c.notificationPolicyService.ResetPolicyTree(ctx, int64(orgID)) + _, err := c.notificationPolicyService.ResetPolicyTree(ctx, int64(orgID), models.ProvenanceFile) if err != nil { return fmt.Errorf("%s: %w", file.Filename, err) } diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index e2b3fc70333..deb1cb083e6 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/correlations" @@ -21,7 +22,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" - "github.com/grafana/grafana/pkg/services/ngalert/store" + alertstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" @@ -41,6 +42,7 @@ func ProvideService( cfg *setting.Cfg, sqlStore db.DB, pluginStore pluginstore.Store, + alertingStore *alertstore.DBstore, encryptionService encryption.Internal, notificatonService *notifications.NotificationService, dashboardProvisioningService dashboardservice.DashboardProvisioningService, @@ -54,12 +56,14 @@ func ProvideService( secrectService secrets.Service, orgService org.Service, resourcePermissions accesscontrol.ReceiverPermissionsService, + tracer tracing.Tracer, ) (*ProvisioningServiceImpl, error) { s := &ProvisioningServiceImpl{ Cfg: cfg, SQLStore: sqlStore, ac: ac, pluginStore: pluginStore, + alertingStore: alertingStore, EncryptionService: encryptionService, NotificationService: notificatonService, newDashboardProvisioner: dashboards.New, @@ -137,6 +141,7 @@ type ProvisioningServiceImpl struct { orgService org.Service ac accesscontrol.AccessControl pluginStore pluginstore.Store + alertingStore *alertstore.DBstore EncryptionService encryption.Internal NotificationService *notifications.NotificationService log log.Logger @@ -157,6 +162,7 @@ type ProvisioningServiceImpl struct { secretService secrets.Service folderService folder.Service resourcePermissions accesscontrol.ReceiverPermissionsService + tracer tracing.Tracer } func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error { @@ -260,16 +266,9 @@ func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) erro func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error { alertingPath := filepath.Join(ps.Cfg.ProvisioningPath, "alerting") - st := store.DBstore{ - Cfg: ps.Cfg.UnifiedAlerting, - SQLStore: ps.SQLStore, - Logger: ps.log, - FolderService: nil, // we don't use it yet - DashboardService: ps.dashboardService, - } ruleService := provisioning.NewAlertRuleService( - st, - st, + ps.alertingStore, + ps.alertingStore, ps.folderService, //ps.dashboardService, ps.quotaService, @@ -278,26 +277,27 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error int64(ps.Cfg.UnifiedAlerting.BaseInterval.Seconds()), ps.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit, ps.log, - notifier.NewCachedNotificationSettingsValidationService(&st), + notifier.NewCachedNotificationSettingsValidationService(ps.alertingStore), alertingauthz.NewRuleService(ps.ac), ) - configStore := legacy_storage.NewAlertmanagerConfigStore(&st) + configStore := legacy_storage.NewAlertmanagerConfigStore(ps.alertingStore) receiverSvc := notifier.NewReceiverService( alertingauthz.NewReceiverAccess[*ngmodels.Receiver](ps.ac, true), configStore, - st, - st, + ps.alertingStore, + ps.alertingStore, ps.secretService, ps.SQLStore, ps.log, ps.resourcePermissions, + ps.tracer, ) contactPointService := provisioning.NewContactPointService(configStore, ps.secretService, - st, ps.SQLStore, receiverSvc, ps.log, &st, ps.resourcePermissions) + ps.alertingStore, ps.SQLStore, receiverSvc, ps.log, ps.alertingStore, ps.resourcePermissions) notificationPolicyService := provisioning.NewNotificationPolicyService(configStore, - st, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log) - mutetimingsService := provisioning.NewMuteTimingService(configStore, st, &st, ps.log, &st) - templateService := provisioning.NewTemplateService(configStore, st, &st, ps.log) + ps.alertingStore, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log) + mutetimingsService := provisioning.NewMuteTimingService(configStore, ps.alertingStore, ps.alertingStore, ps.log, ps.alertingStore) + templateService := provisioning.NewTemplateService(configStore, ps.alertingStore, ps.alertingStore, ps.log) cfg := prov_alerting.ProvisionerConfig{ Path: alertingPath, RuleService: *ruleService, diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 4422ed67ebe..2807dc4c2d6 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -496,7 +496,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe m := metrics.NewNGAlert(prometheus.NewRegistry()) ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) - ruleStore, err := ngstore.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + ruleStore, err := ngstore.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, b) require.NoError(t, err) _, err = ngalert.ProvideService( cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, ngalertfakes.NewFakeKVStore(t), nil, nil, quotaService, diff --git a/pkg/services/searchusers/searchusers.go b/pkg/services/searchusers/searchusers.go index 036ac87aee7..683a318e4d4 100644 --- a/pkg/services/searchusers/searchusers.go +++ b/pkg/services/searchusers/searchusers.go @@ -112,7 +112,7 @@ func (s *OSSService) SearchUser(c *contextmodel.ReqContext) (*user.SearchUserQue for _, user := range res.Users { user.AvatarURL = dtos.GetGravatarUrl(s.cfg, user.Email) - user.AuthLabels = make([]string, len(user.AuthModule)) + user.AuthLabels = make([]string, 0, len(user.AuthModule)) for _, authModule := range user.AuthModule { user.AuthLabels = append(user.AuthLabels, login.GetAuthProviderLabel(authModule)) } diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index d4d8384a4d3..d2c9d319c95 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -158,6 +158,10 @@ func addUserMigrations(mg *Migrator) { // Service accounts login were not unique per org. this migration is part of making it unique per org // to be able to create service accounts that are unique per org mg.AddMigration(usermig.AllowSameLoginCrossOrgs, &usermig.ServiceAccountsSameLoginCrossOrgs{}) + // Before it was fixed, the previous migration introduced the org_id again in logins that already had it. + // This migration removes the duplicate org_id from the login. + // TODO(aarongodin): this migration was causing instances to fail to start. It must be corrected before being added + // mg.AddMigration(usermig.DedupOrgInLogin, &usermig.ServiceAccountsDeduplicateOrgInLogin{}) // Users login and email should be in lower case mg.AddMigration(usermig.LowerCaseUserLoginAndEmail, &usermig.UsersLowerCaseLoginAndEmail{}) diff --git a/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go b/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go index 0737091bda4..54b71250f1b 100644 --- a/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go +++ b/pkg/services/sqlstore/migrations/usermig/service_account_multiple_org_login_migrator.go @@ -16,9 +16,6 @@ const ( // to be able to create service accounts that are unique per org func AddServiceAccountsAllowSameLoginCrossOrgs(mg *migrator.Migrator) { mg.AddMigration(AllowSameLoginCrossOrgs, &ServiceAccountsSameLoginCrossOrgs{}) - // Before it was fixed, the previous migration introduced the org_id again in logins that already had it. - // This migration removes the duplicate org_id from the login. - mg.AddMigration(DedupOrgInLogin, &ServiceAccountsDeduplicateOrgInLogin{}) } var _ migrator.CodeMigration = new(ServiceAccountsSameLoginCrossOrgs) diff --git a/pkg/services/ssosettings/strategies/saml_strategy.go b/pkg/services/ssosettings/strategies/saml_strategy.go index e6e820474fc..740d86ee2c0 100644 --- a/pkg/services/ssosettings/strategies/saml_strategy.go +++ b/pkg/services/ssosettings/strategies/saml_strategy.go @@ -33,6 +33,7 @@ func (s *SAMLStrategy) loadSAMLSettings() map[string]any { section := s.settingsProvider.Section("auth.saml") result := map[string]any{ "enabled": section.KeyValue("enabled").MustBool(false), + "name": section.KeyValue("name").MustString("SAML"), "single_logout": section.KeyValue("single_logout").MustBool(false), "allow_sign_up": section.KeyValue("allow_sign_up").MustBool(false), "auto_login": section.KeyValue("auto_login").MustBool(false), diff --git a/pkg/services/ssosettings/strategies/saml_strategy_test.go b/pkg/services/ssosettings/strategies/saml_strategy_test.go index fb61b54893e..d076f708400 100644 --- a/pkg/services/ssosettings/strategies/saml_strategy_test.go +++ b/pkg/services/ssosettings/strategies/saml_strategy_test.go @@ -16,8 +16,9 @@ var ( [auth.saml] enabled = true single_logout = true + name = "SAML Test" allow_sign_up = true - auto_login = false + auto_login = true certificate = devenv/docker/blocks/auth/saml-enterprise/cert.crt certificate_path = /path/to/cert private_key = dGhpcyBpcyBteSBwcml2YXRlIGtleSB0aGF0IEkgd2FudCB0byBnZXQgZW5jb2RlZCBpbiBiYXNlIDY0 @@ -55,7 +56,8 @@ var ( "enabled": true, "single_logout": true, "allow_sign_up": true, - "auto_login": false, + "auto_login": true, + "name": "SAML Test", "certificate": "devenv/docker/blocks/auth/saml-enterprise/cert.crt", "certificate_path": "/path/to/cert", "private_key": "dGhpcyBpcyBteSBwcml2YXRlIGtleSB0aGF0IEkgd2FudCB0byBnZXQgZW5jb2RlZCBpbiBiYXNlIDY0", diff --git a/pkg/services/unifiedSearch/service.go b/pkg/services/unifiedSearch/service.go index eb83ac8bbaa..7db7a0bffb5 100644 --- a/pkg/services/unifiedSearch/service.go +++ b/pkg/services/unifiedSearch/service.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" @@ -151,16 +152,20 @@ func (s *StandardSearchService) DoQuery(ctx context.Context, user *backend.User, } func (s *StandardSearchService) doQuery(ctx context.Context, signedInUser *user.SignedInUser, orgID int64, q Query) *backend.DataResponse { - response := s.doSearchQuery(ctx, q, s.cfg.AppSubURL) + response := s.doSearchQuery(ctx, q, s.cfg.AppSubURL, orgID) return response } -func (s *StandardSearchService) doSearchQuery(ctx context.Context, qry Query, _ string) *backend.DataResponse { +func (s *StandardSearchService) doSearchQuery(ctx context.Context, qry Query, _ string, orgID int64) *backend.DataResponse { response := &backend.DataResponse{} - req := &resource.SearchRequest{Tenant: s.cfg.StackID, Query: qry.Query} + // will use stack id for cloud and org id for on-prem + tenantId := request.GetNamespaceMapper(s.cfg)(orgID) + + req := &resource.SearchRequest{Tenant: tenantId, Query: qry.Query, Limit: int64(qry.Limit), Offset: int64(qry.From)} res, err := s.resourceClient.Search(ctx, req) if err != nil { + s.logger.Error("Failed to search resources", "error", err) response.Error = err return response } diff --git a/pkg/storage/unified/apistore/store.go b/pkg/storage/unified/apistore/store.go index d4911da167c..11a5cc3e5ea 100644 --- a/pkg/storage/unified/apistore/store.go +++ b/pkg/storage/unified/apistore/store.go @@ -465,7 +465,7 @@ func (s *Storage) GuaranteedUpdate( return apierrors.NewNotFound(s.gr, req.Key.Name) } - updatedObj, _, err = tryUpdate(existingObj, res) + updatedObj, _, err = tryUpdate(existingObj.DeepCopyObject(), res) if err != nil { if attempt >= MaxUpdateAttempts { return err diff --git a/pkg/storage/unified/apistore/store_test.go b/pkg/storage/unified/apistore/store_test.go index 287aeea5c41..f0d648b9b5e 100644 --- a/pkg/storage/unified/apistore/store_test.go +++ b/pkg/storage/unified/apistore/store_test.go @@ -135,12 +135,12 @@ func TestDeleteWithSuggestion(t *testing.T) { storagetesting.RunTestDeleteWithSuggestion(ctx, t, store) } -//func TestDeleteWithSuggestionAndConflict(t *testing.T) { -// ctx, store, destroyFunc, err := testSetup(t) -// defer destroyFunc() -// assert.NoError(t, err) -// storagetesting.RunTestDeleteWithSuggestionAndConflict(ctx, t, store) -//} +func TestDeleteWithSuggestionAndConflict(t *testing.T) { + ctx, store, destroyFunc, err := testSetup(t) + defer destroyFunc() + assert.NoError(t, err) + storagetesting.RunTestDeleteWithSuggestionAndConflict(ctx, t, store) +} // TODO: this test relies on update //func TestDeleteWithSuggestionOfDeletedObject(t *testing.T) { diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 3be85811afb..7f5f354e248 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -43,6 +43,32 @@ require ( go.etcd.io/bbolt v1.3.9 // indirect ) +require ( + github.com/RoaringBitmap/roaring v1.9.3 // indirect + github.com/bits-and-blooms/bitset v1.12.0 // indirect + github.com/blevesearch/bleve_index_api v1.1.10 // indirect + github.com/blevesearch/geo v0.1.20 // indirect + github.com/blevesearch/go-faiss v1.0.20 // indirect + github.com/blevesearch/go-porterstemmer v1.0.3 // indirect + github.com/blevesearch/gtreap v0.1.1 // indirect + github.com/blevesearch/mmap-go v1.0.4 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.2.15 // indirect + github.com/blevesearch/segment v0.9.1 // indirect + github.com/blevesearch/snowballstem v0.9.0 // indirect + github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect + github.com/blevesearch/vellum v1.0.10 // indirect + github.com/blevesearch/zapx/v11 v11.3.10 // indirect + github.com/blevesearch/zapx/v12 v12.3.10 // indirect + github.com/blevesearch/zapx/v13 v13.3.10 // indirect + github.com/blevesearch/zapx/v14 v14.3.10 // indirect + github.com/blevesearch/zapx/v15 v15.3.13 // indirect + github.com/blevesearch/zapx/v16 v16.1.5 // indirect + github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/mschoch/smat v0.2.0 // indirect + go.etcd.io/bbolt v1.3.9 // indirect +) + require ( github.com/beorn7/perks v1.0.1 // indirect github.com/blevesearch/bleve/v2 v2.4.2 diff --git a/pkg/storage/unified/resource/index.go b/pkg/storage/unified/resource/index.go index cdd8b6f68c5..33abc5e92c7 100644 --- a/pkg/storage/unified/resource/index.go +++ b/pkg/storage/unified/resource/index.go @@ -4,13 +4,16 @@ import ( "context" "encoding/json" "fmt" - "log" + golog "log" "os" + "strings" "github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2/analysis/lang/en" "github.com/blevesearch/bleve/v2/mapping" "github.com/google/uuid" + "github.com/grafana/grafana/pkg/infra/log" + "golang.org/x/exp/slices" ) type Shard struct { @@ -23,6 +26,7 @@ type Index struct { shards map[string]Shard opts Opts s *server + log log.Logger } func NewIndex(s *server, opts Opts) *Index { @@ -30,47 +34,70 @@ func NewIndex(s *server, opts Opts) *Index { s: s, opts: opts, shards: make(map[string]Shard), + log: log.New("unifiedstorage.search.index"), } return idx } +func (i *Index) IndexBatch(list *ListResponse, kind string) error { + for _, obj := range list.Items { + res, err := getResource(obj.Value) + if err != nil { + return err + } + + shard, err := i.getShard(tenant(res)) + if err != nil { + return err + } + i.log.Debug("initial indexing resources batch", "count", len(list.Items), "kind", kind, "tenant", tenant(res)) + + var jsonDoc interface{} + err = json.Unmarshal(obj.Value, &jsonDoc) + if err != nil { + return err + } + err = shard.batch.Index(res.Metadata.Uid, jsonDoc) + if err != nil { + return err + } + } + + for _, shard := range i.shards { + err := shard.index.Batch(shard.batch) + if err != nil { + return err + } + shard.batch.Reset() + } + + return nil +} + func (i *Index) Init(ctx context.Context) error { resourceTypes := fetchResourceTypes() for _, rt := range resourceTypes { - r := &ListRequest{Options: rt} - list, err := i.s.List(ctx, r) - if err != nil { - return err - } + i.log.Info("indexing resource", "kind", rt.Key.Resource) + r := &ListRequest{Options: rt, Limit: 100} - for _, obj := range list.Items { - res, err := getResource(obj.Value) + // Paginate through the list of resources and index each page + for { + list, err := i.s.List(ctx, r) if err != nil { return err } - shard, err := i.getShard(tenant(res)) + // Index current page + err = i.IndexBatch(list, rt.Key.Resource) if err != nil { return err } - var jsonDoc interface{} - err = json.Unmarshal(obj.Value, &jsonDoc) - if err != nil { - return err + if list.NextPageToken == "" { + break } - err = shard.batch.Index(res.Metadata.Uid, jsonDoc) - if err != nil { - return err - } - } - for _, shard := range i.shards { - err := shard.index.Batch(shard.batch) - if err != nil { - return err - } - shard.batch.Reset() + r.NextPageToken = list.NextPageToken } } @@ -83,6 +110,7 @@ func (i *Index) Index(ctx context.Context, data *Data) error { return err } tenant := tenant(res) + i.log.Debug("indexing resource for tenant", "res", res, "tenant", tenant) shard, err := i.getShard(tenant) if err != nil { return err @@ -111,7 +139,7 @@ func (i *Index) Delete(ctx context.Context, uid string, key *ResourceKey) error return nil } -func (i *Index) Search(ctx context.Context, tenant string, query string) ([]string, error) { +func (i *Index) Search(ctx context.Context, tenant string, query string, limit int, offset int) ([]SearchSummary, error) { if tenant == "" { tenant = "default" } @@ -119,20 +147,58 @@ func (i *Index) Search(ctx context.Context, tenant string, query string) ([]stri if err != nil { return nil, err } - req := bleve.NewSearchRequest(bleve.NewQueryStringQuery(query)) - req.Fields = []string{"kind", "spec.title"} + docCount, err := shard.index.DocCount() + if err != nil { + return nil, err + } + i.log.Info("got index for tenant", "tenant", tenant, "docCount", docCount) + // use 10 as a default limit for now + if limit <= 0 { + limit = 10 + } + + req := bleve.NewSearchRequest(bleve.NewQueryStringQuery(query)) + req.From = offset + req.Size = limit + + req.Fields = []string{"*"} // return all indexed fields in search results + + i.log.Info("searching index", "query", query, "tenant", tenant) res, err := shard.index.Search(req) if err != nil { return nil, err } - hits := res.Hits - results := []string{} - for _, hit := range hits { - val := fmt.Sprintf("%s:%s", hit.Fields["kind"], hit.Fields["spec.title"]) - results = append(results, val) + + i.log.Info("got search results", "hits", hits) + + results := make([]SearchSummary, len(hits)) + for resKey, hit := range hits { + searchSummary := SearchSummary{} + + // add common fields to search results + searchSummary.Kind = hit.Fields["kind"].(string) + searchSummary.Metadata.CreationTimestamp = hit.Fields["metadata.creationTimestamp"].(string) + searchSummary.Metadata.Uid = hit.Fields["metadata.uid"].(string) + + // add allowed indexed spec fields to search results + specResult := map[string]interface{}{} + for k, v := range hit.Fields { + if strings.HasPrefix(k, "spec.") { + mappedFields := specFieldMappings(searchSummary.Kind) + // should only include spec fields we care about in search results + if slices.Contains(mappedFields, k) { + specKey := strings.TrimPrefix(k, "spec.") + specResult[specKey] = v + } + } + searchSummary.Spec = specResult + } + + results[resKey] = searchSummary } + return results, nil } @@ -140,11 +206,17 @@ func tenant(res *Resource) string { return res.Metadata.Namespace } +type SearchSummary struct { + Kind string `json:"kind"` + Metadata `json:"metadata"` + Spec map[string]interface{} `json:"spec"` +} + type Metadata struct { Name string Namespace string - Uid string - CreationTimestamp string + Uid string `json:"uid"` + CreationTimestamp string `json:"creationTimestamp"` Labels map[string]string Annotations map[string]string } @@ -165,32 +237,31 @@ func createFileIndex() (bleve.Index, string, error) { indexPath := fmt.Sprintf("%s%s.bleve", os.TempDir(), uuid.New().String()) index, err := bleve.New(indexPath, createIndexMappings()) if err != nil { - log.Fatalf("Failed to create index: %v", err) + golog.Fatalf("Failed to create index: %v", err) } return index, indexPath, err } -// TODO: clean this up. it was copied from owens performance test func createIndexMappings() *mapping.IndexMappingImpl { - //Create mapping for the name and creationTimestamp fields in the metadata - nameFieldMapping := bleve.NewTextFieldMapping() + //Create mapping for the creationTimestamp field in the metadata creationTimestampFieldMapping := bleve.NewDateTimeFieldMapping() + uidMapping := bleve.NewTextFieldMapping() metaMapping := bleve.NewDocumentMapping() - metaMapping.AddFieldMappingsAt("name", nameFieldMapping) metaMapping.AddFieldMappingsAt("creationTimestamp", creationTimestampFieldMapping) + metaMapping.AddFieldMappingsAt("uid", uidMapping) metaMapping.Dynamic = false metaMapping.Enabled = true + // Spec is different for all resources, so we create a dynamic mapping for it to index all fields (for now) specMapping := bleve.NewDocumentMapping() - specMapping.AddFieldMappingsAt("title", nameFieldMapping) - specMapping.Dynamic = false + specMapping.Dynamic = true specMapping.Enabled = true //Create a sub-document mapping for the metadata field objectMapping := bleve.NewDocumentMapping() objectMapping.AddSubDocumentMapping("metadata", metaMapping) objectMapping.AddSubDocumentMapping("spec", specMapping) - objectMapping.Dynamic = false + objectMapping.Dynamic = true objectMapping.Enabled = true // a generic reusable mapping for english text @@ -248,3 +319,14 @@ func fetchResourceTypes() []*ListOptions { }) return items } + +func specFieldMappings(kind string) []string { + mappedFields := map[string][]string{ + "Playlist": { + "spec.title", + "spec.interval", + }, + } + + return mappedFields[kind] +} diff --git a/pkg/storage/unified/resource/index_server.go b/pkg/storage/unified/resource/index_server.go index 47937e7f7f3..8feedacd60e 100644 --- a/pkg/storage/unified/resource/index_server.go +++ b/pkg/storage/unified/resource/index_server.go @@ -2,6 +2,7 @@ package resource import ( "context" + "encoding/json" "errors" "log" "strings" @@ -16,23 +17,27 @@ type IndexServer struct { ws *indexWatchServer } -func (is IndexServer) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) { - results, err := is.index.Search(ctx, req.Tenant, req.Query) +func (is *IndexServer) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) { + results, err := is.index.Search(ctx, req.Tenant, req.Query, int(req.Limit), int(req.Offset)) if err != nil { return nil, err } res := &SearchResponse{} for _, r := range results { - res.Items = append(res.Items, &ResourceWrapper{Value: []byte(r)}) + resJsonBytes, err := json.Marshal(r) + if err != nil { + return nil, err + } + res.Items = append(res.Items, &ResourceWrapper{Value: resJsonBytes}) } return res, nil } -func (is IndexServer) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) { +func (is *IndexServer) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) { return nil, nil } -func (is IndexServer) Origin(ctx context.Context, req *OriginRequest) (*OriginResponse, error) { +func (is *IndexServer) Origin(ctx context.Context, req *OriginRequest) (*OriginResponse, error) { return nil, nil } diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index 7728dd880be..1a641f12284 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -1619,8 +1619,16 @@ type SearchRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` - Tenant string `protobuf:"bytes,2,opt,name=tenant,proto3" json:"tenant,omitempty"` + // query string for chosen implementation (currently just bleve) + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + // default to bleve + QueryType string `protobuf:"bytes,2,opt,name=queryType,proto3" json:"queryType,omitempty"` + Tenant string `protobuf:"bytes,3,opt,name=tenant,proto3" json:"tenant,omitempty"` + // resource kind (playlists, dashboards, etc) + Kind string `protobuf:"bytes,4,opt,name=kind,proto3" json:"kind,omitempty"` + // pagination support + Limit int64 `protobuf:"varint,5,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int64 `protobuf:"varint,6,opt,name=offset,proto3" json:"offset,omitempty"` } func (x *SearchRequest) Reset() { @@ -1662,6 +1670,13 @@ func (x *SearchRequest) GetQuery() string { return "" } +func (x *SearchRequest) GetQueryType() string { + if x != nil { + return x.QueryType + } + return "" +} + func (x *SearchRequest) GetTenant() string { if x != nil { return x.Tenant @@ -1669,6 +1684,27 @@ func (x *SearchRequest) GetTenant() string { return "" } +func (x *SearchRequest) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *SearchRequest) GetLimit() int64 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *SearchRequest) GetOffset() int64 { + if x != nil { + return x.Offset + } + return 0 +} + type SearchResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2449,136 +2485,142 @@ var file_resource_proto_rawDesc = []byte{ 0x09, 0x0a, 0x05, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, - 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x3d, - 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x22, 0x41, 0x0a, - 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x22, 0x9a, 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, - 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, - 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, - 0x6f, 0x77, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0xbf, 0x01, - 0x0a, 0x0f, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, - 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, - 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, - 0x8e, 0x01, 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, - 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, - 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, - 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, - 0x22, 0xe5, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, - 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x69, 0x7a, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, - 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, - 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0xc4, 0x01, 0x0a, 0x0e, 0x4f, 0x72, 0x69, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x69, - 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, - 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, - 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, - 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, - 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, - 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, - 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, - 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, - 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, - 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, 0x0a, - 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, - 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, - 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, - 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, - 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, - 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x30, 0x01, 0x32, 0xc9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x12, 0x3b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x17, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, - 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, - 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x9d, + 0x01, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x54, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, + 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x22, 0x41, + 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, + 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, + 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, + 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, + 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, + 0x68, 0x6f, 0x77, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0xbf, + 0x01, 0x0a, 0x0f, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, + 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x22, 0x8e, 0x01, 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, + 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, + 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, + 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, + 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, + 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0xc4, 0x01, 0x0a, 0x0e, 0x4f, 0x72, + 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, + 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, + 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, - 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, - 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, + 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, + 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, + 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, + 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, + 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, + 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, + 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, + 0x74, 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, + 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, + 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x30, 0x01, 0x32, 0xc9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x3b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, + 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, + 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, + 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, + 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index dcaee25b909..bfbb654a3ae 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -325,8 +325,16 @@ message WatchEvent { } message SearchRequest { + // query string for chosen implementation (currently just bleve) string query = 1; - string tenant = 2; + // default to bleve + string queryType = 2; + string tenant = 3; + // resource kind (playlists, dashboards, etc) + string kind = 4; + // pagination support + int64 limit = 5; + int64 offset = 6; } message SearchResponse { diff --git a/pkg/storage/unified/resource/validation.go b/pkg/storage/unified/resource/validation.go index dce25a1b8fe..066ac42c2af 100644 --- a/pkg/storage/unified/resource/validation.go +++ b/pkg/storage/unified/resource/validation.go @@ -4,14 +4,14 @@ import ( "regexp" ) -var validNameCharPattern = `a-zA-Z0-9\-\_\.` +var validNameCharPattern = `a-zA-Z0-9:\-\_\.` var validNamePattern = regexp.MustCompile(`^[` + validNameCharPattern + `]*$`).MatchString func validateName(name string) *ErrorResult { if len(name) == 0 { return NewBadRequestError("name is too short") } - if len(name) > 64 { + if len(name) > 253 { return NewBadRequestError("name is too long") } if !validNamePattern(name) { diff --git a/pkg/storage/unified/resource/validation_test.go b/pkg/storage/unified/resource/validation_test.go index b9f9a51d7b5..bc0e4bd8bda 100644 --- a/pkg/storage/unified/resource/validation_test.go +++ b/pkg/storage/unified/resource/validation_test.go @@ -1,22 +1,22 @@ package resource import ( + "strings" "testing" "github.com/stretchr/testify/require" ) func TestNameValidation(t *testing.T) { - require.NotNil(t, validateName("")) // too short - require.NotNil(t, validateName( // too long (max 64) - "0123456789012345678901234567890123456789012345678901234567890123456789", - )) + require.NotNil(t, validateName("")) // too short + require.NotNil(t, validateName(strings.Repeat("0", 254))) // too long (max 253) // OK require.Nil(t, validateName("a")) require.Nil(t, validateName("hello-world")) require.Nil(t, validateName("hello.world")) require.Nil(t, validateName("hello_world")) + require.Nil(t, validateName("hello:world")) // Bad characters require.NotNil(t, validateName("hello world")) diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 1a6e79e3257..950814ba9d8 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -10,18 +10,19 @@ import ( "time" "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "google.golang.org/protobuf/proto" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql/db" "github.com/grafana/grafana/pkg/storage/unified/sql/dbutil" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - "google.golang.org/protobuf/proto" - apierrors "k8s.io/apimachinery/pkg/api/errors" ) -const trace_prefix = "sql.resource." +const tracePrefix = "sql.resource." const defaultPollingInterval = 100 * time.Millisecond type Backend interface { @@ -119,7 +120,7 @@ func (b *backend) Stop(_ context.Context) error { } func (b *backend) WriteEvent(ctx context.Context, event resource.WriteEvent) (int64, error) { - _, span := b.tracer.Start(ctx, trace_prefix+"WriteEvent") + _, span := b.tracer.Start(ctx, tracePrefix+"WriteEvent") defer span.End() // TODO: validate key ? switch event.Type { @@ -135,7 +136,7 @@ func (b *backend) WriteEvent(ctx context.Context, event resource.WriteEvent) (in } func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, error) { - ctx, span := b.tracer.Start(ctx, trace_prefix+"Create") + ctx, span := b.tracer.Start(ctx, tracePrefix+"Create") defer span.End() var newVersion int64 guid := uuid.New().String() @@ -192,7 +193,7 @@ func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, } func (b *backend) update(ctx context.Context, event resource.WriteEvent) (int64, error) { - ctx, span := b.tracer.Start(ctx, trace_prefix+"Update") + ctx, span := b.tracer.Start(ctx, tracePrefix+"Update") defer span.End() var newVersion int64 guid := uuid.New().String() @@ -251,7 +252,7 @@ func (b *backend) update(ctx context.Context, event resource.WriteEvent) (int64, } func (b *backend) delete(ctx context.Context, event resource.WriteEvent) (int64, error) { - ctx, span := b.tracer.Start(ctx, trace_prefix+"Delete") + ctx, span := b.tracer.Start(ctx, tracePrefix+"Delete") defer span.End() var newVersion int64 guid := uuid.New().String() @@ -303,7 +304,7 @@ func (b *backend) delete(ctx context.Context, event resource.WriteEvent) (int64, } func (b *backend) ReadResource(ctx context.Context, req *resource.ReadRequest) *resource.ReadResponse { - _, span := b.tracer.Start(ctx, trace_prefix+".Read") + _, span := b.tracer.Start(ctx, tracePrefix+".Read") defer span.End() // TODO: validate key ? @@ -338,7 +339,7 @@ func (b *backend) ReadResource(ctx context.Context, req *resource.ReadRequest) * } func (b *backend) ListIterator(ctx context.Context, req *resource.ListRequest, cb func(resource.ListIterator) error) (int64, error) { - _, span := b.tracer.Start(ctx, trace_prefix+"List") + _, span := b.tracer.Start(ctx, tracePrefix+"List") defer span.End() if req.Options == nil || req.Options.Key.Group == "" || req.Options.Key.Resource == "" { @@ -592,12 +593,12 @@ func (b *backend) listLatestRVs(ctx context.Context) (groupResourceRV, error) { // fetchLatestRV returns the current maximum RV in the resource table func fetchLatestRV(ctx context.Context, x db.ContextExecer, d sqltemplate.Dialect, group, resource string) (int64, error) { - res, err := dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionRequest{ - SQLTemplate: sqltemplate.New(d), - Group: group, - Resource: resource, - ReadOnly: true, - resourceVersion: new(resourceVersion), + res, err := dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{ + SQLTemplate: sqltemplate.New(d), + Group: group, + Resource: resource, + ReadOnly: true, + Response: new(resourceVersionResponse), }) if errors.Is(err, sql.ErrNoRows) { return 1, nil @@ -608,9 +609,8 @@ func fetchLatestRV(ctx context.Context, x db.ContextExecer, d sqltemplate.Dialec } func (b *backend) poll(ctx context.Context, grp string, res string, since int64, stream chan<- *resource.WrittenEvent) (int64, error) { - ctx, span := b.tracer.Start(ctx, trace_prefix+"poll") + ctx, span := b.tracer.Start(ctx, tracePrefix+"poll") defer span.End() - var records []*historyPollResponse err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error { var err error @@ -657,53 +657,58 @@ func (b *backend) poll(ctx context.Context, grp string, res string, since int64, return nextRV, nil } -// resourceVersionAtomicInc atomically increases the version of a kind within a -// transaction. +// resourceVersionAtomicInc atomically increases the version of a kind within a transaction. // TODO: Ideally we should attempt to update the RV in the resource and resource_history tables // in a single roundtrip. This would reduce the latency of the operation, and also increase the // throughput of the system. This is a good candidate for a future optimization. func resourceVersionAtomicInc(ctx context.Context, x db.ContextExecer, d sqltemplate.Dialect, key *resource.ResourceKey) (newVersion int64, err error) { - // TODO: refactor this code to run in a multi-statement transaction in order to minimize the number of round trips. - // 1 Lock the row for update - rv, err := dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionRequest{ - SQLTemplate: sqltemplate.New(d), - Group: key.Group, - Resource: key.Resource, - resourceVersion: new(resourceVersion), - }) - - if errors.Is(err, sql.ErrNoRows) { - // if there wasn't a row associated with the given resource, we create one with - // version 2 to match the etcd behavior. - if _, err = dbutil.Exec(ctx, x, sqlResourceVersionInsert, sqlResourceVersionRequest{ - SQLTemplate: sqltemplate.New(d), - Group: key.Group, - Resource: key.Resource, - resourceVersion: &resourceVersion{1}, - }); err != nil { - return 0, fmt.Errorf("insert into resource_version: %w", err) - } - return 2, nil - } - - if err != nil { - return 0, fmt.Errorf("get current resource version: %w", err) - } - nextRV := rv.ResourceVersion + 1 - - // 2. Increment the resource version - _, err = dbutil.Exec(ctx, x, sqlResourceVersionInc, sqlResourceVersionRequest{ + // 1. Lock to row and prevent concurrent updates until the transaction is committed. + res, err := dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{ SQLTemplate: sqltemplate.New(d), Group: key.Group, Resource: key.Resource, - resourceVersion: &resourceVersion{ - ResourceVersion: nextRV, - }, + + Response: new(resourceVersionResponse), ReadOnly: false, // This locks the row for update + }) + + if errors.Is(err, sql.ErrNoRows) { + // if there wasn't a row associated with the given resource, then we create it. + if _, err = dbutil.Exec(ctx, x, sqlResourceVersionInsert, sqlResourceVersionUpsertRequest{ + SQLTemplate: sqltemplate.New(d), + Group: key.Group, + Resource: key.Resource, + }); err != nil { + return 0, fmt.Errorf("insert into resource_version: %w", err) + } + res, err = dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{ + SQLTemplate: sqltemplate.New(d), + Group: key.Group, + Resource: key.Resource, + Response: new(resourceVersionResponse), + ReadOnly: true, // This locks the row for update + }) + if err != nil { + return 0, fmt.Errorf("fetching RV after read") + } + return res.ResourceVersion, nil + } else if err != nil { + return 0, fmt.Errorf("lock the resource version: %w", err) + } + + // 2. Update the RV + // Most times, the RV is the current microsecond timestamp generated on the sql server (to avoid clock skew). + // In rare occasion, the server clock might go back in time. In those cases, we simply increment the + // previous RV until the clock catches up. + nextRV := max(res.CurrentEpoch, res.ResourceVersion+1) + + _, err = dbutil.Exec(ctx, x, sqlResourceVersionUpdate, sqlResourceVersionUpsertRequest{ + SQLTemplate: sqltemplate.New(d), + Group: key.Group, + Resource: key.Resource, + ResourceVersion: nextRV, }) if err != nil { return 0, fmt.Errorf("increase resource version: %w", err) } - - // 3. Return the incremented value return nextRV, nil } diff --git a/pkg/storage/unified/sql/backend_test.go b/pkg/storage/unified/sql/backend_test.go index 33b7bab7d6a..0d96d73896d 100644 --- a/pkg/storage/unified/sql/backend_test.go +++ b/pkg/storage/unified/sql/backend_test.go @@ -36,8 +36,8 @@ type testBackend struct { test.TestDBProvider } -func (b testBackend) ExecWithResult(expectedSQL string) { - b.SQLMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(0, 0)) +func (b testBackend) ExecWithResult(expectedSQL string, lastInsertID int64, rowsAffected int64) { + b.SQLMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(lastInsertID, rowsAffected)) } func (b testBackend) ExecWithErr(expectedSQL string, err error) { @@ -204,8 +204,8 @@ func TestBackend_IsHealthy(t *testing.T) { // expectSuccessfulResourceVersionAtomicInc sets up expectations for calling // resourceVersionAtomicInc, where the returned RV will be 1. func expectSuccessfulResourceVersionAtomicInc(t *testing.T, b testBackend) { - b.QueryWithResult("select resource_version for update", 0, nil) - b.ExecWithResult("insert resource_version") + b.QueryWithResult("select resource_version for update", 2, Rows{{12345, 23456}}) + b.ExecWithResult("update resource_version set resource_version", 0, 0) } // expectUnsuccessfulResourceVersionAtomicInc sets up expectations for calling @@ -227,7 +227,7 @@ func TestResourceVersionAtomicInc(t *testing.T) { v, err := resourceVersionAtomicInc(ctx, b.DB, dialect, resKey) require.NoError(t, err) - require.Equal(t, int64(2), v) + require.Equal(t, int64(23456), v) }) t.Run("happy path - update existing row", func(t *testing.T) { @@ -235,24 +235,23 @@ func TestResourceVersionAtomicInc(t *testing.T) { b, ctx := setupBackendTest(t) - b.QueryWithResult("select resource_version for update", 1, Rows{{2}}) - b.ExecWithResult("update resource_version") + b.QueryWithResult("select resource_version for update", 2, Rows{{12345, 23456}}) + b.ExecWithResult("update resource_version", 0, 1) v, err := resourceVersionAtomicInc(ctx, b.DB, dialect, resKey) require.NoError(t, err) - require.Equal(t, int64(3), v) + require.Equal(t, int64(23456), v) }) t.Run("error getting current version", func(t *testing.T) { t.Parallel() b, ctx := setupBackendTest(t) - b.QueryWithErr("select resource_version for update", errTest) v, err := resourceVersionAtomicInc(ctx, b.DB, dialect, resKey) require.Zero(t, v) require.Error(t, err) - require.ErrorContains(t, err, "get current resource version") + require.ErrorContains(t, err, "lock the resource version") }) t.Run("error inserting new row", func(t *testing.T) { @@ -260,7 +259,7 @@ func TestResourceVersionAtomicInc(t *testing.T) { b, ctx := setupBackendTest(t) - b.QueryWithResult("select resource_version for update", 0, nil) + b.QueryWithResult("select resource_version", 0, Rows{}) b.ExecWithErr("insert resource_version", errTest) v, err := resourceVersionAtomicInc(ctx, b.DB, dialect, resKey) @@ -273,7 +272,7 @@ func TestResourceVersionAtomicInc(t *testing.T) { t.Parallel() b, ctx := setupBackendTest(t) - b.QueryWithResult("select resource_version for update", 1, Rows{{2}}) + b.QueryWithResult("select resource_version for update", 2, Rows{{12345, 23456}}) b.ExecWithErr("update resource_version", errTest) v, err := resourceVersionAtomicInc(ctx, b.DB, dialect, resKey) @@ -295,16 +294,16 @@ func TestBackend_create(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("insert resource") - b.ExecWithResult("insert resource_history") + b.ExecWithResult("insert resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) expectSuccessfulResourceVersionAtomicInc(t, b) // returns RV=1 - b.ExecWithResult("update resource_history") - b.ExecWithResult("update resource") + b.ExecWithResult("update resource_history", 0, 1) + b.ExecWithResult("update resource", 0, 1) b.SQLMock.ExpectCommit() v, err := b.create(ctx, event) require.NoError(t, err) - require.Equal(t, int64(2), v) + require.Equal(t, int64(23456), v) }) t.Run("error inserting into resource", func(t *testing.T) { @@ -326,7 +325,7 @@ func TestBackend_create(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("insert resource") + b.ExecWithResult("insert resource", 0, 1) b.ExecWithErr("insert resource_history", errTest) b.SQLMock.ExpectRollback() @@ -341,8 +340,8 @@ func TestBackend_create(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("insert resource") - b.ExecWithResult("insert resource_history") + b.ExecWithResult("insert resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) expectUnsuccessfulResourceVersionAtomicInc(t, b, errTest) b.SQLMock.ExpectRollback() @@ -357,8 +356,8 @@ func TestBackend_create(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("insert resource") - b.ExecWithResult("insert resource_history") + b.ExecWithResult("insert resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) expectSuccessfulResourceVersionAtomicInc(t, b) b.ExecWithErr("update resource_history", errTest) b.SQLMock.ExpectRollback() @@ -366,7 +365,7 @@ func TestBackend_create(t *testing.T) { v, err := b.create(ctx, event) require.Zero(t, v) require.Error(t, err) - require.ErrorContains(t, err, "update resource_history") + require.ErrorContains(t, err, "update resource_history", 0, 1) }) t.Run("error updating resource", func(t *testing.T) { @@ -374,10 +373,10 @@ func TestBackend_create(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("insert resource") - b.ExecWithResult("insert resource_history") + b.ExecWithResult("insert resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) expectSuccessfulResourceVersionAtomicInc(t, b) - b.ExecWithResult("update resource_history") + b.ExecWithResult("update resource_history", 0, 1) b.ExecWithErr("update resource", errTest) b.SQLMock.ExpectRollback() @@ -400,16 +399,16 @@ func TestBackend_update(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("update resource") - b.ExecWithResult("insert resource_history") - expectSuccessfulResourceVersionAtomicInc(t, b) // returns RV=1 - b.ExecWithResult("update resource_history") - b.ExecWithResult("update resource") + b.ExecWithResult("update resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) + expectSuccessfulResourceVersionAtomicInc(t, b) + b.ExecWithResult("update resource_history", 0, 1) + b.ExecWithResult("update resource", 0, 1) b.SQLMock.ExpectCommit() v, err := b.update(ctx, event) require.NoError(t, err) - require.Equal(t, int64(2), v) + require.Equal(t, int64(23456), v) }) t.Run("error in first update to resource", func(t *testing.T) { @@ -431,7 +430,7 @@ func TestBackend_update(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("update resource") + b.ExecWithResult("update resource", 0, 1) b.ExecWithErr("insert resource_history", errTest) b.SQLMock.ExpectRollback() @@ -446,8 +445,8 @@ func TestBackend_update(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("update resource") - b.ExecWithResult("insert resource_history") + b.ExecWithResult("update resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) expectUnsuccessfulResourceVersionAtomicInc(t, b, errTest) b.SQLMock.ExpectRollback() @@ -462,8 +461,8 @@ func TestBackend_update(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("update resource") - b.ExecWithResult("insert resource_history") + b.ExecWithResult("update resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) expectSuccessfulResourceVersionAtomicInc(t, b) // returns RV=1 b.ExecWithErr("update resource_history", errTest) b.SQLMock.ExpectRollback() @@ -479,10 +478,10 @@ func TestBackend_update(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("update resource") - b.ExecWithResult("insert resource_history") + b.ExecWithResult("update resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) expectSuccessfulResourceVersionAtomicInc(t, b) // returns RV=1 - b.ExecWithResult("update resource_history") + b.ExecWithResult("update resource_history", 0, 1) b.ExecWithErr("update resource", errTest) b.SQLMock.ExpectRollback() @@ -505,15 +504,15 @@ func TestBackend_delete(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("delete resource") - b.ExecWithResult("insert resource_history") - expectSuccessfulResourceVersionAtomicInc(t, b) // returns RV=1 - b.ExecWithResult("update resource_history") + b.ExecWithResult("delete resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) + expectSuccessfulResourceVersionAtomicInc(t, b) + b.ExecWithResult("update resource_history", 0, 1) b.SQLMock.ExpectCommit() v, err := b.delete(ctx, event) require.NoError(t, err) - require.Equal(t, int64(2), v) + require.Equal(t, int64(23456), v) }) t.Run("error deleting resource", func(t *testing.T) { @@ -535,7 +534,7 @@ func TestBackend_delete(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("delete resource") + b.ExecWithResult("delete resource", 0, 1) b.ExecWithErr("insert resource_history", errTest) b.SQLMock.ExpectCommit() @@ -550,8 +549,8 @@ func TestBackend_delete(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("delete resource") - b.ExecWithResult("insert resource_history") + b.ExecWithResult("delete resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) expectUnsuccessfulResourceVersionAtomicInc(t, b, errTest) b.SQLMock.ExpectCommit() @@ -566,8 +565,8 @@ func TestBackend_delete(t *testing.T) { b, ctx := setupBackendTest(t) b.SQLMock.ExpectBegin() - b.ExecWithResult("delete resource") - b.ExecWithResult("insert resource_history") + b.ExecWithResult("delete resource", 0, 1) + b.ExecWithResult("insert resource_history", 0, 1) expectSuccessfulResourceVersionAtomicInc(t, b) // returns RV=1 b.ExecWithErr("update resource_history", errTest) b.SQLMock.ExpectCommit() diff --git a/pkg/storage/unified/sql/data/resource_version_get.sql b/pkg/storage/unified/sql/data/resource_version_get.sql index e63f0bb34de..8228179e50e 100644 --- a/pkg/storage/unified/sql/data/resource_version_get.sql +++ b/pkg/storage/unified/sql/data/resource_version_get.sql @@ -1,5 +1,6 @@ SELECT - {{ .Ident "resource_version" | .Into .ResourceVersion }} + {{ .Ident "resource_version" | .Into .Response.ResourceVersion }}, + {{ .CurrentEpoch | .Into .Response.CurrentEpoch }} FROM {{ .Ident "resource_version" }} WHERE 1 = 1 AND {{ .Ident "group" }} = {{ .Arg .Group }} diff --git a/pkg/storage/unified/sql/data/resource_version_insert.sql b/pkg/storage/unified/sql/data/resource_version_insert.sql index 6c3aab0dcd4..d1ee612d9a0 100644 --- a/pkg/storage/unified/sql/data/resource_version_insert.sql +++ b/pkg/storage/unified/sql/data/resource_version_insert.sql @@ -8,6 +8,6 @@ INSERT INTO {{ .Ident "resource_version" }} VALUES ( {{ .Arg .Group }}, {{ .Arg .Resource }}, - 2 + {{ .CurrentEpoch }} ) ; diff --git a/pkg/storage/unified/sql/data/resource_version_inc.sql b/pkg/storage/unified/sql/data/resource_version_update.sql similarity index 71% rename from pkg/storage/unified/sql/data/resource_version_inc.sql rename to pkg/storage/unified/sql/data/resource_version_update.sql index e7bf52fd1eb..8cfec671aab 100644 --- a/pkg/storage/unified/sql/data/resource_version_inc.sql +++ b/pkg/storage/unified/sql/data/resource_version_update.sql @@ -1,6 +1,6 @@ UPDATE {{ .Ident "resource_version" }} SET - {{ .Ident "resource_version" }} = {{ .Arg .ResourceVersion}} + {{ .Ident "resource_version" }} = {{ .Arg .ResourceVersion }} WHERE 1 = 1 AND {{ .Ident "group" }} = {{ .Arg .Group }} AND {{ .Ident "resource" }} = {{ .Arg .Resource }} diff --git a/pkg/storage/unified/sql/db/dbimpl/dbEngine.go b/pkg/storage/unified/sql/db/dbimpl/dbEngine.go index 3968cfc7839..46392d04cb6 100644 --- a/pkg/storage/unified/sql/db/dbimpl/dbEngine.go +++ b/pkg/storage/unified/sql/db/dbimpl/dbEngine.go @@ -9,11 +9,10 @@ import ( "github.com/go-sql-driver/mysql" "xorm.io/xorm" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/storage/unified/sql/db" ) -func getEngineMySQL(getter confGetter, tracer tracing.Tracer) (*xorm.Engine, error) { +func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { config := mysql.NewConfig() config.User = getter.String("user") // accept the core Grafana jargon of `password` as well, originally Unified @@ -54,8 +53,6 @@ func getEngineMySQL(getter confGetter, tracer tracing.Tracer) (*xorm.Engine, err } // FIXME: get rid of xorm - // TODO figure out why wrapping the db driver with hooks causes mysql errors when writing - //driverName := sqlstore.WrapDatabaseDriverWithHooks(db.DriverMySQL, tracer) engine, err := xorm.NewEngine(db.DriverMySQL, config.FormatDSN()) if err != nil { return nil, fmt.Errorf("open database: %w", err) @@ -68,7 +65,7 @@ func getEngineMySQL(getter confGetter, tracer tracing.Tracer) (*xorm.Engine, err return engine, nil } -func getEnginePostgres(getter confGetter, tracer tracing.Tracer) (*xorm.Engine, error) { +func getEnginePostgres(getter confGetter) (*xorm.Engine, error) { dsnKV := map[string]string{ "user": getter.String("user"), // accept the core Grafana jargon of `password` as well, originally diff --git a/pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go b/pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go index 659d3bd1bb6..583e2bcdc99 100644 --- a/pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go +++ b/pkg/storage/unified/sql/db/dbimpl/dbEngine_test.go @@ -25,14 +25,14 @@ func TestGetEngineMySQLFromConfig(t *testing.T) { t.Run("happy path - with key prefix", func(t *testing.T) { t.Parallel() - engine, err := getEngineMySQL(newValidMySQLGetter(true), nil) + engine, err := getEngineMySQL(newValidMySQLGetter(true)) assert.NotNil(t, engine) assert.NoError(t, err) }) t.Run("happy path - without key prefix", func(t *testing.T) { t.Parallel() - engine, err := getEngineMySQL(newValidMySQLGetter(false), nil) + engine, err := getEngineMySQL(newValidMySQLGetter(false)) assert.NotNil(t, engine) assert.NoError(t, err) }) @@ -47,7 +47,7 @@ func TestGetEngineMySQLFromConfig(t *testing.T) { "db_user": "user", "db_password": "password", }, "db_") - engine, err := getEngineMySQL(getter, nil) + engine, err := getEngineMySQL(getter) assert.Nil(t, engine) assert.Error(t, err) assert.ErrorIs(t, err, errInvalidUTF8Sequence) @@ -73,14 +73,14 @@ func TestGetEnginePostgresFromConfig(t *testing.T) { t.Run("happy path - with key prefix", func(t *testing.T) { t.Parallel() - engine, err := getEnginePostgres(newValidPostgresGetter(true), nil) + engine, err := getEnginePostgres(newValidPostgresGetter(true)) assert.NotNil(t, engine) assert.NoError(t, err) }) t.Run("happy path - without key prefix", func(t *testing.T) { t.Parallel() - engine, err := getEnginePostgres(newValidPostgresGetter(false), nil) + engine, err := getEnginePostgres(newValidPostgresGetter(false)) assert.NotNil(t, engine) assert.NoError(t, err) }) @@ -94,7 +94,7 @@ func TestGetEnginePostgresFromConfig(t *testing.T) { "db_user": "user", "db_password": "password", }, "db_") - engine, err := getEnginePostgres(getter, nil) + engine, err := getEnginePostgres(getter) assert.Nil(t, engine) assert.Error(t, err) @@ -110,7 +110,7 @@ func TestGetEnginePostgresFromConfig(t *testing.T) { "db_user": "user", "db_password": "password", }, "db_") - engine, err := getEnginePostgres(getter, nil) + engine, err := getEnginePostgres(getter) assert.Nil(t, engine) assert.Error(t, err) diff --git a/pkg/storage/unified/sql/db/dbimpl/dbimpl.go b/pkg/storage/unified/sql/db/dbimpl/dbimpl.go index 4756a1f4d56..2264ad3c8a3 100644 --- a/pkg/storage/unified/sql/db/dbimpl/dbimpl.go +++ b/pkg/storage/unified/sql/db/dbimpl/dbimpl.go @@ -9,6 +9,7 @@ import ( "github.com/dlmiddlecote/sqlstats" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/trace" "xorm.io/xorm" infraDB "github.com/grafana/grafana/pkg/infra/db" @@ -63,7 +64,7 @@ type resourceDBProvider struct { logQueries bool } -func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer tracing.Tracer) (p *resourceDBProvider, err error) { +func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer trace.Tracer) (p *resourceDBProvider, err error) { // Resource API has other configs in its section besides database ones, so // we prefix them with "db_". We use the database config from core Grafana // as fallback, and as it uses a dedicated INI section, then keys are not @@ -85,12 +86,12 @@ func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer tracin // specific to Unified Storage case dbType == dbTypePostgres: p.registerMetrics = true - p.engine, err = getEnginePostgres(getter, tracer) + p.engine, err = getEnginePostgres(getter) return p, err case dbType == dbTypeMySQL: p.registerMetrics = true - p.engine, err = getEngineMySQL(getter, tracer) + p.engine, err = getEngineMySQL(getter) return p, err // TODO: add support for SQLite @@ -103,12 +104,12 @@ func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer tracin case grafanaDBType == dbTypePostgres: p.registerMetrics = true - p.engine, err = getEnginePostgres(fallbackGetter, tracer) + p.engine, err = getEnginePostgres(fallbackGetter) return p, err case grafanaDBType == dbTypeMySQL: p.registerMetrics = true - p.engine, err = getEngineMySQL(fallbackGetter, tracer) + p.engine, err = getEngineMySQL(fallbackGetter) return p, err // TODO: add support for SQLite diff --git a/pkg/storage/unified/sql/db/dbimpl/driver_test.go b/pkg/storage/unified/sql/db/dbimpl/driver_test.go new file mode 100644 index 00000000000..b52f70e1197 --- /dev/null +++ b/pkg/storage/unified/sql/db/dbimpl/driver_test.go @@ -0,0 +1,95 @@ +package dbimpl + +import ( + "context" + "database/sql" + "database/sql/driver" + "sync" +) + +var _ driver.Driver = driverWithoutIsolationLevel{} +var _ driver.Driver = driverWithIsolationLevel{} + +const ( + driverWithoutIsolationLevelName = "test driver without isolation levels" + driverWithIsolationLevelName = "test driver with isolation levels" +) + +var registerTestDriversOnce sync.Once + +func registerTestSQLDrivers() { + registerTestDriversOnce.Do(func() { + sql.Register(driverWithoutIsolationLevelName, driverWithoutIsolationLevel{}) + sql.Register(driverWithIsolationLevelName, driverWithIsolationLevel{}) + }) +} + +type ( + // without isolation level + + driverWithoutIsolationLevel struct{} + connWithoutIsolationLevel struct{} + + // with isolation level + + driverWithIsolationLevel struct{} + connWithIsolationLevel struct { + connWithoutIsolationLevel + } + + // common + + testStmt struct{} + testTx struct{} + testResults struct{} + testRows struct{} +) + +// driver.Driver + +func (driverWithoutIsolationLevel) Open(name string) (driver.Conn, error) { + return connWithoutIsolationLevel{}, nil +} + +func (driverWithIsolationLevel) Open(name string) (driver.Conn, error) { + return connWithIsolationLevel{}, nil +} + +// driver.Conn + +func (connWithoutIsolationLevel) Prepare(query string) (driver.Stmt, error) { + return testStmt{}, nil +} +func (connWithoutIsolationLevel) Close() error { + return nil +} +func (connWithoutIsolationLevel) Begin() (driver.Tx, error) { + return testTx{}, nil +} + +func (connWithIsolationLevel) BeginTx(context.Context, driver.TxOptions) (driver.Tx, error) { + return testTx{}, nil +} + +// driver.Stmt + +func (testStmt) Close() error { return nil } +func (testStmt) NumInput() int { return 0 } +func (testStmt) Exec(args []driver.Value) (driver.Result, error) { return testResults{}, nil } +func (testStmt) Query(args []driver.Value) (driver.Rows, error) { return testRows{}, nil } + +// driver.Tx + +func (testTx) Commit() error { return nil } +func (testTx) Rollback() error { return nil } + +// driver.Results + +func (testResults) LastInsertId() (int64, error) { return 1, nil } +func (testResults) RowsAffected() (int64, error) { return 1, nil } + +// driver.Rows + +func (testRows) Columns() []string { return nil } +func (testRows) Close() error { return nil } +func (testRows) Next(dest []driver.Value) error { return nil } diff --git a/pkg/storage/unified/sql/db/dbimpl/regression_incident_2144_test.go b/pkg/storage/unified/sql/db/dbimpl/regression_incident_2144_test.go index 1d0b6ba9b02..ad29caf8cd2 100644 --- a/pkg/storage/unified/sql/db/dbimpl/regression_incident_2144_test.go +++ b/pkg/storage/unified/sql/db/dbimpl/regression_incident_2144_test.go @@ -1,10 +1,7 @@ package dbimpl import ( - "context" "database/sql" - "database/sql/driver" - "sync" "testing" "github.com/stretchr/testify/require" @@ -16,93 +13,6 @@ import ( const noIsolationLevelSupportErrStr = "sql: driver does not support non-" + "default isolation level" -var _ driver.Driver = driverWithoutIsolationLevel{} -var _ driver.Driver = driverWithIsolationLevel{} - -const ( - driverWithoutIsolationLevelName = "test driver without isolation levels" - driverWithIsolationLevelName = "test driver with isolation levels" -) - -var registerTestDriversOnce sync.Once - -func registerTestSQLDrivers() { - registerTestDriversOnce.Do(func() { - sql.Register(driverWithoutIsolationLevelName, driverWithoutIsolationLevel{}) - sql.Register(driverWithIsolationLevelName, driverWithIsolationLevel{}) - }) -} - -type ( - // without isolation level - - driverWithoutIsolationLevel struct{} - connWithoutIsolationLevel struct{} - - // with isolation level - - driverWithIsolationLevel struct{} - connWithIsolationLevel struct { - connWithoutIsolationLevel - } - - // common - - testStmt struct{} - testTx struct{} - testResults struct{} - testRows struct{} -) - -// driver.Driver - -func (driverWithoutIsolationLevel) Open(name string) (driver.Conn, error) { - return connWithoutIsolationLevel{}, nil -} - -func (driverWithIsolationLevel) Open(name string) (driver.Conn, error) { - return connWithIsolationLevel{}, nil -} - -// driver.Conn - -func (connWithoutIsolationLevel) Prepare(query string) (driver.Stmt, error) { - return testStmt{}, nil -} -func (connWithoutIsolationLevel) Close() error { - return nil -} -func (connWithoutIsolationLevel) Begin() (driver.Tx, error) { - return testTx{}, nil -} - -func (connWithIsolationLevel) BeginTx(context.Context, driver.TxOptions) (driver.Tx, error) { - return testTx{}, nil -} - -// driver.Stmt - -func (testStmt) Close() error { return nil } -func (testStmt) NumInput() int { return 0 } -func (testStmt) Exec(args []driver.Value) (driver.Result, error) { return testResults{}, nil } -func (testStmt) Query(args []driver.Value) (driver.Rows, error) { return testRows{}, nil } - -// driver.Tx - -func (testTx) Commit() error { return nil } -func (testTx) Rollback() error { return nil } - -// driver.Results - -func (testResults) LastInsertId() (int64, error) { return 1, nil } -func (testResults) RowsAffected() (int64, error) { return 1, nil } - -// driver.Rows - -func (testRows) Columns() []string { return nil } -func (testRows) Close() error { return nil } -func (testRows) Next(dest []driver.Value) error { return nil } - func TestReproIncident2144IndependentOfGrafanaDB(t *testing.T) { t.Parallel() registerTestSQLDrivers() diff --git a/pkg/storage/unified/sql/db/migrations/resource_mig.go b/pkg/storage/unified/sql/db/migrations/resource_mig.go index 15e13c08a38..fcaddaacc80 100644 --- a/pkg/storage/unified/sql/db/migrations/resource_mig.go +++ b/pkg/storage/unified/sql/db/migrations/resource_mig.go @@ -22,7 +22,7 @@ func initResourceTables(mg *migrator.Migrator) string { {Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, {Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, {Name: "namespace", Type: migrator.DB_NVarchar, Length: 63, Nullable: false}, - {Name: "name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, {Name: "value", Type: migrator.DB_LongText, Nullable: true}, {Name: "action", Type: migrator.DB_Int, Nullable: false}, // 1: create, 2: update, 3: delete @@ -44,7 +44,7 @@ func initResourceTables(mg *migrator.Migrator) string { {Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, {Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, {Name: "namespace", Type: migrator.DB_NVarchar, Length: 63, Nullable: false}, - {Name: "name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, {Name: "value", Type: migrator.DB_LongText, Nullable: true}, {Name: "action", Type: migrator.DB_Int, Nullable: false}, // 1: create, 2: update, 3: delete diff --git a/pkg/storage/unified/sql/db/mocks/DB.go b/pkg/storage/unified/sql/db/mocks/DB.go new file mode 100644 index 00000000000..fa86325a6aa --- /dev/null +++ b/pkg/storage/unified/sql/db/mocks/DB.go @@ -0,0 +1,482 @@ +// Code generated by mockery v2.43.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + db "github.com/grafana/grafana/pkg/storage/unified/sql/db" + mock "github.com/stretchr/testify/mock" + + sql "database/sql" +) + +// DB is an autogenerated mock type for the DB type +type DB struct { + mock.Mock +} + +type DB_Expecter struct { + mock *mock.Mock +} + +func (_m *DB) EXPECT() *DB_Expecter { + return &DB_Expecter{mock: &_m.Mock} +} + +// BeginTx provides a mock function with given fields: _a0, _a1 +func (_m *DB) BeginTx(_a0 context.Context, _a1 *sql.TxOptions) (db.Tx, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for BeginTx") + } + + var r0 db.Tx + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *sql.TxOptions) (db.Tx, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *sql.TxOptions) db.Tx); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(db.Tx) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *sql.TxOptions) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_BeginTx_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BeginTx' +type DB_BeginTx_Call struct { + *mock.Call +} + +// BeginTx is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *sql.TxOptions +func (_e *DB_Expecter) BeginTx(_a0 interface{}, _a1 interface{}) *DB_BeginTx_Call { + return &DB_BeginTx_Call{Call: _e.mock.On("BeginTx", _a0, _a1)} +} + +func (_c *DB_BeginTx_Call) Run(run func(_a0 context.Context, _a1 *sql.TxOptions)) *DB_BeginTx_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*sql.TxOptions)) + }) + return _c +} + +func (_c *DB_BeginTx_Call) Return(_a0 db.Tx, _a1 error) *DB_BeginTx_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_BeginTx_Call) RunAndReturn(run func(context.Context, *sql.TxOptions) (db.Tx, error)) *DB_BeginTx_Call { + _c.Call.Return(run) + return _c +} + +// DriverName provides a mock function with given fields: +func (_m *DB) DriverName() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for DriverName") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// DB_DriverName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DriverName' +type DB_DriverName_Call struct { + *mock.Call +} + +// DriverName is a helper method to define mock.On call +func (_e *DB_Expecter) DriverName() *DB_DriverName_Call { + return &DB_DriverName_Call{Call: _e.mock.On("DriverName")} +} + +func (_c *DB_DriverName_Call) Run(run func()) *DB_DriverName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DB_DriverName_Call) Return(_a0 string) *DB_DriverName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_DriverName_Call) RunAndReturn(run func() string) *DB_DriverName_Call { + _c.Call.Return(run) + return _c +} + +// ExecContext provides a mock function with given fields: ctx, query, args +func (_m *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for ExecContext") + } + + var r0 sql.Result + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) (sql.Result, error)); ok { + return rf(ctx, query, args...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) sql.Result); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(sql.Result) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_ExecContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecContext' +type DB_ExecContext_Call struct { + *mock.Call +} + +// ExecContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *DB_Expecter) ExecContext(ctx interface{}, query interface{}, args ...interface{}) *DB_ExecContext_Call { + return &DB_ExecContext_Call{Call: _e.mock.On("ExecContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *DB_ExecContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *DB_ExecContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *DB_ExecContext_Call) Return(_a0 sql.Result, _a1 error) *DB_ExecContext_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_ExecContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) (sql.Result, error)) *DB_ExecContext_Call { + _c.Call.Return(run) + return _c +} + +// PingContext provides a mock function with given fields: _a0 +func (_m *DB) PingContext(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for PingContext") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_PingContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PingContext' +type DB_PingContext_Call struct { + *mock.Call +} + +// PingContext is a helper method to define mock.On call +// - _a0 context.Context +func (_e *DB_Expecter) PingContext(_a0 interface{}) *DB_PingContext_Call { + return &DB_PingContext_Call{Call: _e.mock.On("PingContext", _a0)} +} + +func (_c *DB_PingContext_Call) Run(run func(_a0 context.Context)) *DB_PingContext_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *DB_PingContext_Call) Return(_a0 error) *DB_PingContext_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_PingContext_Call) RunAndReturn(run func(context.Context) error) *DB_PingContext_Call { + _c.Call.Return(run) + return _c +} + +// QueryContext provides a mock function with given fields: ctx, query, args +func (_m *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for QueryContext") + } + + var r0 *sql.Rows + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) (*sql.Rows, error)); ok { + return rf(ctx, query, args...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) *sql.Rows); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Rows) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_QueryContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryContext' +type DB_QueryContext_Call struct { + *mock.Call +} + +// QueryContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *DB_Expecter) QueryContext(ctx interface{}, query interface{}, args ...interface{}) *DB_QueryContext_Call { + return &DB_QueryContext_Call{Call: _e.mock.On("QueryContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *DB_QueryContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *DB_QueryContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *DB_QueryContext_Call) Return(_a0 *sql.Rows, _a1 error) *DB_QueryContext_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_QueryContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) (*sql.Rows, error)) *DB_QueryContext_Call { + _c.Call.Return(run) + return _c +} + +// QueryRowContext provides a mock function with given fields: ctx, query, args +func (_m *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for QueryRowContext") + } + + var r0 *sql.Row + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) *sql.Row); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Row) + } + } + + return r0 +} + +// DB_QueryRowContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryRowContext' +type DB_QueryRowContext_Call struct { + *mock.Call +} + +// QueryRowContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *DB_Expecter) QueryRowContext(ctx interface{}, query interface{}, args ...interface{}) *DB_QueryRowContext_Call { + return &DB_QueryRowContext_Call{Call: _e.mock.On("QueryRowContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *DB_QueryRowContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *DB_QueryRowContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *DB_QueryRowContext_Call) Return(_a0 *sql.Row) *DB_QueryRowContext_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_QueryRowContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) *sql.Row) *DB_QueryRowContext_Call { + _c.Call.Return(run) + return _c +} + +// Stats provides a mock function with given fields: +func (_m *DB) Stats() sql.DBStats { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Stats") + } + + var r0 sql.DBStats + if rf, ok := ret.Get(0).(func() sql.DBStats); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(sql.DBStats) + } + + return r0 +} + +// DB_Stats_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stats' +type DB_Stats_Call struct { + *mock.Call +} + +// Stats is a helper method to define mock.On call +func (_e *DB_Expecter) Stats() *DB_Stats_Call { + return &DB_Stats_Call{Call: _e.mock.On("Stats")} +} + +func (_c *DB_Stats_Call) Run(run func()) *DB_Stats_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DB_Stats_Call) Return(_a0 sql.DBStats) *DB_Stats_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_Stats_Call) RunAndReturn(run func() sql.DBStats) *DB_Stats_Call { + _c.Call.Return(run) + return _c +} + +// WithTx provides a mock function with given fields: _a0, _a1, _a2 +func (_m *DB) WithTx(_a0 context.Context, _a1 *sql.TxOptions, _a2 func(context.Context, db.Tx) error) error { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for WithTx") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *sql.TxOptions, func(context.Context, db.Tx) error) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_WithTx_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithTx' +type DB_WithTx_Call struct { + *mock.Call +} + +// WithTx is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *sql.TxOptions +// - _a2 func(context.Context , db.Tx) error +func (_e *DB_Expecter) WithTx(_a0 interface{}, _a1 interface{}, _a2 interface{}) *DB_WithTx_Call { + return &DB_WithTx_Call{Call: _e.mock.On("WithTx", _a0, _a1, _a2)} +} + +func (_c *DB_WithTx_Call) Run(run func(_a0 context.Context, _a1 *sql.TxOptions, _a2 func(context.Context, db.Tx) error)) *DB_WithTx_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*sql.TxOptions), args[2].(func(context.Context, db.Tx) error)) + }) + return _c +} + +func (_c *DB_WithTx_Call) Return(_a0 error) *DB_WithTx_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_WithTx_Call) RunAndReturn(run func(context.Context, *sql.TxOptions, func(context.Context, db.Tx) error) error) *DB_WithTx_Call { + _c.Call.Return(run) + return _c +} + +// NewDB creates a new instance of DB. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDB(t interface { + mock.TestingT + Cleanup(func()) +}) *DB { + mock := &DB{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/storage/unified/sql/db/mocks/Tx.go b/pkg/storage/unified/sql/db/mocks/Tx.go new file mode 100644 index 00000000000..2927a125fae --- /dev/null +++ b/pkg/storage/unified/sql/db/mocks/Tx.go @@ -0,0 +1,328 @@ +// Code generated by mockery v2.43.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + sql "database/sql" +) + +// Tx is an autogenerated mock type for the Tx type +type Tx struct { + mock.Mock +} + +type Tx_Expecter struct { + mock *mock.Mock +} + +func (_m *Tx) EXPECT() *Tx_Expecter { + return &Tx_Expecter{mock: &_m.Mock} +} + +// Commit provides a mock function with given fields: +func (_m *Tx) Commit() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Commit") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Tx_Commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Commit' +type Tx_Commit_Call struct { + *mock.Call +} + +// Commit is a helper method to define mock.On call +func (_e *Tx_Expecter) Commit() *Tx_Commit_Call { + return &Tx_Commit_Call{Call: _e.mock.On("Commit")} +} + +func (_c *Tx_Commit_Call) Run(run func()) *Tx_Commit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Tx_Commit_Call) Return(_a0 error) *Tx_Commit_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Tx_Commit_Call) RunAndReturn(run func() error) *Tx_Commit_Call { + _c.Call.Return(run) + return _c +} + +// ExecContext provides a mock function with given fields: ctx, query, args +func (_m *Tx) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for ExecContext") + } + + var r0 sql.Result + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) (sql.Result, error)); ok { + return rf(ctx, query, args...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) sql.Result); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(sql.Result) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Tx_ExecContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecContext' +type Tx_ExecContext_Call struct { + *mock.Call +} + +// ExecContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *Tx_Expecter) ExecContext(ctx interface{}, query interface{}, args ...interface{}) *Tx_ExecContext_Call { + return &Tx_ExecContext_Call{Call: _e.mock.On("ExecContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *Tx_ExecContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *Tx_ExecContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Tx_ExecContext_Call) Return(_a0 sql.Result, _a1 error) *Tx_ExecContext_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Tx_ExecContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) (sql.Result, error)) *Tx_ExecContext_Call { + _c.Call.Return(run) + return _c +} + +// QueryContext provides a mock function with given fields: ctx, query, args +func (_m *Tx) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for QueryContext") + } + + var r0 *sql.Rows + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) (*sql.Rows, error)); ok { + return rf(ctx, query, args...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) *sql.Rows); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Rows) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Tx_QueryContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryContext' +type Tx_QueryContext_Call struct { + *mock.Call +} + +// QueryContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *Tx_Expecter) QueryContext(ctx interface{}, query interface{}, args ...interface{}) *Tx_QueryContext_Call { + return &Tx_QueryContext_Call{Call: _e.mock.On("QueryContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *Tx_QueryContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *Tx_QueryContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Tx_QueryContext_Call) Return(_a0 *sql.Rows, _a1 error) *Tx_QueryContext_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Tx_QueryContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) (*sql.Rows, error)) *Tx_QueryContext_Call { + _c.Call.Return(run) + return _c +} + +// QueryRowContext provides a mock function with given fields: ctx, query, args +func (_m *Tx) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for QueryRowContext") + } + + var r0 *sql.Row + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) *sql.Row); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Row) + } + } + + return r0 +} + +// Tx_QueryRowContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryRowContext' +type Tx_QueryRowContext_Call struct { + *mock.Call +} + +// QueryRowContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *Tx_Expecter) QueryRowContext(ctx interface{}, query interface{}, args ...interface{}) *Tx_QueryRowContext_Call { + return &Tx_QueryRowContext_Call{Call: _e.mock.On("QueryRowContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *Tx_QueryRowContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *Tx_QueryRowContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Tx_QueryRowContext_Call) Return(_a0 *sql.Row) *Tx_QueryRowContext_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Tx_QueryRowContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) *sql.Row) *Tx_QueryRowContext_Call { + _c.Call.Return(run) + return _c +} + +// Rollback provides a mock function with given fields: +func (_m *Tx) Rollback() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Rollback") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Tx_Rollback_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Rollback' +type Tx_Rollback_Call struct { + *mock.Call +} + +// Rollback is a helper method to define mock.On call +func (_e *Tx_Expecter) Rollback() *Tx_Rollback_Call { + return &Tx_Rollback_Call{Call: _e.mock.On("Rollback")} +} + +func (_c *Tx_Rollback_Call) Run(run func()) *Tx_Rollback_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Tx_Rollback_Call) Return(_a0 error) *Tx_Rollback_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Tx_Rollback_Call) RunAndReturn(run func() error) *Tx_Rollback_Call { + _c.Call.Return(run) + return _c +} + +// NewTx creates a new instance of Tx. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTx(t interface { + mock.TestingT + Cleanup(func()) +}) *Tx { + mock := &Tx{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/storage/unified/sql/db/service.go b/pkg/storage/unified/sql/db/service.go index dfd9019789d..09f4c664b67 100755 --- a/pkg/storage/unified/sql/db/service.go +++ b/pkg/storage/unified/sql/db/service.go @@ -5,6 +5,9 @@ import ( "database/sql" ) +//go:generate mockery --with-expecter --name DB +//go:generate mockery --with-expecter --name Tx + const ( DriverPostgres = "postgres" DriverMySQL = "mysql" diff --git a/pkg/storage/unified/sql/queries.go b/pkg/storage/unified/sql/queries.go index dc31b7726c4..999d001a7e9 100644 --- a/pkg/storage/unified/sql/queries.go +++ b/pkg/storage/unified/sql/queries.go @@ -41,7 +41,7 @@ var ( // sqlResourceLabelsInsert = mustTemplate("resource_labels_insert.sql") sqlResourceVersionGet = mustTemplate("resource_version_get.sql") - sqlResourceVersionInc = mustTemplate("resource_version_inc.sql") + sqlResourceVersionUpdate = mustTemplate("resource_version_update.sql") sqlResourceVersionInsert = mustTemplate("resource_version_insert.sql") sqlResourceVersionList = mustTemplate("resource_version_list.sql") ) @@ -191,8 +191,13 @@ func (r sqlResourceUpdateRVRequest) Validate() error { } // resource_version table requests. -type resourceVersion struct { +type resourceVersionResponse struct { ResourceVersion int64 + CurrentEpoch int64 +} + +func (r *resourceVersionResponse) Results() (*resourceVersionResponse, error) { + return r, nil } type groupResourceVersion struct { @@ -200,20 +205,32 @@ type groupResourceVersion struct { ResourceVersion int64 } -func (r *resourceVersion) Results() (*resourceVersion, error) { - return r, nil +type sqlResourceVersionUpsertRequest struct { + sqltemplate.SQLTemplate + Group, Resource string + ResourceVersion int64 } -type sqlResourceVersionRequest struct { +func (r sqlResourceVersionUpsertRequest) Validate() error { + return nil // TODO +} + +type sqlResourceVersionGetRequest struct { sqltemplate.SQLTemplate Group, Resource string ReadOnly bool - *resourceVersion + Response *resourceVersionResponse } -func (r sqlResourceVersionRequest) Validate() error { +func (r sqlResourceVersionGetRequest) Validate() error { return nil // TODO } +func (r sqlResourceVersionGetRequest) Results() (*resourceVersionResponse, error) { + return &resourceVersionResponse{ + ResourceVersion: r.Response.ResourceVersion, + CurrentEpoch: r.Response.CurrentEpoch, + }, nil +} type sqlResourceVersionListRequest struct { sqltemplate.SQLTemplate diff --git a/pkg/storage/unified/sql/queries_test.go b/pkg/storage/unified/sql/queries_test.go index df7ed9167f7..5e2557af8c4 100644 --- a/pkg/storage/unified/sql/queries_test.go +++ b/pkg/storage/unified/sql/queries_test.go @@ -52,7 +52,12 @@ func TestUnifiedStorageQueries(t *testing.T) { Data: &sqlResourceRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), WriteEvent: resource.WriteEvent{ - Key: &resource.ResourceKey{}, + Key: &resource.ResourceKey{ + Namespace: "nn", + Group: "gg", + Resource: "rr", + Name: "name", + }, }, }, }, @@ -63,7 +68,12 @@ func TestUnifiedStorageQueries(t *testing.T) { Data: &sqlResourceReadRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), Request: &resource.ReadRequest{ - Key: &resource.ResourceKey{}, + Key: &resource.ResourceKey{ + Namespace: "nn", + Group: "gg", + Resource: "rr", + Name: "name", + }, }, readResponse: new(readResponse), }, @@ -155,7 +165,12 @@ func TestUnifiedStorageQueries(t *testing.T) { Data: &sqlResourceRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), WriteEvent: resource.WriteEvent{ - Key: &resource.ResourceKey{}, + Key: &resource.ResourceKey{ + Namespace: "nn", + Group: "gg", + Resource: "rr", + Name: "name", + }, PreviousRV: 1234, }, }, @@ -165,22 +180,24 @@ func TestUnifiedStorageQueries(t *testing.T) { sqlResourceVersionGet: { { Name: "single path", - Data: &sqlResourceVersionRequest{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - resourceVersion: new(resourceVersion), - ReadOnly: false, + Data: &sqlResourceVersionGetRequest{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Resource: "resource", + Group: "group", + Response: new(resourceVersionResponse), + ReadOnly: false, }, }, }, - sqlResourceVersionInc: { + sqlResourceVersionUpdate: { { Name: "increment resource version", - Data: &sqlResourceVersionRequest{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - resourceVersion: &resourceVersion{ - ResourceVersion: 123, - }, + Data: &sqlResourceVersionUpsertRequest{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Resource: "resource", + Group: "group", + ResourceVersion: int64(12354), }, }, }, @@ -188,8 +205,9 @@ func TestUnifiedStorageQueries(t *testing.T) { sqlResourceVersionInsert: { { Name: "single path", - Data: &sqlResourceVersionRequest{ - SQLTemplate: mocks.NewTestingSQLTemplate(), + Data: &sqlResourceVersionUpsertRequest{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + ResourceVersion: int64(12354), }, }, }, diff --git a/pkg/storage/unified/sql/sqltemplate/dialect.go b/pkg/storage/unified/sql/sqltemplate/dialect.go index 5806c38b33e..918545fdb25 100644 --- a/pkg/storage/unified/sql/sqltemplate/dialect.go +++ b/pkg/storage/unified/sql/sqltemplate/dialect.go @@ -62,6 +62,9 @@ type Dialect interface { // WHERE id = ? // {{ .SelectFor "Update NoWait" }}; -- will be uppercased SelectFor(...string) (string, error) + + // CurrentEpoch returns the current epoch value for the database in microseconds. + CurrentEpoch() string } // RowLockingClause represents a row-locking clause in a SELECT statement. diff --git a/pkg/storage/unified/sql/sqltemplate/dialect_mysql.go b/pkg/storage/unified/sql/sqltemplate/dialect_mysql.go index 6101667fa51..14fd18e0456 100644 --- a/pkg/storage/unified/sql/sqltemplate/dialect_mysql.go +++ b/pkg/storage/unified/sql/sqltemplate/dialect_mysql.go @@ -33,3 +33,7 @@ func (backtickIdent) Ident(s string) (string, error) { return s }) } + +func (mysql) CurrentEpoch() string { + return "CAST(FLOOR(UNIX_TIMESTAMP(NOW(6)) * 1000000) AS SIGNED)" +} diff --git a/pkg/storage/unified/sql/sqltemplate/dialect_postgresql.go b/pkg/storage/unified/sql/sqltemplate/dialect_postgresql.go index 39e8603ae0b..a0dd76010eb 100644 --- a/pkg/storage/unified/sql/sqltemplate/dialect_postgresql.go +++ b/pkg/storage/unified/sql/sqltemplate/dialect_postgresql.go @@ -35,3 +35,7 @@ func (p postgresql) Ident(s string) (string, error) { return p.standardIdent.Ident(s) } + +func (postgresql) CurrentEpoch() string { + return "(EXTRACT(EPOCH FROM statement_timestamp()) * 1000000)::BIGINT" +} diff --git a/pkg/storage/unified/sql/sqltemplate/dialect_sqlite.go b/pkg/storage/unified/sql/sqltemplate/dialect_sqlite.go index 8a41a8f2c4d..f84ea78c477 100644 --- a/pkg/storage/unified/sql/sqltemplate/dialect_sqlite.go +++ b/pkg/storage/unified/sql/sqltemplate/dialect_sqlite.go @@ -16,3 +16,8 @@ type sqlite struct { argPlaceholderFunc name } + +func (sqlite) CurrentEpoch() string { + // Alternative approaches like `unixepoch('subsecond') * 1000000` returns millisecond precision. + return "CAST((julianday('now') - 2440587.5) * 86400000000.0 AS BIGINT)" +} diff --git a/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go b/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go index d714c54e46d..86cbb32fd5b 100644 --- a/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go +++ b/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go @@ -171,6 +171,51 @@ func (_c *SQLTemplate_ArgPlaceholder_Call) RunAndReturn(run func(int) string) *S return _c } +// CurrentEpoch provides a mock function with given fields: +func (_m *SQLTemplate) CurrentEpoch() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for CurrentEpoch") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// SQLTemplate_CurrentEpoch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentEpoch' +type SQLTemplate_CurrentEpoch_Call struct { + *mock.Call +} + +// CurrentEpoch is a helper method to define mock.On call +func (_e *SQLTemplate_Expecter) CurrentEpoch() *SQLTemplate_CurrentEpoch_Call { + return &SQLTemplate_CurrentEpoch_Call{Call: _e.mock.On("CurrentEpoch")} +} + +func (_c *SQLTemplate_CurrentEpoch_Call) Run(run func()) *SQLTemplate_CurrentEpoch_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SQLTemplate_CurrentEpoch_Call) Return(_a0 string) *SQLTemplate_CurrentEpoch_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SQLTemplate_CurrentEpoch_Call) RunAndReturn(run func() string) *SQLTemplate_CurrentEpoch_Call { + _c.Call.Return(run) + return _c +} + // DialectName provides a mock function with given fields: func (_m *SQLTemplate) DialectName() string { ret := _m.Called() diff --git a/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go b/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go index ea9dd58baf2..50f71da1e28 100644 --- a/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go +++ b/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go @@ -171,6 +171,51 @@ func (_c *WithResults_ArgPlaceholder_Call[T]) RunAndReturn(run func(int) string) return _c } +// CurrentEpoch provides a mock function with given fields: +func (_m *WithResults[T]) CurrentEpoch() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for CurrentEpoch") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// WithResults_CurrentEpoch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentEpoch' +type WithResults_CurrentEpoch_Call[T interface{}] struct { + *mock.Call +} + +// CurrentEpoch is a helper method to define mock.On call +func (_e *WithResults_Expecter[T]) CurrentEpoch() *WithResults_CurrentEpoch_Call[T] { + return &WithResults_CurrentEpoch_Call[T]{Call: _e.mock.On("CurrentEpoch")} +} + +func (_c *WithResults_CurrentEpoch_Call[T]) Run(run func()) *WithResults_CurrentEpoch_Call[T] { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *WithResults_CurrentEpoch_Call[T]) Return(_a0 string) *WithResults_CurrentEpoch_Call[T] { + _c.Call.Return(_a0) + return _c +} + +func (_c *WithResults_CurrentEpoch_Call[T]) RunAndReturn(run func() string) *WithResults_CurrentEpoch_Call[T] { + _c.Call.Return(run) + return _c +} + // DialectName provides a mock function with given fields: func (_m *WithResults[T]) DialectName() string { ret := _m.Called() diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 1ab5b765ef9..ba420ba3518 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -58,7 +58,9 @@ func newServer(t *testing.T) (sql.Backend, resource.ResourceServer) { } func TestIntegrationBackendHappyPath(t *testing.T) { - t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + if infraDB.IsTestDbSQLite() { + t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + } if testing.Short() { t.Skip("skipping integration test") } @@ -76,47 +78,48 @@ func TestIntegrationBackendHappyPath(t *testing.T) { stream, err := backend.WatchWriteEvents(context.Background()) // Using a different context to avoid canceling the stream after the DefaultContextTimeout require.NoError(t, err) + var rv1, rv2, rv3, rv4, rv5 int64 t.Run("Add 3 resources", func(t *testing.T) { - rv, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) + rv1, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) require.NoError(t, err) - require.Equal(t, int64(1), rv) + require.Greater(t, rv1, int64(0)) - rv, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) + rv2, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) require.NoError(t, err) - require.Equal(t, int64(2), rv) + require.Greater(t, rv2, rv1) - rv, err = writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) + rv3, err = writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) require.NoError(t, err) - require.Equal(t, int64(3), rv) + require.Greater(t, rv3, rv2) }) t.Run("Update item2", func(t *testing.T) { - rv, err := writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) + rv4, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) require.NoError(t, err) - require.Equal(t, int64(4), rv) + require.Greater(t, rv4, rv3) }) t.Run("Delete item1", func(t *testing.T) { - rv, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_DELETED) + rv5, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_DELETED) require.NoError(t, err) - require.Equal(t, int64(5), rv) + require.Greater(t, rv5, rv4) }) t.Run("Read latest item 2", func(t *testing.T) { resp := backend.ReadResource(ctx, &resource.ReadRequest{Key: resourceKey("item2")}) require.Nil(t, resp.Error) - require.Equal(t, int64(4), resp.ResourceVersion) + require.Equal(t, rv4, resp.ResourceVersion) require.Equal(t, "item2 MODIFIED", string(resp.Value)) }) t.Run("Read early version of item2", func(t *testing.T) { resp := backend.ReadResource(ctx, &resource.ReadRequest{ Key: resourceKey("item2"), - ResourceVersion: 3, // item2 was created at rv=2 and updated at rv=4 + ResourceVersion: rv3, // item2 was created at rv2 and updated at rv4 }) require.Nil(t, resp.Error) - require.Equal(t, int64(2), resp.ResourceVersion) + require.Equal(t, rv2, resp.ResourceVersion) require.Equal(t, "item2 ADDED", string(resp.Value)) }) @@ -135,38 +138,40 @@ func TestIntegrationBackendHappyPath(t *testing.T) { require.Len(t, resp.Items, 2) require.Equal(t, "item2 MODIFIED", string(resp.Items[0].Value)) require.Equal(t, "item3 ADDED", string(resp.Items[1].Value)) - require.Equal(t, int64(5), resp.ResourceVersion) + require.Equal(t, rv5, resp.ResourceVersion) }) t.Run("Watch events", func(t *testing.T) { event := <-stream require.Equal(t, "item1", event.Key.Name) - require.Equal(t, int64(1), event.ResourceVersion) + require.Equal(t, rv1, event.ResourceVersion) require.Equal(t, resource.WatchEvent_ADDED, event.Type) event = <-stream require.Equal(t, "item2", event.Key.Name) - require.Equal(t, int64(2), event.ResourceVersion) + require.Equal(t, rv2, event.ResourceVersion) require.Equal(t, resource.WatchEvent_ADDED, event.Type) event = <-stream require.Equal(t, "item3", event.Key.Name) - require.Equal(t, int64(3), event.ResourceVersion) + require.Equal(t, rv3, event.ResourceVersion) require.Equal(t, resource.WatchEvent_ADDED, event.Type) event = <-stream require.Equal(t, "item2", event.Key.Name) - require.Equal(t, int64(4), event.ResourceVersion) + require.Equal(t, rv4, event.ResourceVersion) require.Equal(t, resource.WatchEvent_MODIFIED, event.Type) event = <-stream require.Equal(t, "item1", event.Key.Name) - require.Equal(t, int64(5), event.ResourceVersion) + require.Equal(t, rv5, event.ResourceVersion) require.Equal(t, resource.WatchEvent_DELETED, event.Type) }) } func TestIntegrationBackendWatchWriteEventsFromLastest(t *testing.T) { - t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + if infraDB.IsTestDbSQLite() { + t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + } if testing.Short() { t.Skip("skipping integration test") } @@ -189,7 +194,9 @@ func TestIntegrationBackendWatchWriteEventsFromLastest(t *testing.T) { } func TestIntegrationBackendList(t *testing.T) { - t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + if infraDB.IsTestDbSQLite() { + t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + } if testing.Short() { t.Skip("skipping integration test") } @@ -198,14 +205,23 @@ func TestIntegrationBackendList(t *testing.T) { backend, server := newServer(t) // Create a few resources before starting the watch - _, _ = writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) // rv=1 - _, _ = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) // rv=2 - will be modified at rv=6 - _, _ = writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) // rv=3 - will be deleted at rv=7 - _, _ = writeEvent(ctx, backend, "item4", resource.WatchEvent_ADDED) // rv=4 - _, _ = writeEvent(ctx, backend, "item5", resource.WatchEvent_ADDED) // rv=5 - _, _ = writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) // rv=6 - _, _ = writeEvent(ctx, backend, "item3", resource.WatchEvent_DELETED) // rv=7 - _, _ = writeEvent(ctx, backend, "item6", resource.WatchEvent_ADDED) // rv=8 + rv1, _ := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) + require.Greater(t, rv1, int64(0)) + rv2, _ := writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) // rv=2 - will be modified at rv=6 + require.Greater(t, rv2, rv1) + rv3, _ := writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) // rv=3 - will be deleted at rv=7 + require.Greater(t, rv3, rv2) + rv4, _ := writeEvent(ctx, backend, "item4", resource.WatchEvent_ADDED) + require.Greater(t, rv4, rv3) + rv5, _ := writeEvent(ctx, backend, "item5", resource.WatchEvent_ADDED) + require.Greater(t, rv5, rv4) + rv6, _ := writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) + require.Greater(t, rv6, rv5) + rv7, _ := writeEvent(ctx, backend, "item3", resource.WatchEvent_DELETED) + require.Greater(t, rv7, rv6) + rv8, _ := writeEvent(ctx, backend, "item6", resource.WatchEvent_ADDED) + require.Greater(t, rv8, rv7) + t.Run("fetch all latest", func(t *testing.T) { res, err := server.List(ctx, &resource.ListRequest{ Options: &resource.ListOptions{ @@ -246,12 +262,12 @@ func TestIntegrationBackendList(t *testing.T) { require.Equal(t, "item1 ADDED", string(res.Items[0].Value)) require.Equal(t, "item2 MODIFIED", string(res.Items[1].Value)) require.Equal(t, "item4 ADDED", string(res.Items[2].Value)) - require.Equal(t, int64(8), continueToken.ResourceVersion) + require.Equal(t, rv8, continueToken.ResourceVersion) }) t.Run("list at revision", func(t *testing.T) { res, err := server.List(ctx, &resource.ListRequest{ - ResourceVersion: 4, + ResourceVersion: rv4, Options: &resource.ListOptions{ Key: &resource.ResourceKey{ Group: "group", @@ -272,7 +288,7 @@ func TestIntegrationBackendList(t *testing.T) { t.Run("fetch first page at revision with limit", func(t *testing.T) { res, err := server.List(ctx, &resource.ListRequest{ Limit: 3, - ResourceVersion: 7, + ResourceVersion: rv7, Options: &resource.ListOptions{ Key: &resource.ResourceKey{ Group: "group", @@ -291,12 +307,12 @@ func TestIntegrationBackendList(t *testing.T) { continueToken, err := sql.GetContinueToken(res.NextPageToken) require.NoError(t, err) - require.Equal(t, int64(7), continueToken.ResourceVersion) + require.Equal(t, rv7, continueToken.ResourceVersion) }) t.Run("fetch second page at revision", func(t *testing.T) { continueToken := &sql.ContinueToken{ - ResourceVersion: 8, + ResourceVersion: rv8, StartOffset: 2, } res, err := server.List(ctx, &resource.ListRequest{ @@ -318,12 +334,14 @@ func TestIntegrationBackendList(t *testing.T) { continueToken, err = sql.GetContinueToken(res.NextPageToken) require.NoError(t, err) - require.Equal(t, int64(8), continueToken.ResourceVersion) + require.Equal(t, rv8, continueToken.ResourceVersion) require.Equal(t, int64(4), continueToken.StartOffset) }) } func TestClientServer(t *testing.T) { - t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + if infraDB.IsTestDbSQLite() { + t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + } ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) dbstore := infraDB.InitTestDB(t) diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql index d76132ae625..27f85926301 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_insert-insert into resource_history.sql @@ -11,10 +11,10 @@ INSERT INTO `resource_history` ) VALUES ( '', - '', - '', - '', - '', + 'gg', + 'rr', + 'nn', + 'name', 1234, '[]', 'UNKNOWN' diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_read-without_resource_version.sql b/pkg/storage/unified/sql/testdata/mysql--resource_read-without_resource_version.sql index 27530d7792d..10a19336dcd 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_read-without_resource_version.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_read-without_resource_version.sql @@ -3,8 +3,8 @@ SELECT `value` FROM `resource` WHERE 1 = 1 - AND `namespace` = '' - AND `group` = '' - AND `resource` = '' - AND `name` = '' + AND `namespace` = 'nn' + AND `group` = 'gg' + AND `resource` = 'rr' + AND `name` = 'name' ; diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_update-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_update-single path.sql index f3a173f5a03..34582a97d87 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_update-single path.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_update-single path.sql @@ -4,8 +4,8 @@ UPDATE `resource` `value` = '[]', `action` = 'UNKNOWN' WHERE 1 = 1 - AND `group` = '' - AND `resource` = '' - AND `namespace` = '' - AND `name` = '' + AND `group` = 'gg' + AND `resource` = 'rr' + AND `namespace` = 'nn' + AND `name` = 'name' ; diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_version_get-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_version_get-single path.sql index 15678ba3e7b..69d7075bd3d 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_version_get-single path.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_version_get-single path.sql @@ -1,8 +1,9 @@ SELECT - `resource_version` + `resource_version`, + CAST(FLOOR(UNIX_TIMESTAMP(NOW(6)) * 1000000) AS SIGNED) FROM `resource_version` WHERE 1 = 1 - AND `group` = '' - AND `resource` = '' + AND `group` = 'group' + AND `resource` = 'resource' FOR UPDATE ; diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_version_inc-increment resource version.sql b/pkg/storage/unified/sql/testdata/mysql--resource_version_inc-increment resource version.sql deleted file mode 100755 index 2a737b37014..00000000000 --- a/pkg/storage/unified/sql/testdata/mysql--resource_version_inc-increment resource version.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE `resource_version` -SET - `resource_version` = 123 -WHERE 1 = 1 - AND `group` = '' - AND `resource` = '' -; diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql index f99b2b00148..432520706ae 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_version_insert-single path.sql @@ -7,6 +7,6 @@ INSERT INTO `resource_version` VALUES ( '', '', - 2 + CAST(FLOOR(UNIX_TIMESTAMP(NOW(6)) * 1000000) AS SIGNED) ) ; diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_version_update-increment resource version.sql b/pkg/storage/unified/sql/testdata/mysql--resource_version_update-increment resource version.sql new file mode 100755 index 00000000000..610283edb9f --- /dev/null +++ b/pkg/storage/unified/sql/testdata/mysql--resource_version_update-increment resource version.sql @@ -0,0 +1,7 @@ +UPDATE `resource_version` +SET + `resource_version` = 12354 +WHERE 1 = 1 + AND `group` = 'group' + AND `resource` = 'resource' +; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql index a15a8db4b1e..c7db6c75739 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_insert-insert into resource_history.sql @@ -11,10 +11,10 @@ INSERT INTO "resource_history" ) VALUES ( '', - '', - '', - '', - '', + 'gg', + 'rr', + 'nn', + 'name', 1234, '[]', 'UNKNOWN' diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_read-without_resource_version.sql b/pkg/storage/unified/sql/testdata/postgres--resource_read-without_resource_version.sql index e010d010dbc..5b8879b6c6a 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_read-without_resource_version.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_read-without_resource_version.sql @@ -3,8 +3,8 @@ SELECT "value" FROM "resource" WHERE 1 = 1 - AND "namespace" = '' - AND "group" = '' - AND "resource" = '' - AND "name" = '' + AND "namespace" = 'nn' + AND "group" = 'gg' + AND "resource" = 'rr' + AND "name" = 'name' ; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_update-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_update-single path.sql index c0b2f77ce6c..4febc55d530 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_update-single path.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_update-single path.sql @@ -4,8 +4,8 @@ UPDATE "resource" "value" = '[]', "action" = 'UNKNOWN' WHERE 1 = 1 - AND "group" = '' - AND "resource" = '' - AND "namespace" = '' - AND "name" = '' + AND "group" = 'gg' + AND "resource" = 'rr' + AND "namespace" = 'nn' + AND "name" = 'name' ; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_version_get-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_version_get-single path.sql index d6e8f041e5a..bec77d7b02e 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_version_get-single path.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_version_get-single path.sql @@ -1,8 +1,9 @@ SELECT - "resource_version" + "resource_version", + (EXTRACT(EPOCH FROM statement_timestamp()) * 1000000)::BIGINT FROM "resource_version" WHERE 1 = 1 - AND "group" = '' - AND "resource" = '' + AND "group" = 'group' + AND "resource" = 'resource' FOR UPDATE ; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_version_inc-increment resource version.sql b/pkg/storage/unified/sql/testdata/postgres--resource_version_inc-increment resource version.sql deleted file mode 100755 index a6f7c024e25..00000000000 --- a/pkg/storage/unified/sql/testdata/postgres--resource_version_inc-increment resource version.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE "resource_version" -SET - "resource_version" = 123 -WHERE 1 = 1 - AND "group" = '' - AND "resource" = '' -; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql index 14b25955585..42392fee7d0 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_version_insert-single path.sql @@ -7,6 +7,6 @@ INSERT INTO "resource_version" VALUES ( '', '', - 2 + (EXTRACT(EPOCH FROM statement_timestamp()) * 1000000)::BIGINT ) ; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_version_update-increment resource version.sql b/pkg/storage/unified/sql/testdata/postgres--resource_version_update-increment resource version.sql new file mode 100755 index 00000000000..9077af0ba47 --- /dev/null +++ b/pkg/storage/unified/sql/testdata/postgres--resource_version_update-increment resource version.sql @@ -0,0 +1,7 @@ +UPDATE "resource_version" +SET + "resource_version" = 12354 +WHERE 1 = 1 + AND "group" = 'group' + AND "resource" = 'resource' +; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql index a15a8db4b1e..c7db6c75739 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_insert-insert into resource_history.sql @@ -11,10 +11,10 @@ INSERT INTO "resource_history" ) VALUES ( '', - '', - '', - '', - '', + 'gg', + 'rr', + 'nn', + 'name', 1234, '[]', 'UNKNOWN' diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_read-without_resource_version.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_read-without_resource_version.sql index e010d010dbc..5b8879b6c6a 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_read-without_resource_version.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_read-without_resource_version.sql @@ -3,8 +3,8 @@ SELECT "value" FROM "resource" WHERE 1 = 1 - AND "namespace" = '' - AND "group" = '' - AND "resource" = '' - AND "name" = '' + AND "namespace" = 'nn' + AND "group" = 'gg' + AND "resource" = 'rr' + AND "name" = 'name' ; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_update-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_update-single path.sql index c0b2f77ce6c..4febc55d530 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_update-single path.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_update-single path.sql @@ -4,8 +4,8 @@ UPDATE "resource" "value" = '[]', "action" = 'UNKNOWN' WHERE 1 = 1 - AND "group" = '' - AND "resource" = '' - AND "namespace" = '' - AND "name" = '' + AND "group" = 'gg' + AND "resource" = 'rr' + AND "namespace" = 'nn' + AND "name" = 'name' ; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_version_get-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_version_get-single path.sql index 071a12f8677..9443791d642 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_version_get-single path.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_version_get-single path.sql @@ -1,7 +1,8 @@ SELECT - "resource_version" + "resource_version", + CAST((julianday('now') - 2440587.5) * 86400000000.0 AS BIGINT) FROM "resource_version" WHERE 1 = 1 - AND "group" = '' - AND "resource" = '' + AND "group" = 'group' + AND "resource" = 'resource' ; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_version_inc-increment resource version.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_version_inc-increment resource version.sql deleted file mode 100755 index a6f7c024e25..00000000000 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_version_inc-increment resource version.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE "resource_version" -SET - "resource_version" = 123 -WHERE 1 = 1 - AND "group" = '' - AND "resource" = '' -; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql index 14b25955585..e58e7094a3d 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_version_insert-single path.sql @@ -7,6 +7,6 @@ INSERT INTO "resource_version" VALUES ( '', '', - 2 + CAST((julianday('now') - 2440587.5) * 86400000000.0 AS BIGINT) ) ; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_version_update-increment resource version.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_version_update-increment resource version.sql new file mode 100755 index 00000000000..9077af0ba47 --- /dev/null +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_version_update-increment resource version.sql @@ -0,0 +1,7 @@ +UPDATE "resource_version" +SET + "resource_version" = 12354 +WHERE 1 = 1 + AND "group" = 'group' + AND "resource" = 'resource' +; diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index c90bb4d9382..2aa4fcdcf20 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -23,8 +23,10 @@ import ( common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/generated/clientset/versioned" notificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" @@ -870,7 +872,7 @@ func TestIntegrationProvisioning(t *testing.T) { env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) - db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) created, err := adminClient.Create(ctx, &v0alpha1.Receiver{ @@ -1127,7 +1129,7 @@ func TestIntegrationReferentialIntegrity(t *testing.T) { helper := getTestHelper(t) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) - db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) orgID := helper.Org1.Admin.Identity.GetOrgID() @@ -1447,7 +1449,7 @@ func TestIntegrationReceiverListSelector(t *testing.T) { env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) - db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) require.NoError(t, db.SetProvenance(ctx, &definitions.EmbeddedContactPoint{ UID: *recv2.Spec.Integrations[0].Uid, diff --git a/pkg/tests/apis/alerting/notifications/template_group/templates_group_test.go b/pkg/tests/apis/alerting/notifications/template_group/templates_group_test.go index 6a942075285..8c40d6de7df 100644 --- a/pkg/tests/apis/alerting/notifications/template_group/templates_group_test.go +++ b/pkg/tests/apis/alerting/notifications/template_group/templates_group_test.go @@ -13,7 +13,9 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/generated/clientset/versioned" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" @@ -355,7 +357,7 @@ func TestIntegrationProvisioning(t *testing.T) { env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) - db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) created, err := adminClient.Create(ctx, &v0alpha1.TemplateGroup{ @@ -579,7 +581,7 @@ func TestIntegrationListSelector(t *testing.T) { require.NoError(t, err) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) - db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) require.NoError(t, db.SetProvenance(ctx, &definitions.NotificationTemplate{ Name: template2.Spec.Title, diff --git a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go index bccb8214e55..352d6940402 100644 --- a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go +++ b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go @@ -18,7 +18,9 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/generated/clientset/versioned" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" @@ -365,7 +367,7 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) { env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) - db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) created, err := adminClient.Create(ctx, &v0alpha1.TimeInterval{ @@ -593,7 +595,7 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) { require.NoError(t, err) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) - db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) require.NoError(t, db.SetProvenance(ctx, &definitions.MuteTimeInterval{ MuteTimeInterval: config.MuteTimeInterval{ @@ -653,7 +655,7 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) { helper := getTestHelper(t) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) - db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) require.NoError(t, err) orgID := helper.Org1.Admin.Identity.GetOrgID() diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index aea2069d750..6c7afa8e2cc 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -3,6 +3,7 @@ package playlist import ( "context" "encoding/json" + "fmt" "net/http" "slices" @@ -13,10 +14,13 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/grafana/pkg/api/dtos" folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -286,6 +290,24 @@ func TestIntegrationFoldersApp(t *testing.T) { doFolderTests(t, helper) }) + + t.Run("with dual write (unified storage, mode 1, create nested folders)", func(t *testing.T) { + doNestedCreateTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode1, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagNestedFolders, + featuremgmt.FlagKubernetesFolders, + }, + })) + }) } func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper { @@ -408,3 +430,145 @@ func getFromBothAPIs(t *testing.T, } return found } + +func doNestedCreateTest(t *testing.T, helper *apis.K8sTestHelper) { + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + + parentPayload := `{ + "title": "Test/parent", + "uid": "" + }` + parentCreate := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(parentPayload), + }, &folder.Folder{}) + require.NotNil(t, parentCreate.Result) + parentUID := parentCreate.Result.UID + require.NotEmpty(t, parentUID) + + childPayload := fmt.Sprintf(`{ + "title": "Test/child", + "uid": "", + "parentUid": "%s" + }`, parentUID) + childCreate := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(childPayload), + }, &dtos.Folder{}) + require.NotNil(t, childCreate.Result) + childUID := childCreate.Result.UID + require.NotEmpty(t, childUID) + require.Equal(t, "Test/child", childCreate.Result.Title) + require.Equal(t, 1, len(childCreate.Result.Parents)) + + parent := childCreate.Result.Parents[0] + require.Equal(t, parentUID, parent.UID) + require.Equal(t, "Test\\/parent", parent.Title) + require.Equal(t, parentCreate.Result.URL, parent.URL) +} + +func TestIntegrationFolderCreatePermissions(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}" + folderWithParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" + + type testCase struct { + description string + input string + permissions []resourcepermissions.SetResourcePermissionCommand + expectedCode int + } + tcs := []testCase{ + { + description: "creation of folder without parent succeeds given the correct request for creating a folder", + input: folderWithoutParentInput, + expectedCode: http.StatusOK, + permissions: []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{"folders:create"}, + Resource: "folders", + ResourceAttribute: "uid", + ResourceID: "*", + }, + }, + }, + { + description: "creation of folder without parent fails without permissions to create a folder", + input: folderWithoutParentInput, + expectedCode: http.StatusForbidden, + permissions: []resourcepermissions.SetResourcePermissionCommand{}, + }, + { + description: "creation of folder with parent succeeds given the correct request for creating a folder", + input: folderWithParentInput, + expectedCode: http.StatusOK, + permissions: []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{"folders:create"}, + Resource: "folders", + ResourceAttribute: "uid", + ResourceID: "parentuid", + }, + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.description, func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode1, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagNestedFolders, + featuremgmt.FlagKubernetesFolders, + }, + }) + + user := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) + + parentPayload := `{ + "title": "Test/parent", + "uid": "parentuid" + }` + parentCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(parentPayload), + }, &folder.Folder{}) + require.NotNil(t, parentCreate.Result) + parentUID := parentCreate.Result.UID + require.NotEmpty(t, parentUID) + + resp := apis.DoRequest(helper, apis.RequestParams{ + User: user, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(tc.input), + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, resp.Response.StatusCode) + + if tc.expectedCode == http.StatusOK { + require.Equal(t, "uid", resp.Result.UID) + require.Equal(t, "Folder", resp.Result.Title) + } + }) + } +} diff --git a/pkg/tests/apis/query/query_test.go b/pkg/tests/apis/query/query_test.go index 02e63b6ca02..515c2910f15 100644 --- a/pkg/tests/apis/query/query_test.go +++ b/pkg/tests/apis/query/query_test.go @@ -143,9 +143,8 @@ func TestIntegrationSimpleQuery(t *testing.T) { "apiVersion": "v1", "metadata": {}, "status": "Failure", - "message": "did not execute expression [Y] due to a failure to of the dependent expression or query [X]", - "reason": "BadRequest", - "details": { "uid": "sse.dependencyError" }, + "message": "[sse.dependencyError] did not execute expression [Y] due to a failure to of the dependent expression or query [X]", + "reason": "Invalid", "code": 400 }`, string(body)) // require.JSONEq(t, `{ diff --git a/pkg/tests/apis/scopes/scopes_test.go b/pkg/tests/apis/scopes/scopes_test.go index 87e8abb1318..eb380937dff 100644 --- a/pkg/tests/apis/scopes/scopes_test.go +++ b/pkg/tests/apis/scopes/scopes_test.go @@ -3,6 +3,7 @@ package scopes import ( "context" "encoding/json" + "strings" "testing" "github.com/stretchr/testify/require" @@ -159,6 +160,25 @@ func TestIntegrationScopes(t *testing.T) { ) require.NoError(t, err) + // Name length test + scope3 := helper.LoadYAMLOrJSONFile("testdata/example-scope3.yaml") + + // Name too long (>253) + scope3.SetName(strings.Repeat("0", 254)) + _, err = scopeClient.Resource.Create(ctx, + scope3, + createOptions, + ) + require.Error(t, err) + + // Maximum allowed length for name (253) + scope3.SetName(strings.Repeat("0", 253)) + _, err = scopeClient.Resource.Create(ctx, + scope3, + createOptions, + ) + require.NoError(t, err) + // Field Selector test found, err := scopeClient.Resource.List(ctx, metav1.ListOptions{ FieldSelector: "spec.title=foo-scope", diff --git a/pkg/tests/apis/scopes/testdata/example-scope3.yaml b/pkg/tests/apis/scopes/testdata/example-scope3.yaml new file mode 100644 index 00000000000..e1f36c39d87 --- /dev/null +++ b/pkg/tests/apis/scopes/testdata/example-scope3.yaml @@ -0,0 +1,14 @@ +apiVersion: scope.grafana.app/v0alpha1 +kind: Scope +metadata: + name: example-long +spec: + title: baz-scope + description: Longer description for a scope + filters: + - key: aaa + operator: equals + value: eee + - key: ccc + operator: not-equals + value: fff diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index f06ef71f098..6785a0addd8 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -393,7 +393,7 @@ func addDataLinksToFields(query *AzureLogAnalyticsQuery, azurePortalBaseUrl stri } func addTraceDataLinksToFields(query *AzureLogAnalyticsQuery, azurePortalBaseUrl string, frame *data.Frame, dsInfo types.DatasourceInfo) error { - tracesUrl, err := getTracesQueryUrl(query.Resources, azurePortalBaseUrl) + tracesUrl, err := getTracesQueryUrl(azurePortalBaseUrl) if err != nil { return err } @@ -552,20 +552,12 @@ func getQueryUrl(query string, resources []string, azurePortalUrl string, timeRa return portalUrl, nil } -func getTracesQueryUrl(resources []string, azurePortalUrl string) (string, error) { +func getTracesQueryUrl(azurePortalUrl string) (string, error) { portalUrl := azurePortalUrl portalUrl += "/#view/AppInsightsExtension/DetailsV2Blade/ComponentId~/" - resource := struct { - ResourceId string `json:"ResourceId"` - }{ - resources[0], - } - resourceMarshalled, err := json.Marshal(resource) - if err != nil { - return "", fmt.Errorf("failed to marshal application insights resource: %s", err) - } - portalUrl += url.PathEscape(string(resourceMarshalled)) + resource := "%7B%22ResourceId%22:%22${__data.fields.resource:percentencode}%22%7D" + portalUrl += resource portalUrl += "/DataModel~/" // We're making use of data link variables to select the necessary fields in the frontend diff --git a/pkg/tsdb/azuremonitor/loganalytics/traces.go b/pkg/tsdb/azuremonitor/loganalytics/traces.go index e5c2824ca07..e9b7ddf5078 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/traces.go +++ b/pkg/tsdb/azuremonitor/loganalytics/traces.go @@ -114,8 +114,8 @@ func buildTracesQuery(operationId string, parentSpanID *string, traceTypes []str `| extend serviceName = cloud_RoleName` + `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` propertiesQuery := fmt.Sprintf(`| extend tags = %s`, propertiesFunc) - projectClause := `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + projectClause := `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc` return baseQuery + whereClause + parentWhereClause + propertiesStaticQuery + errorProperty + propertiesQuery + filtersClause + projectClause } diff --git a/pkg/tsdb/azuremonitor/loganalytics/traces_test.go b/pkg/tsdb/azuremonitor/loganalytics/traces_test.go index da069c41db4..3e365061c6d 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/traces_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/traces_test.go @@ -135,8 +135,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -150,8 +150,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true trace` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + @@ -163,8 +163,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", @@ -210,8 +210,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -225,8 +225,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true trace` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + @@ -238,8 +238,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", @@ -282,8 +282,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -297,8 +297,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == '${__data.fields.traceID}') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == '${__data.fields.traceID}')` + @@ -310,8 +310,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"${__data.fields.traceID}\"", @@ -357,8 +357,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -372,8 +372,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + @@ -385,8 +385,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", @@ -435,8 +435,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + `| where appId in ("test-app-id")` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -451,8 +451,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + `| where appId in ("test-app-id")` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + @@ -465,8 +465,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + `| where appId in ("test-app-id")` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", @@ -515,8 +515,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + `| where appId !in ("test-app-id")` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -531,8 +531,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + `| where appId !in ("test-app-id")` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + @@ -545,8 +545,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + `| where appId !in ("test-app-id")` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", @@ -595,8 +595,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + `| where appId !in ("test-app-id")| where clientId in ("test-client-id")` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -611,8 +611,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + `| where appId !in ("test-app-id")| where clientId in ("test-client-id")` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,traces` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + @@ -625,8 +625,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + `| where appId !in ("test-app-id")| where clientId in ("test-client-id")` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", @@ -669,8 +669,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -684,8 +684,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests` + `| where (operation_Id != '' and operation_Id == '${__data.fields.traceID}') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == '${__data.fields.traceID}')` + @@ -697,8 +697,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"${__data.fields.traceID}\"", @@ -744,8 +744,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -759,8 +759,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests` + `| where (operation_Id != '' and operation_Id == 'test-op-id') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'test-op-id')` + @@ -772,8 +772,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", @@ -861,8 +861,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"}, TimeRange: timeRange, @@ -875,8 +875,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').requests` + `| where (operation_Id != '' and operation_Id == 'op-id-multi') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'op-id-multi')` + @@ -887,8 +887,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union *,\n" + "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,\n" + @@ -937,8 +937,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"}, TimeRange: timeRange, @@ -951,8 +951,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').requests` + `| where (operation_Id != '' and operation_Id == '${__data.fields.traceID}') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == '${__data.fields.traceID}')` + @@ -963,8 +963,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union *,\n" + "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,\n" + @@ -1016,8 +1016,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"}, TimeRange: timeRange, @@ -1030,8 +1030,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').requests` + `| where (operation_Id != '' and operation_Id == 'op-id-multi') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'op-id-multi')` + @@ -1042,8 +1042,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union *,\n" + "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,\n" + @@ -1095,8 +1095,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"}, TimeRange: timeRange, @@ -1109,8 +1109,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceParentExploreQuery: `set truncationmaxrecords=10000; set truncationmaxsize=67108864; union isfuzzy=true availabilityResults,customEvents,dependencies,exceptions,pageViews,requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').requests,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').availabilityResults,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').customEvents,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').dependencies,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').exceptions,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').pageViews,app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').requests` + `| where (operation_Id != '' and operation_Id == 'op-id-non-overlapping') or (customDimensions.ai_legacyRootId != '' and customDimensions.ai_legacyRootId == 'op-id-non-overlapping')` + @@ -1121,8 +1121,8 @@ func TestBuildAppInsightsQuery(t *testing.T) { `| extend serviceName = cloud_RoleName| extend serviceTags = bag_pack_columns(cloud_RoleInstance, cloud_RoleName)` + `| extend error = todynamic(iff(itemType == "exception", "true", "false"))` + `| extend tags = bag_merge(bag_pack_columns(appId,appName,application_Version,assembly,client_Browser,client_City,client_CountryOrRegion,client_IP,client_Model,client_OS,client_StateOrProvince,client_Type,data,details,duration,error,handledAt,iKey,id,innermostAssembly,innermostMessage,innermostMethod,innermostType,itemCount,itemId,itemType,location,message,method,name,operation_Id,operation_Name,operation_ParentId,operation_SyntheticSource,outerAssembly,outerMessage,outerMethod,outerType,performanceBucket,problemId,resultCode,sdkVersion,session_Id,severityLevel,size,source,success,target,timestamp,type,url,user_AccountId,user_AuthenticatedId,user_Id), customDimensions, customMeasurements)` + - `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp` + - `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId` + + `| project-rename traceID = operation_Id, parentSpanID = operation_ParentId, startTime = timestamp, resource = _ResourceId` + + `| project startTime, itemType, serviceName, duration, traceID, spanID, parentSpanID, operationName, serviceTags, tags, itemId, resource` + `| order by startTime asc`, TraceLogsExploreQuery: "union *,\n" + "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').availabilityResults,\n" + diff --git a/pkg/tsdb/cloud-monitoring/time_series_filter.go b/pkg/tsdb/cloud-monitoring/time_series_filter.go index 5928840f599..baade93989a 100644 --- a/pkg/tsdb/cloud-monitoring/time_series_filter.go +++ b/pkg/tsdb/cloud-monitoring/time_series_filter.go @@ -37,6 +37,9 @@ func parseTimeSeriesResponse(queryRes *backend.DataResponse, "groupBys": groupBys, }, } + // Ensure the time field is named correctly + timeField := frame.Fields[0] + timeField.Name = data.TimeSeriesTimeFieldName var err error frames, err = appendFrames(frames, series, 0, defaultMetricName, seriesLabels, frame, query) diff --git a/pkg/tsdb/cloud-monitoring/time_series_filter_test.go b/pkg/tsdb/cloud-monitoring/time_series_filter_test.go index 031d40a0347..c44949e6c51 100644 --- a/pkg/tsdb/cloud-monitoring/time_series_filter_test.go +++ b/pkg/tsdb/cloud-monitoring/time_series_filter_test.go @@ -11,7 +11,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - sdkdata "github.com/grafana/grafana-plugin-sdk-go/data" + gdata "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/tsdb/cloud-monitoring/kinds/dataquery" "github.com/stretchr/testify/assert" @@ -429,7 +429,7 @@ func TestTimeSeriesFilter(t *testing.T) { frames := res.Frames custom, ok := frames[0].Meta.Custom.(map[string]any) require.True(t, ok) - labels, ok := custom["labels"].(sdkdata.Labels) + labels, ok := custom["labels"].(gdata.Labels) require.True(t, ok) assert.Equal(t, "114250375703598695", labels["resource.label.instance_id"]) }) @@ -459,12 +459,12 @@ func TestTimeSeriesFilter(t *testing.T) { require.NoError(t, (&cloudMonitoringTimeSeriesList{parameters: &dataquery.TimeSeriesList{GroupBys: []string{"test_group_by"}}}).parseResponse(res, data, "test_query", service.logger)) require.NotNil(t, res.Frames[0].Meta) - assert.Equal(t, sdkdata.FrameMeta{ + assert.Equal(t, gdata.FrameMeta{ ExecutedQueryString: "test_query", Custom: map[string]any{ "groupBys": []string{"test_group_by"}, "alignmentPeriod": "", - "labels": sdkdata.Labels{ + "labels": gdata.Labels{ "resource.label.project_id": "grafana-prod", "resource.type": "https_lb_rule", }, @@ -482,12 +482,12 @@ func TestTimeSeriesFilter(t *testing.T) { require.NoError(t, (&cloudMonitoringTimeSeriesList{parameters: &dataquery.TimeSeriesList{GroupBys: []string{"test_group_by"}}}).parseResponse(res, data, "test_query", service.logger)) require.NotNil(t, res.Frames[0].Meta) - assert.Equal(t, sdkdata.FrameMeta{ + assert.Equal(t, gdata.FrameMeta{ ExecutedQueryString: "test_query", Custom: map[string]any{ "groupBys": []string{"test_group_by"}, "alignmentPeriod": "", - "labels": sdkdata.Labels{ + "labels": gdata.Labels{ "resource.label.project_id": "grafana-demo", "resource.type": "global", }, @@ -505,12 +505,12 @@ func TestTimeSeriesFilter(t *testing.T) { require.NoError(t, (&cloudMonitoringTimeSeriesList{parameters: &dataquery.TimeSeriesList{GroupBys: []string{"test_group_by"}}}).parseResponse(res, data, "test_query", service.logger)) require.NotNil(t, res.Frames[0].Meta) - assert.Equal(t, sdkdata.FrameMeta{ + assert.Equal(t, gdata.FrameMeta{ ExecutedQueryString: "test_query", Custom: map[string]any{ "groupBys": []string{"test_group_by"}, "alignmentPeriod": "", - "labels": sdkdata.Labels{ + "labels": gdata.Labels{ "resource.label.project_id": "grafana-prod", "resource.type": "https_lb_rule", }, @@ -544,6 +544,22 @@ func TestTimeSeriesFilter(t *testing.T) { assert.Contains(t, value, `zone=monitoring.regex.full_match("us-central1-a~")`) }) }) + + t.Run("time field is appropriately named", func(t *testing.T) { + res := &backend.DataResponse{} + data, err := loadTestFile("./test-data/4-series-response-distribution-explicit.json") + require.NoError(t, err) + query := &cloudMonitoringTimeSeriesList{ + parameters: &dataquery.TimeSeriesList{ + ProjectName: "test-proj", + }, + aliasBy: "", + } + err = query.parseResponse(res, data, "", service.logger) + require.NoError(t, err) + frames := res.Frames + assert.Equal(t, gdata.TimeSeriesTimeFieldName, frames[0].Fields[0].Name) + }) } func loadTestFile(path string) (cloudMonitoringResponse, error) { diff --git a/pkg/tsdb/cloud-monitoring/time_series_query.go b/pkg/tsdb/cloud-monitoring/time_series_query.go index 13bd03d7d6e..169c151bc24 100644 --- a/pkg/tsdb/cloud-monitoring/time_series_query.go +++ b/pkg/tsdb/cloud-monitoring/time_series_query.go @@ -75,6 +75,9 @@ func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) parseResponse(queryRes *b return err } } + // Ensure the time field is named correctly + timeField := frame.Fields[0] + timeField.Name = data.TimeSeriesTimeFieldName } if len(response.TimeSeriesData) > 0 { dl := timeSeriesQuery.buildDeepLink() diff --git a/pkg/tsdb/cloud-monitoring/time_series_query_test.go b/pkg/tsdb/cloud-monitoring/time_series_query_test.go index 8ddfc183366..4097630f43d 100644 --- a/pkg/tsdb/cloud-monitoring/time_series_query_test.go +++ b/pkg/tsdb/cloud-monitoring/time_series_query_test.go @@ -148,4 +148,26 @@ func TestTimeSeriesQuery(t *testing.T) { query := &cloudMonitoringTimeSeriesQuery{parameters: &dataquery.TimeSeriesQuery{GraphPeriod: strPtr("disabled")}} assert.Equal(t, query.appendGraphPeriod(&backend.QueryDataRequest{Queries: []backend.DataQuery{{}}}), "") }) + + t.Run("time field is appropriately named", func(t *testing.T) { + res := &backend.DataResponse{} + data, err := loadTestFile("./test-data/7-series-response-mql.json") + require.NoError(t, err) + fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) + query := &cloudMonitoringTimeSeriesQuery{ + parameters: &dataquery.TimeSeriesQuery{ + ProjectName: "test-proj", + Query: "test-query", + }, + aliasBy: "", + timeRange: backend.TimeRange{ + From: fromStart, + To: fromStart.Add(34 * time.Minute), + }, + } + err = query.parseResponse(res, data, "", service.logger) + require.NoError(t, err) + frames := res.Frames + assert.Equal(t, gdata.TimeSeriesTimeFieldName, frames[0].Fields[0].Name) + }) } diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index 133deae94d3..e85295e4660 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -18,6 +18,7 @@ import { MegaMenu, MENU_WIDTH } from './MegaMenu/MegaMenu'; import { NavToolbar } from './NavToolbar/NavToolbar'; import { ReturnToPrevious } from './ReturnToPrevious/ReturnToPrevious'; import { SingleTopBar } from './TopBar/SingleTopBar'; +import { SingleTopBarActions } from './TopBar/SingleTopBarActions'; import { TopSearchBar } from './TopBar/TopSearchBar'; import { TOP_BAR_LEVEL_HEIGHT } from './types'; @@ -28,13 +29,15 @@ export function AppChrome({ children }: Props) { const state = chrome.useState(); const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV; const theme = useTheme2(); - const styles = useStyles2(getStyles, searchBarHidden); + const styles = useStyles2(getStyles, searchBarHidden, Boolean(state.actions)); const dockedMenuBreakpoint = theme.breakpoints.values.xl; const dockedMenuLocalStorageState = store.getBool(DOCKED_LOCAL_STORAGE_KEY, true); const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen; const scopesDashboardsState = useScopesDashboardsState(); - const isScopesDashboardsOpen = Boolean(scopesDashboardsState?.isEnabled && scopesDashboardsState?.isPanelOpened); + const isScopesDashboardsOpen = Boolean( + scopesDashboardsState?.isEnabled && scopesDashboardsState?.isPanelOpened && !scopesDashboardsState?.isReadOnly + ); const isSingleTopNav = config.featureToggles.singleTopNav; useMediaQueryChange({ breakpoint: dockedMenuBreakpoint, @@ -97,12 +100,15 @@ export function AppChrome({ children }: Props) { )}
{isSingleTopNav ? ( - + <> + + {state.actions && {state.actions}} + ) : ( <> {!searchBarHidden && } @@ -154,13 +160,13 @@ export function AppChrome({ children }: Props) { ); } -const getStyles = (theme: GrafanaTheme2, searchBarHidden: boolean) => { +const getStyles = (theme: GrafanaTheme2, searchBarHidden: boolean, hasActions: boolean) => { const isSingleTopNav = config.featureToggles.singleTopNav; return { content: css({ display: 'flex', flexDirection: 'column', - paddingTop: isSingleTopNav ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2, + paddingTop: !isSingleTopNav || hasActions ? TOP_BAR_LEVEL_HEIGHT * 2 : TOP_BAR_LEVEL_HEIGHT, flexGrow: 1, height: 'auto', }), diff --git a/public/app/core/components/AppChrome/AppChromeUpdate.tsx b/public/app/core/components/AppChrome/AppChromeUpdate.tsx index ef92b81903a..8daf42dba22 100644 --- a/public/app/core/components/AppChrome/AppChromeUpdate.tsx +++ b/public/app/core/components/AppChrome/AppChromeUpdate.tsx @@ -7,7 +7,7 @@ export interface AppChromeUpdateProps { actions?: React.ReactNode; } /** - * @deprecated This component is deprecated and will be removed in a future release. + * This is the way core pages add actions to the second chrome toolbar */ export const AppChromeUpdate = React.memo(({ actions }: AppChromeUpdateProps) => { const { chrome } = useGrafana(); diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index b2be2f189b1..23aa2b0b65a 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -13,6 +13,8 @@ import { setBookmark } from 'app/core/reducers/navBarTree'; import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index'; import { useDispatch, useSelector } from 'app/types'; +import { TOP_BAR_LEVEL_HEIGHT } from '../types'; + import { MegaMenuHeader } from './MegaMenuHeader'; import { MegaMenuItem } from './MegaMenuItem'; import { usePinnedItems } from './hooks'; @@ -166,41 +168,44 @@ export const MegaMenu = memo( MegaMenu.displayName = 'MegaMenu'; -const getStyles = (theme: GrafanaTheme2) => ({ - content: css({ - display: 'flex', - flexDirection: 'column', - height: '100%', - minHeight: 0, - position: 'relative', - }), - mobileHeader: css({ - display: 'flex', - justifyContent: 'space-between', - padding: theme.spacing(1, 1, 1, 2), - borderBottom: `1px solid ${theme.colors.border.weak}`, +const getStyles = (theme: GrafanaTheme2) => { + const isSingleTopNav = config.featureToggles.singleTopNav; + return { + content: css({ + display: 'flex', + flexDirection: 'column', + height: isSingleTopNav ? `calc(100% - ${TOP_BAR_LEVEL_HEIGHT}px)` : '100%', + minHeight: 0, + position: 'relative', + }), + mobileHeader: css({ + display: 'flex', + justifyContent: 'space-between', + padding: theme.spacing(1, 1, 1, 2), + borderBottom: `1px solid ${theme.colors.border.weak}`, - [theme.breakpoints.up('md')]: { + [theme.breakpoints.up('md')]: { + display: 'none', + }, + }), + itemList: css({ + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'column', + listStyleType: 'none', + padding: theme.spacing(1, 1, 2, 1), + [theme.breakpoints.up('md')]: { + width: MENU_WIDTH, + }, + }), + dockMenuButton: css({ display: 'none', - }, - }), - itemList: css({ - boxSizing: 'border-box', - display: 'flex', - flexDirection: 'column', - listStyleType: 'none', - padding: theme.spacing(1, 1, 2, 1), - [theme.breakpoints.up('md')]: { - width: MENU_WIDTH, - }, - }), - dockMenuButton: css({ - display: 'none', - position: 'relative', - top: theme.spacing(1), + position: 'relative', + top: theme.spacing(1), - [theme.breakpoints.up('xl')]: { - display: 'inline-flex', - }, - }), -}); + [theme.breakpoints.up('xl')]: { + display: 'inline-flex', + }, + }), + }; +}; diff --git a/public/app/core/components/AppChrome/MegaMenu/utils.test.ts b/public/app/core/components/AppChrome/MegaMenu/utils.test.ts index 0a99bffd6b3..ea5a289bd95 100644 --- a/public/app/core/components/AppChrome/MegaMenu/utils.test.ts +++ b/public/app/core/components/AppChrome/MegaMenu/utils.test.ts @@ -5,6 +5,22 @@ import { enrichHelpItem, getActiveItem, findByUrl } from './utils'; const starredDashboardUid = 'foo'; const mockNavTree: NavModelItem[] = [ + { + text: 'Bookmarks', + url: '/bookmarks', + id: 'bookmarks', + children: [ + { + text: 'Item with children', + url: '/itemWithChildren', + id: 'item-with-children', + parentItem: { + text: 'Bookmarks', + id: 'bookmarks', + }, + }, + ], + }, { text: 'Item', url: '/item', @@ -112,6 +128,10 @@ describe('getActiveItem', () => { const mockPage: NavModelItem = { text: 'Some child page', id: 'child', + parentItem: { + text: 'Item with children', + id: 'item-with-children', + }, }; expect(getActiveItem(mockNavTree, mockPage)?.id).toEqual('child'); }); diff --git a/public/app/core/components/AppChrome/MegaMenu/utils.ts b/public/app/core/components/AppChrome/MegaMenu/utils.ts index ab41e68dee6..e8864399a0c 100644 --- a/public/app/core/components/AppChrome/MegaMenu/utils.ts +++ b/public/app/core/components/AppChrome/MegaMenu/utils.ts @@ -98,7 +98,9 @@ export const getActiveItem = ( } } - if (parentItem) { + // Do not search for the parent in the bookmarks section + const isInBookmarksSection = navTree[0]?.parentItem?.id === 'bookmarks'; + if (parentItem && !isInBookmarksSection) { return getActiveItem(navTree, parentItem); } diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx new file mode 100644 index 00000000000..51e5bfad6d3 --- /dev/null +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx @@ -0,0 +1,33 @@ +import { css } from '@emotion/css'; +import { PropsWithChildren } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Components } from '@grafana/e2e-selectors'; +import { Stack, useStyles2 } from '@grafana/ui'; + +import { TOP_BAR_LEVEL_HEIGHT } from '../types'; + +export function SingleTopBarActions({ children }: PropsWithChildren) { + const styles = useStyles2(getStyles); + + return ( +
+ + {children} + +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + actionsBar: css({ + alignItems: 'center', + backgroundColor: theme.colors.background.primary, + borderBottom: `1px solid ${theme.colors.border.weak}`, + display: 'flex', + height: TOP_BAR_LEVEL_HEIGHT, + padding: theme.spacing(0, 1, 0, 2), + }), + }; +}; diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 0c5aab757b5..cd29bbfb556 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -1,58 +1,19 @@ import { css, cx } from '@emotion/css'; -import { - createContext, - Dispatch, - ReactNode, - SetStateAction, - useContext, - useEffect, - useLayoutEffect, - useState, -} from 'react'; +import { useLayoutEffect } from 'react'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; -import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types'; import NativeScrollbar from '../NativeScrollbar'; import { PageContents } from './PageContents'; import { PageHeader } from './PageHeader'; import { PageTabs } from './PageTabs'; -import { PageToolbarActions } from './PageToolbarActions'; import { PageType } from './types'; import { usePageNav } from './usePageNav'; import { usePageTitle } from './usePageTitle'; -export interface PageContextType { - setToolbar: Dispatch>; -} - -export const PageContext = createContext(undefined); - -function usePageContext(): PageContextType { - const context = useContext(PageContext); - if (!context) { - throw new Error('No PageContext found'); - } - return context; -} - -/** - * Hook to dynamically set the toolbar of a Page from a child component. - * Prefer setting the toolbar directly as a prop to Page. - * @param toolbar a ReactNode that will be rendered in a second toolbar - */ -export function usePageToolbar(toolbar?: ReactNode) { - const { setToolbar } = usePageContext(); - useEffect(() => { - setToolbar(toolbar); - return () => setToolbar(undefined); - }, [setToolbar, toolbar]); -} - export const Page: PageType = ({ navId, navModel: oldNavProp, @@ -63,15 +24,12 @@ export const Page: PageType = ({ subTitle, children, className, - toolbar: toolbarProp, info, layout = PageLayoutType.Standard, onSetScrollRef, ...otherProps }) => { - const isSingleTopNav = config.featureToggles.singleTopNav; - const [toolbar, setToolbar] = useState(toolbarProp); - const styles = useStyles2(getStyles, Boolean(isSingleTopNav && toolbar)); + const styles = useStyles2(getStyles); const navModel = usePageNav(navId, oldNavProp); const { chrome } = useGrafana(); @@ -92,58 +50,54 @@ export const Page: PageType = ({ }, [navModel, pageNav, chrome, layout]); return ( - -
- {isSingleTopNav && toolbar && {toolbar}} - {layout === PageLayoutType.Standard && ( - -
- {pageHeaderNav && ( - - )} - {pageNav && pageNav.children && } -
{children}
-
-
- )} +
+ {layout === PageLayoutType.Standard && ( + +
+ {pageHeaderNav && ( + + )} + {pageNav && pageNav.children && } +
{children}
+
+
+ )} - {layout === PageLayoutType.Canvas && ( - -
{children}
-
- )} + {layout === PageLayoutType.Canvas && ( + +
{children}
+
+ )} - {layout === PageLayoutType.Custom && children} -
- + {layout === PageLayoutType.Custom && children} +
); }; Page.Contents = PageContents; -const getStyles = (theme: GrafanaTheme2, hasToolbar: boolean) => { +const getStyles = (theme: GrafanaTheme2) => { return { wrapper: css({ label: 'page-wrapper', display: 'flex', flex: '1 1 0', flexDirection: 'column', - marginTop: hasToolbar ? TOP_BAR_LEVEL_HEIGHT : 0, position: 'relative', }), pageContent: css({ diff --git a/public/app/core/components/Page/PageToolbarActions.tsx b/public/app/core/components/Page/PageToolbarActions.tsx deleted file mode 100644 index 20c5589333f..00000000000 --- a/public/app/core/components/Page/PageToolbarActions.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { css } from '@emotion/css'; -import { PropsWithChildren } from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Components } from '@grafana/e2e-selectors'; -import { useChromeHeaderHeight } from '@grafana/runtime'; -import { Stack, useStyles2 } from '@grafana/ui'; -import { useGrafana } from 'app/core/context/GrafanaContext'; - -import { MENU_WIDTH } from '../AppChrome/MegaMenu/MegaMenu'; -import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types'; - -export interface Props {} - -export function PageToolbarActions({ children }: PropsWithChildren) { - const chromeHeaderHeight = useChromeHeaderHeight(); - const { chrome } = useGrafana(); - const state = chrome.useState(); - const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen; - const styles = useStyles2(getStyles, chromeHeaderHeight ?? 0, menuDockedAndOpen); - - return ( -
- - {children} - -
- ); -} - -const getStyles = (theme: GrafanaTheme2, chromeHeaderHeight: number, menuDockedAndOpen: boolean) => { - return { - pageToolbar: css({ - alignItems: 'center', - backgroundColor: theme.colors.background.primary, - borderBottom: `1px solid ${theme.colors.border.weak}`, - display: 'flex', - height: TOP_BAR_LEVEL_HEIGHT, - left: menuDockedAndOpen ? MENU_WIDTH : 0, - padding: theme.spacing(0, 1, 0, 2), - position: 'fixed', - top: chromeHeaderHeight, - right: 0, - zIndex: theme.zIndex.navbarFixed, - }), - }; -}; diff --git a/public/app/core/components/Page/types.ts b/public/app/core/components/Page/types.ts index eb6bd7dd4d3..f83f00e77f3 100644 --- a/public/app/core/components/Page/types.ts +++ b/public/app/core/components/Page/types.ts @@ -25,8 +25,6 @@ export interface PageProps extends HTMLAttributes { layout?: PageLayoutType; /** Can be used to get the scroll container element to access scroll position */ onSetScrollRef?: (ref: ScrollRefElement) => void; - /** Set a page-level toolbar */ - toolbar?: React.ReactNode; } export interface PageInfoItem { diff --git a/public/app/core/components/TimelineChart/timeline.ts b/public/app/core/components/TimelineChart/timeline.ts index f0b7eec771c..68acea164f6 100644 --- a/public/app/core/components/TimelineChart/timeline.ts +++ b/public/app/core/components/TimelineChart/timeline.ts @@ -321,7 +321,11 @@ export function getConfig(opts: TimelineCoreOptions) { continue; } - let maxChars = Math.floor(boxRect?.w / pxPerChar); + // if x placement is negative, rect is left truncated, remove it from width for calculating how many chars will display + // right truncation happens automatically + const displayedBoxWidth = boxRect.x < 0 ? boxRect?.w + boxRect.x : boxRect?.w; + + let maxChars = Math.floor(displayedBoxWidth / pxPerChar); if (showValue === VisibilityMode.Auto && maxChars < 2) { continue; @@ -333,7 +337,7 @@ export function getConfig(opts: TimelineCoreOptions) { let x = round(boxRect.x + xOff + boxRect.w / 2); if (mode === TimelineMode.Changes) { if (alignValue === 'left') { - x = round(boxRect.x + xOff + strokeWidth + textPadding); + x = round(Math.max(boxRect.x, 0) + xOff + strokeWidth + textPadding); } else if (alignValue === 'right') { x = round(boxRect.x + xOff + boxRect.w - strokeWidth - textPadding); } diff --git a/public/app/core/context/GrafanaContext.ts b/public/app/core/context/GrafanaContext.ts index 424bd71ee68..cf3ed6f1f1c 100644 --- a/public/app/core/context/GrafanaContext.ts +++ b/public/app/core/context/GrafanaContext.ts @@ -4,6 +4,7 @@ import { GrafanaConfig } from '@grafana/data'; import { LocationService, locationService, BackendSrv, config } from '@grafana/runtime'; import { AppChromeService } from '../components/AppChrome/AppChromeService'; +import { TOP_BAR_LEVEL_HEIGHT } from '../components/AppChrome/types'; import { NewFrontendAssetsChecker } from '../services/NewFrontendAssetsChecker'; import { KeybindingSrv } from '../services/keybindingSrv'; @@ -42,17 +43,25 @@ export function useReturnToPreviousInternal() { ); } -const SINGLE_HEADER_BAR_HEIGHT = 40; - export function useChromeHeaderHeight() { const { chrome } = useGrafana(); - const { kioskMode, searchBarHidden, chromeless } = chrome.useState(); + const { actions, kioskMode, searchBarHidden, chromeless } = chrome.useState(); - if (kioskMode || chromeless) { - return 0; - } else if (searchBarHidden || config.featureToggles.singleTopNav) { - return SINGLE_HEADER_BAR_HEIGHT; + if (config.featureToggles.singleTopNav) { + if (kioskMode || chromeless) { + return 0; + } else if (actions) { + return TOP_BAR_LEVEL_HEIGHT * 2; + } else { + return TOP_BAR_LEVEL_HEIGHT; + } } else { - return SINGLE_HEADER_BAR_HEIGHT * 2; + if (kioskMode || chromeless) { + return 0; + } else if (searchBarHidden) { + return TOP_BAR_LEVEL_HEIGHT; + } else { + return TOP_BAR_LEVEL_HEIGHT * 2; + } } } diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 82bac2e5764..75da07f4577 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -1,8 +1,11 @@ -import { DataSourceApi, dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data'; +import { DataSourceApi, dateTime, ExploreUrlState, GrafanaConfig, locationUtil, LogsSortOrder } from '@grafana/data'; import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; +import { config } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { RefreshPicker } from '@grafana/ui'; +import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DEFAULT_RANGE } from 'app/features/explore/state/utils'; +import { getVariablesUrlParams } from 'app/features/variables/getAllVariableValuesForUrl'; import { DatasourceSrvMock, MockDataSourceApi } from '../../../test/mocks/datasource_srv'; @@ -152,6 +155,27 @@ describe('getExploreUrl', () => { expect(interpolateMockLoki).toBeCalled(); expect(interpolateMockProm).toBeCalled(); }); + + describe('subpath', () => { + beforeAll(() => { + locationUtil.initialize({ + config: { appSubUrl: '/subpath' } as GrafanaConfig, + getVariablesUrlParams: jest.fn(), + getTimeRangeForUrl: jest.fn(), + }); + }); + afterAll(() => { + // Reset locationUtil + locationUtil.initialize({ + config, + getTimeRangeForUrl: getTimeSrv().timeRangeForUrl, + getVariablesUrlParams: getVariablesUrlParams, + }); + }); + it('should work with sub path', async () => { + expect(await getExploreUrl(args)).toMatch(/subpath\/explore/g); + }); + }); }); describe('hasNonEmptyQuery', () => { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index af320c4c6ec..cb22e191a77 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -12,6 +12,7 @@ import { DefaultTimeZone, getNextRefId, IntervalValues, + locationUtil, LogsDedupStrategy, LogsSortOrder, rangeUtil, @@ -94,7 +95,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise { const methods = useForm({ defaultValues: emptySettings }); const { control, - formState: { isDirty }, + formState: { isDirty, errors }, getValues, setValue, handleSubmit, @@ -216,7 +216,7 @@ export const LdapSettingsPage = () => { /** * Button's Actions */ - const submitAndEnableLdapSettings = async (payload: LdapPayload) => { + const submitFormAndToggleSettings = async (payload: LdapPayload) => { payload.settings.enabled = !payload.settings.enabled; await putPayload(payload); reportInteraction('authentication_ldap_enabled'); @@ -254,6 +254,11 @@ export const LdapSettingsPage = () => { reportInteraction('authentication_ldap_abandoned'); }; + const isInvalidField = (field: string) => { + const err = errors?.settings?.config?.servers?.[0]; + return typeof err === 'object' && field in err; + }; + const subTitle = ( The LDAP integration in Grafana allows your Grafana users to log in with their LDAP credentials. Find out more in @@ -282,7 +287,7 @@ export const LdapSettingsPage = () => { {config.disableLoginForm && disabledFormAlert} -
+ {isLoading && } {!isLoading && ( @@ -291,7 +296,10 @@ export const LdapSettingsPage = () => { Basic Settings { /> { /> !!value?.length }} name={`${serverConfig}.search_base_dns`} control={control} render={({ field: { onChange, ref, ...field } }) => ( @@ -397,7 +412,7 @@ export const LdapSettingsPage = () => { Disable )} - diff --git a/public/app/features/alerting/.eslintrc b/public/app/features/alerting/.eslintrc index 00287bc0bb8..c34b924496a 100644 --- a/public/app/features/alerting/.eslintrc +++ b/public/app/features/alerting/.eslintrc @@ -12,6 +12,7 @@ "extends": ["plugin:testing-library/react"], "rules": { "testing-library/prefer-user-event": "error", + "jest/expect-expect": ["error", { "assertFunctionNames": ["expect*", "reducerTester"] }], }, }, ], diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index 93101ec6250..09a3da9f551 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -190,6 +190,18 @@ export const trackAlertRuleFormError = ( reportInteraction('grafana_alerting_rule_form_error', props); }; +export const trackNewGrafanaAlertRuleFormSavedSuccess = () => { + reportInteraction('grafana_alerting_grafana_rule_creation_new_success'); +}; + +export const trackNewGrafanaAlertRuleFormCancelled = () => { + reportInteraction('grafana_alerting_grafana_rule_creation_new_aborted'); +}; + +export const trackNewGrafanaAlertRuleFormError = () => { + reportInteraction('grafana_alerting_grafana_rule_creation_new_error'); +}; + export const trackInsightsFeedback = async (props: { useful: boolean; panel: string }) => { const defaults = { grafana_version: config.buildInfo.version, diff --git a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/CloneRuleEditor.test.tsx index fec861fa12c..38155aa917a 100644 --- a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/CloneRuleEditor.test.tsx @@ -5,7 +5,6 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { selectors } from '@grafana/e2e-selectors/src'; import { setDataSourceSrv } from '@grafana/runtime'; -import { PageContext } from 'app/core/components/Page/Page'; import { DashboardSearchItem, DashboardSearchItemType } from 'app/features/search/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; @@ -73,9 +72,7 @@ function Wrapper({ children }: React.PropsWithChildren<{}>) { const formApi = useForm({ defaultValues: getDefaultFormValues() }); return ( - - {children} - + {children} ); } diff --git a/public/app/features/alerting/unified/MuteTimings.test.tsx b/public/app/features/alerting/unified/MuteTimings.test.tsx index c329ddcecab..c583d45599a 100644 --- a/public/app/features/alerting/unified/MuteTimings.test.tsx +++ b/public/app/features/alerting/unified/MuteTimings.test.tsx @@ -158,7 +158,7 @@ const defaultConfigWithBothTimeIntervalsField: AlertManagerCortexConfig = { template_files: {}, }; -const expectedToHaveRedirectedToRoutesRoute = async () => +const expectToHaveRedirectedToRoutesRoute = async () => expect(await screen.findByText(indexPageText)).toBeInTheDocument(); const fillOutForm = async ({ @@ -226,7 +226,7 @@ describe('Mute timings', () => { await saveMuteTiming(); - await expectedToHaveRedirectedToRoutesRoute(); + await expectToHaveRedirectedToRoutesRoute(); const requests = await capture; const alertmanagerUpdate = await getAlertmanagerConfigUpdate(requests); @@ -252,7 +252,7 @@ describe('Mute timings', () => { }); await saveMuteTiming(); - await expectedToHaveRedirectedToRoutesRoute(); + await expectToHaveRedirectedToRoutesRoute(); const requests = await capture; const alertmanagerUpdate = await getAlertmanagerConfigUpdate(requests); @@ -278,7 +278,7 @@ describe('Mute timings', () => { }); await saveMuteTiming(); - await expectedToHaveRedirectedToRoutesRoute(); + await expectToHaveRedirectedToRoutesRoute(); }); it('prepopulates the form when editing a mute timing', async () => { @@ -310,7 +310,7 @@ describe('Mute timings', () => { await fillOutForm(formValues); await saveMuteTiming(); - await expectedToHaveRedirectedToRoutesRoute(); + await expectToHaveRedirectedToRoutesRoute(); const requests = await capture; const alertmanagerUpdate = await getAlertmanagerConfigUpdate(requests); @@ -345,7 +345,7 @@ describe('Mute timings', () => { await fillOutForm({ name: 'Lunch breaks' }); await saveMuteTiming(); - await expectedToHaveRedirectedToRoutesRoute(); + await expectToHaveRedirectedToRoutesRoute(); }); it('shows error when mute timing does not exist', async () => { @@ -367,7 +367,7 @@ describe('Mute timings', () => { await fillOutForm({ name: 'a new mute timing' }); await saveMuteTiming(); - await expectedToHaveRedirectedToRoutesRoute(); + await expectToHaveRedirectedToRoutesRoute(); }); it('shows error when mute timing does not exist', async () => { @@ -384,7 +384,7 @@ describe('Mute timings', () => { }); await saveMuteTiming(); - await expectedToHaveRedirectedToRoutesRoute(); + await expectToHaveRedirectedToRoutesRoute(); }); it('loads view form for provisioned interval', async () => { diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index 6a4d4227e6f..36b18b52ee3 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -19,7 +19,7 @@ import NewReceiverView from './components/receivers/NewReceiverView'; const server = setupMswServer(); -const assertSaveWasSuccessful = async () => { +const expectSaveWasSuccessful = async () => { // TODO: Have a better way to assert that the contact point was saved. This is instead asserting on some // text that's present on the list page, as there's a lot of overlap in text between the form and the list page return waitFor(() => expect(screen.getByText(/search by name or type/i)).toBeInTheDocument(), { timeout: 2000 }); @@ -77,7 +77,7 @@ it('can save a contact point with a select dropdown', async () => { await saveContactPoint(); - await assertSaveWasSuccessful(); + await expectSaveWasSuccessful(); }); it('can save existing Telegram contact point', async () => { @@ -89,5 +89,5 @@ it('can save existing Telegram contact point', async () => { // trigger this error if it regresses await saveContactPoint(); - await assertSaveWasSuccessful(); + await expectSaveWasSuccessful(); }); diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx index be6cb47b1a7..40fd64998b4 100644 --- a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx @@ -118,7 +118,8 @@ describe('RuleEditor grafana managed rules', () => { await clickSelectOption(groupInput, grafanaRulerGroup.name); await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description'); - // save and check what was sent to backend await userEvent.click(ui.buttons.saveAndExit.get()); + + expect(await screen.findByRole('status')).toHaveTextContent('Rule added successfully'); }); }); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index 76d8b271183..fa70dacd4ab 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -3,11 +3,7 @@ import { ComponentProps, ReactNode } from 'react'; import { render, screen, userEvent, waitFor, waitForElementToBeRemoved, within } from 'test/test-utils'; import { selectors } from '@grafana/e2e-selectors'; -import { - flushMicrotasks, - testWithFeatureToggles, - testWithLicenseFeatures, -} from 'app/features/alerting/unified/test/test-utils'; +import { flushMicrotasks, testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; @@ -533,32 +529,20 @@ describe('contact points', () => { ).toBeInTheDocument(); }); - it('does not show manage permissions', async () => { - renderGrafanaContactPoints(); + it('shows manage permissions and allows closing', async () => { + const { user } = renderGrafanaContactPoints(); await clickMoreActionsButton('lotsa-emails'); - expect(screen.queryByRole('menuitem', { name: /manage permissions/i })).not.toBeInTheDocument(); - }); + await user.click(await screen.findByRole('menuitem', { name: /manage permissions/i })); - describe('accesscontrol license feature enabled', () => { - testWithLicenseFeatures(['accesscontrol']); + const permissionsDialog = await screen.findByRole('dialog', { name: /drawer title manage permissions/i }); - it('shows manage permissions and allows closing', async () => { - const { user } = renderGrafanaContactPoints(); + expect(permissionsDialog).toBeInTheDocument(); + expect(await screen.findByRole('table')).toBeInTheDocument(); - await clickMoreActionsButton('lotsa-emails'); - - await user.click(await screen.findByRole('menuitem', { name: /manage permissions/i })); - - const permissionsDialog = await screen.findByRole('dialog', { name: /drawer title manage permissions/i }); - - expect(permissionsDialog).toBeInTheDocument(); - expect(await screen.findByRole('table')).toBeInTheDocument(); - - await user.click(within(permissionsDialog).getAllByRole('button', { name: /close/i })[0]); - expect(permissionsDialog).not.toBeInTheDocument(); - }); + await user.click(within(permissionsDialog).getAllByRole('button', { name: /close/i })[0]); + expect(permissionsDialog).not.toBeInTheDocument(); }); }); }); diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index 0f81ff496a9..5f6deb9c0b0 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -2,7 +2,6 @@ import { difference, groupBy, take, trim, upperFirst } from 'lodash'; import { ReactNode } from 'react'; import { config } from '@grafana/runtime'; -import { contextSrv } from 'app/core/core'; import { canAdminEntity, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; import { AlertManagerCortexConfig, @@ -210,4 +209,4 @@ function getNotifierMetadata(notifiers: NotifierDTO[], receiver: GrafanaManagedR } export const showManageContactPointPermissions = (alertmanager: string, contactPoint: GrafanaManagedContactPoint) => - shouldUseK8sApi(alertmanager) && contextSrv.licensedAccessControlEnabled() && canAdminEntity(contactPoint); + shouldUseK8sApi(alertmanager) && canAdminEntity(contactPoint); diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index 20a0a9561bb..863ca7bd9db 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -4,7 +4,7 @@ import { FC, useCallback, useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { DataFrame, dateTimeFormat, GrafanaTheme2, isTimeSeriesFrames, LoadingState, PanelData } from '@grafana/data'; -import { Alert, AutoSizeInput, Button, clearButtonStyles, IconButton, Stack, useStyles2 } from '@grafana/ui'; +import { Alert, AutoSizeInput, Button, clearButtonStyles, IconButton, Stack, Text, useStyles2 } from '@grafana/ui'; import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions'; import { Math } from 'app/features/expressions/components/Math'; import { Reduce } from 'app/features/expressions/components/Reduce'; @@ -359,6 +359,11 @@ interface FrameProps extends Pick { index: number; } +const OpeningBracket = () => {'{'}; +const ClosingBracket = () => {'}'}; +const Quote = () => {'"'}; +const Equals = () => {'='}; + const FrameRow: FC = ({ frame, index, isAlertCondition }) => { const styles = useStyles2(getStyles); @@ -377,23 +382,26 @@ const FrameRow: FC = ({ frame, index, isAlertCondition }) => {
- {hasLabels ? '' : name} - {hasLabels && ( - <> - {'{'} - {labels.map(([key, value], index) => ( - - {key} - = - " - {value} - " - {index < labels.length - 1 && , } - - ))} - {'}'} - - )} + + {hasLabels ? ( + <> + + {labels.map(([key, value], index) => ( + + {key} + + + {value} + + {index < labels.length - 1 && , } + + ))} + + + ) : ( + {title} + )} +
{value}
{showFiring && } diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx index dfff88a463a..1e3a0af1583 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx @@ -1,10 +1,10 @@ -import { render, renderHook, screen, within } from '@testing-library/react'; +import { renderHook, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { first, noop } from 'lodash'; -import { Router } from 'react-router-dom'; -import { CompatRouter } from 'react-router-dom-v5-compat'; +import { Routes, Route } from 'react-router-dom-v5-compat'; +import { render } from 'test/test-utils'; -import { config, locationService } from '@grafana/runtime'; +import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { AlertmanagerGroup, @@ -345,13 +345,17 @@ describe('Policy', () => { }); }); +// Doesn't matter which path the routes use, it just needs to match the initialEntries history entry to render the element const renderPolicy = (element: JSX.Element) => render( - - - {element} - - + + {element}} /> + , + { + historyOptions: { + initialEntries: ['/'], + }, + } ); const eq = MatcherOperator.equal; diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversSection.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversSection.tsx index a8c9a2591e8..5b3fb521e8c 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversSection.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversSection.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import * as React from 'react'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import { useToggle } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index c306701172b..eeb6890e744 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -1,13 +1,13 @@ import { css, cx } from '@emotion/css'; import { addMinutes, subDays, subHours } from 'date-fns'; import { Location } from 'history'; -import { useMemo, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useToggle } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; -import { config as runtimeConfig, isFetchError, locationService } from '@grafana/runtime'; +import { isFetchError, locationService } from '@grafana/runtime'; import { Alert, Button, @@ -21,7 +21,6 @@ import { InlineField, Box, } from '@grafana/ui'; -import { usePageToolbar } from 'app/core/components/Page/Page'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useCleanup } from 'app/core/hooks/useCleanup'; import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints'; @@ -158,33 +157,28 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) } }; - const actionButtons = useMemo( - () => ( - - - - Cancel - - - ), - [alertmanager, isSubmitting] + const actionButtons = ( + + + + Cancel + + ); - usePageToolbar(actionButtons); - return ( <> - {!runtimeConfig.featureToggles.singleTopNav && } + {/* error message */} {error && ( 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 3a780463010..c7694e69519 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -143,6 +143,7 @@ function ManualAndAutomaticRouting({ alertUid }: { alertUid?: string }) { { switchMode.setAdvancedMode(event.currentTarget.checked); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 8805b40cf1e..4d12179b1fc 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -1,13 +1,12 @@ import { css } from '@emotion/css'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form'; -import { useParams } from 'react-router-dom'; +import { useEffect, useMemo, useState } from 'react'; +import { FormProvider, SubmitErrorHandler, useForm, UseFormWatch } from 'react-hook-form'; +import { useParams } from 'react-router-dom-v5-compat'; import { GrafanaTheme2 } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; -import { usePageToolbar } from 'app/core/components/Page/Page'; import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule'; @@ -20,35 +19,40 @@ import { isGrafanaRulerRulePaused, isRecordingRuleByType, } from 'app/features/alerting/unified/utils/rules'; +import { isExpressionQuery } from 'app/features/expressions/guards'; import { RuleGroupIdentifier, RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting'; import { PostableRuleGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { - LogMessages, logInfo, + LogMessages, trackAlertRuleFormCancelled, trackAlertRuleFormError, trackAlertRuleFormSaved, + trackNewGrafanaAlertRuleFormCancelled, + trackNewGrafanaAlertRuleFormError, + trackNewGrafanaAlertRuleFormSavedSuccess, } from '../../../Analytics'; import { shouldUsePrometheusRulesPrimary } from '../../../featureToggles'; import { useDeleteRuleFromGroup } from '../../../hooks/ruleGroup/useDeleteRuleFromGroup'; import { useAddRuleToRuleGroup, useUpdateRuleInRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup'; import { useURLSearchParams } from '../../../hooks/useURLSearchParams'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; +import { DataSourceType } from '../../../utils/datasource'; import { DEFAULT_GROUP_EVALUATION_INTERVAL, - MANUAL_ROUTING_KEY, - SIMPLIFIED_QUERY_EDITOR_KEY, formValuesFromExistingRule, formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, getDefaultFormValues, getDefaultQueries, ignoreHiddenQueries, + MANUAL_ROUTING_KEY, normalizeDefaultAnnotations, + SIMPLIFIED_QUERY_EDITOR_KEY, } from '../../../utils/rule-form'; -import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier, stringifyIdentifier } from '../../../utils/rule-id'; import * as ruleId from '../../../utils/rule-id'; +import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier, stringifyIdentifier } from '../../../utils/rule-id'; import { createRelativeUrl } from '../../../utils/url'; import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter'; import { AlertRuleNameAndMetric } from '../AlertRuleNameInput'; @@ -82,7 +86,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { const routeParams = useParams<{ type: string; id: string }>(); const ruleType = translateRouteParamToRuleType(routeParams.type); - const uidFromParams = routeParams.id; + const uidFromParams = routeParams.id || ''; const [showDeleteModal, setShowDeleteModal] = useState(false); @@ -134,66 +138,57 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { }; // @todo why is error not propagated to form? - const submit = useCallback( - async (values: RuleFormValues, exitOnSave: boolean) => { - if (conditionErrorMsg !== '') { - notifyApp.error(conditionErrorMsg); - return; + const submit = async (values: RuleFormValues, exitOnSave: boolean) => { + if (conditionErrorMsg !== '') { + notifyApp.error(conditionErrorMsg); + if (!existing && grafanaTypeRule) { + // new Grafana-managed rule + trackNewGrafanaAlertRuleFormError(); } + return; + } - trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); + trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); - const ruleDefinition = grafanaTypeRule - ? formValuesToRulerGrafanaRuleDTO(values) - : formValuesToRulerRuleDTO(values); + const ruleDefinition = grafanaTypeRule ? formValuesToRulerGrafanaRuleDTO(values) : formValuesToRulerRuleDTO(values); - const ruleGroupIdentifier = existing - ? getRuleGroupLocationFromRuleWithLocation(existing) - : getRuleGroupLocationFromFormValues(values); + const ruleGroupIdentifier = existing + ? getRuleGroupLocationFromRuleWithLocation(existing) + : getRuleGroupLocationFromFormValues(values); - // @TODO move this to a hook too to make sure the logic here is tested for regressions? - if (!existing) { - // when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage - storeInLocalStorageValues(values); - await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, evaluateEvery); - } else { - const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); - const targetRuleGroupIdentifier = getRuleGroupLocationFromFormValues(values); - await updateRuleInRuleGroup.execute( - ruleGroupIdentifier, - ruleIdentifier, - ruleDefinition, - targetRuleGroupIdentifier, - evaluateEvery - ); - } + // @TODO move this to a hook too to make sure the logic here is tested for regressions? + if (!existing) { + // when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage + storeInLocalStorageValues(values); + await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, evaluateEvery); + grafanaTypeRule && trackNewGrafanaAlertRuleFormSavedSuccess(); // new Grafana-managed rule + } else { + const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); + const targetRuleGroupIdentifier = getRuleGroupLocationFromFormValues(values); + await updateRuleInRuleGroup.execute( + ruleGroupIdentifier, + ruleIdentifier, + ruleDefinition, + targetRuleGroupIdentifier, + evaluateEvery + ); + } - const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier; - if (exitOnSave) { - const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition); + const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier; + if (exitOnSave) { + const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition); - locationService.push(returnTo); - return; - } + locationService.push(returnTo); + return; + } - // Cloud Ruler rules identifier changes on update due to containing rule name and hash components - // After successful update we need to update the URL to avoid displaying 404 errors - if (isCloudRulerRule(ruleDefinition)) { - const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition); - locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`); - } - }, - [ - addRuleToRuleGroup, - conditionErrorMsg, - evaluateEvery, - existing, - grafanaTypeRule, - notifyApp, - queryParams, - updateRuleInRuleGroup, - ] - ); + // Cloud Ruler rules identifier changes on update due to containing rule name and hash components + // After successful update we need to update the URL to avoid displaying 404 errors + if (isCloudRulerRule(ruleDefinition)) { + const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition); + locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`); + } + }; const deleteRule = async () => { if (existing) { @@ -206,78 +201,77 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { } }; - const onInvalid: SubmitErrorHandler = useCallback( - (errors): void => { - trackAlertRuleFormError({ - grafana_version: config.buildInfo.version, - org_id: contextSrv.user.orgId, - user_id: contextSrv.user.id, - error: Object.keys(errors).toString(), - formAction: existing ? 'update' : 'create', - }); - notifyApp.error('There are errors in the form. Please correct them and try again!'); - }, - [existing, notifyApp] - ); + const onInvalid: SubmitErrorHandler = (errors): void => { + trackAlertRuleFormError({ + grafana_version: config.buildInfo.version, + org_id: contextSrv.user.orgId, + user_id: contextSrv.user.id, + error: Object.keys(errors).toString(), + formAction: existing ? 'update' : 'create', + }); + notifyApp.error('There are errors in the form. Please correct them and try again!'); + }; - const cancelRuleCreation = useCallback(() => { + const cancelRuleCreation = () => { logInfo(LogMessages.cancelSavingAlertRule); trackAlertRuleFormCancelled({ formAction: existing ? 'update' : 'create' }); + if (!existing && grafanaTypeRule) { + // new Grafana-managed rule + trackNewGrafanaAlertRuleFormCancelled(); + } locationService.getHistory().goBack(); - }, [existing]); + }; const evaluateEveryInForm = watch('evaluateEvery'); useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]); - const actionButtons = useMemo( - () => ( - - {existing && ( - - )} + const actionButtons = ( + + {existing && ( - + + {existing ? ( + - {existing ? ( - - ) : null} - {existing && isCortexLokiOrRecordingRule(watch) && ( - - )} - - ), - [cancelRuleCreation, existing, handleSubmit, isSubmitting, onInvalid, styles.buttonSpinner, submit, watch] + ) : null} + {existing && isCortexLokiOrRecordingRule(watch) && ( + + )} + ); - usePageToolbar(actionButtons); const isPaused = existing && isGrafanaRulerRule(existing.rule) && isGrafanaRulerRulePaused(existing.rule); if (!type) { @@ -285,7 +279,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { } return ( - {!config.featureToggles.singleTopNav && } + e.preventDefault()} className={styles.form}>
{isPaused && } @@ -384,14 +378,16 @@ function formValuesFromQueryParams(ruleDefinition: string, type: RuleFormType): }; } - return ignoreHiddenQueries({ - ...getDefaultFormValues(), - ...ruleFromQueryParams, - annotations: normalizeDefaultAnnotations(ruleFromQueryParams.annotations ?? []), - queries: ruleFromQueryParams.queries ?? getDefaultQueries(), - type: type || RuleFormType.grafana, - evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, - }); + return setInstantOrRange( + ignoreHiddenQueries({ + ...getDefaultFormValues(), + ...ruleFromQueryParams, + annotations: normalizeDefaultAnnotations(ruleFromQueryParams.annotations ?? []), + queries: ruleFromQueryParams.queries ?? getDefaultQueries(), + type: type || RuleFormType.grafana, + evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL, + }) + ); } function formValuesFromPrefill(rule: Partial): RuleFormValues { @@ -401,6 +397,31 @@ function formValuesFromPrefill(rule: Partial): RuleFormValues { }); } +function setInstantOrRange(values: RuleFormValues): RuleFormValues { + return { + ...values, + queries: values.queries?.map((query) => { + if (isExpressionQuery(query.model)) { + return query; + } + // data query + const defaultToInstant = + query.model.datasource?.type === DataSourceType.Loki || + query.model.datasource?.type === DataSourceType.Prometheus; + const isInstant = + 'instant' in query.model && query.model.instant !== undefined ? query.model.instant : defaultToInstant; + return { + ...query, + model: { + ...query.model, + instant: isInstant, + range: !isInstant, // we cannot have both instant and range queries in alerting + }, + }; + }), + }; +} + function storeInLocalStorageValues(values: RuleFormValues) { if (values.manualRouting) { localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx index ee890b033bc..79eef183128 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx @@ -2,9 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useAsync } from 'react-use'; -import { config } from '@grafana/runtime'; import { Button, CustomScrollbar, LinkButton, LoadingPlaceholder, Stack } from '@grafana/ui'; -import { usePageToolbar } from 'app/core/components/Page/Page'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; @@ -52,47 +50,39 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor const [conditionErrorMsg, setConditionErrorMsg] = useState(''); const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? DEFAULT_GROUP_EVALUATION_INTERVAL); - const onInvalid = useCallback((): void => { + const onInvalid = (): void => { notifyApp.error('There are errors in the form. Please correct them and try again!'); - }, [notifyApp]); + }; const checkAlertCondition = (msg = '') => { setConditionErrorMsg(msg); }; - const submit = useCallback( - (exportData: RuleFormValues | undefined) => { - if (conditionErrorMsg !== '') { - notifyApp.error(conditionErrorMsg); - return; - } - setExportData(exportData); - }, - [conditionErrorMsg, notifyApp] - ); + const submit = (exportData: RuleFormValues | undefined) => { + if (conditionErrorMsg !== '') { + notifyApp.error(conditionErrorMsg); + return; + } + setExportData(exportData); + }; const onClose = useCallback(() => { setExportData(undefined); }, [setExportData]); - const actionButtons = useMemo( - () => [ - submit(undefined)}> - Cancel - , - , - ], - [formAPI, onInvalid, returnTo, submit] - ); - - usePageToolbar(actionButtons); + const actionButtons = [ + submit(undefined)}> + Cancel + , + , + ]; return ( <> - {!config.featureToggles.singleTopNav && } + e.preventDefault()}>
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx index 70dbaf94fdf..c78fb2df968 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import { Route } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom-v5-compat'; import { ui } from 'test/helpers/alertingRuleEditor'; import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { render, screen, waitForElementToBeRemoved, userEvent } from 'test/test-utils'; @@ -157,7 +157,10 @@ describe('Can create a new grafana managed alert using simplified routing', () = function renderSimplifiedRuleEditor() { return render( - + + } /> + } /> + , { historyOptions: { initialEntries: ['/alerting/new/alerting'] } } ); diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx index 3bfaa4fe6e0..4b2d1e0d7e9 100644 --- a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx @@ -39,7 +39,14 @@ export function LabelsFieldInForm({ onEditClick }: LabelsFieldInFormProps) { ) : ( No labels selected - diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 535f13d37f4..7073b257626 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -34,6 +34,7 @@ import { useDispatch } from 'app/types'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler'; +import { useURLSearchParams } from '../../../hooks/useURLSearchParams'; import { fetchAllPromBuildInfoAction } from '../../../state/actions'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource'; @@ -136,7 +137,9 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P } = useFormContext(); const { queryPreviewData, runQueries, cancelQueries, isPreviewLoading, clearPreviewData } = useAlertQueryRunner(); + const [queryParams] = useURLSearchParams(); const isSwitchModeEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; + const isNewFromQueryParams = queryParams.has('defaults') && !editingExistingRule; const initialState = { queries: getValues('queries'), @@ -165,8 +168,12 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P const isGrafanaAlertingType = isGrafanaAlertingRuleByType(type); const isRecordingRuleType = isCloudRecordingRuleByType(type); const isCloudAlertRuleType = isCloudAlertingRuleByType(type); + const queryParamsAreTransformable = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); - const isAdvancedMode = editorSettings?.simplifiedQueryEditor !== true || !isGrafanaAlertingType; + const isAdvancedMode = + Boolean(editorSettings?.simplifiedQueryEditor) === false || + !isGrafanaAlertingType || + (isNewFromQueryParams && !queryParamsAreTransformable); const [showResetModeModal, setShowResetModal] = useState(false); @@ -718,7 +725,7 @@ function TypeSelectorButton({ onClickType }: { onClickType: (type: ExpressionQue return ( - diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx index 7307675defd..1e745c46da6 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx @@ -218,11 +218,17 @@ export function getSimpleConditionFromExpressions(expressions: Array query.model.type === ExpressionQueryType.threshold && query.refId === SIMPLE_CONDITION_THRESHOLD_ID ); const conditionsFromThreshold = thresholdExpression?.model.conditions ?? []; + const whenField = reduceExpression?.model.reducer ?? ReducerID.last; + const params = conditionsFromThreshold[0]?.evaluator?.params + ? [...conditionsFromThreshold[0]?.evaluator?.params] + : [0]; + const type = conditionsFromThreshold[0]?.evaluator?.type ?? EvalFunction.IsAbove; + return { - whenField: reduceExpression?.model.reducer ?? ReducerID.last, + whenField: whenField, evaluator: { - params: [...conditionsFromThreshold[0]?.evaluator?.params] ?? [0], - type: conditionsFromThreshold[0]?.evaluator?.type ?? EvalFunction.IsAbove, + params: params, + type: type, }, }; } diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx index b1f83b217aa..cc9bd6a8241 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx @@ -2,7 +2,7 @@ import { useFormContext } from 'react-hook-form'; import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceJsonData } from '@grafana/schema'; -import { RadioButtonGroup, Text, Stack } from '@grafana/ui'; +import { RadioButtonGroup, Stack, Text } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { ExpressionDatasourceUID } from 'app/features/expressions/types'; import { AccessControlAction } from 'app/types'; @@ -133,6 +133,7 @@ export function SmartAlertTypeDetector({ disabledOptions={disabledOptions} value={ruleFormType} onChange={onClickSwitch} + data-testid="rule-type-radio-group" /> {/* editing an existing rule, we just show "cannot be changed" */} {editingExistingRule && ( diff --git a/public/app/features/alerting/unified/components/rules/CloneRule.tsx b/public/app/features/alerting/unified/components/rules/CloneRule.tsx index cb0d0cdbc80..4e12142a156 100644 --- a/public/app/features/alerting/unified/components/rules/CloneRule.tsx +++ b/public/app/features/alerting/unified/components/rules/CloneRule.tsx @@ -1,6 +1,5 @@ import { forwardRef, useState } from 'react'; -import { useLocation } from 'react-router-dom'; -import { Navigate } from 'react-router-dom-v5-compat'; +import { Navigate, useLocation } from 'react-router-dom-v5-compat'; import { Button, ConfirmModal } from '@grafana/ui'; import { RuleIdentifier } from 'app/types/unified-alerting'; diff --git a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx index 510e5390379..e58a09f6666 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx +++ b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx @@ -207,10 +207,6 @@ describe('AlertRule abilities', () => { expect(result.current).toMatchSnapshot(); }); - - it('should not allow certain actions for provisioned rules', () => {}); - - it('should not allow certain actions for federated rules', () => {}); }); function createAlertmanagerWrapper(alertmanagerSourceName: string) { diff --git a/public/app/features/alerting/unified/state/AlertmanagerContext.test.tsx b/public/app/features/alerting/unified/state/AlertmanagerContext.test.tsx index 9a4ab2346f4..a6b007f507e 100644 --- a/public/app/features/alerting/unified/state/AlertmanagerContext.test.tsx +++ b/public/app/features/alerting/unified/state/AlertmanagerContext.test.tsx @@ -1,9 +1,7 @@ import { renderHook } from '@testing-library/react'; -import { createMemoryHistory } from 'history'; import * as React from 'react'; -import { MemoryRouter, Router } from 'react-router-dom'; -import { CompatRouter } from 'react-router-dom-v5-compat'; +import { locationService } from '@grafana/runtime'; import store from 'app/core/store'; import { AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; @@ -34,11 +32,7 @@ describe('useAlertmanager', () => { .spyOn(useAlertManagerSources, 'useAlertManagersByPermission') .mockReturnValueOnce({ availableExternalDataSources: [], availableInternalDataSources: [] }); const wrapper = ({ children }: React.PropsWithChildren) => ( - - - {children} - - + {children} ); const { result } = renderHook(() => useAlertmanager(), { wrapper }); @@ -52,11 +46,7 @@ describe('useAlertmanager', () => { }); const wrapper = ({ children }: React.PropsWithChildren) => ( - - - {children} - - + {children} ); const { result } = renderHook(() => useAlertmanager(), { wrapper }); @@ -68,15 +58,10 @@ describe('useAlertmanager', () => { .spyOn(useAlertManagerSources, 'useAlertManagersByPermission') .mockReturnValueOnce({ availableExternalDataSources: [externalAmProm], availableInternalDataSources: [] }); - const history = createMemoryHistory(); - history.push({ search: `alertmanager=${externalAmProm.name}` }); + locationService.push({ search: `alertmanager=${externalAmProm.name}` }); const wrapper = ({ children }: React.PropsWithChildren) => ( - - - {children} - - + {children} ); const { result } = renderHook(() => useAlertmanager(), { wrapper }); @@ -88,15 +73,10 @@ describe('useAlertmanager', () => { .spyOn(useAlertManagerSources, 'useAlertManagersByPermission') .mockReturnValueOnce({ availableExternalDataSources: [], availableInternalDataSources: [] }); - const history = createMemoryHistory(); - history.push({ search: `alertmanager=Not available external AM` }); + locationService.push({ search: `alertmanager=Not available external AM` }); const wrapper = ({ children }: React.PropsWithChildren) => ( - - - {children} - - + {children} ); const { result } = renderHook(() => useAlertmanager(), { wrapper }); @@ -109,15 +89,11 @@ describe('useAlertmanager', () => { .mockReturnValueOnce({ availableExternalDataSources: [externalAmProm], availableInternalDataSources: [] }); const wrapper = ({ children }: React.PropsWithChildren) => ( - - - {children} - - + {children} ); store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmProm.name); - + locationService.push({ search: '' }); const { result } = renderHook(() => useAlertmanager(), { wrapper }); expect(result.current.selectedAlertmanager).toBe(externalAmProm.name); }); @@ -128,15 +104,10 @@ describe('useAlertmanager', () => { availableInternalDataSources: [], }); - const history = createMemoryHistory(); - history.push({ search: `alertmanager=${externalAmProm.name}` }); + locationService.push({ search: `alertmanager=${externalAmProm.name}` }); const wrapper = ({ children }: React.PropsWithChildren) => ( - - - {children} - - + {children} ); store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmMimir.name); diff --git a/public/app/features/alerting/unified/state/AlertmanagerContext.tsx b/public/app/features/alerting/unified/state/AlertmanagerContext.tsx index f2d6e774ce8..d196dd7b565 100644 --- a/public/app/features/alerting/unified/state/AlertmanagerContext.tsx +++ b/public/app/features/alerting/unified/state/AlertmanagerContext.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; +import { locationService } from '@grafana/runtime'; import store from 'app/core/store'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources'; -import { useURLSearchParams } from '../hooks/useURLSearchParams'; import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants'; import { AlertManagerDataSource, @@ -30,7 +30,8 @@ interface Props extends React.PropsWithChildren { } const AlertmanagerProvider = ({ children, accessType, alertmanagerSourceName }: Props) => { - const [queryParams, updateQueryParams] = useURLSearchParams(); + const queryParams = locationService.getSearch(); + const updateQueryParams = locationService.partial; const allAvailableAlertManagers = useAlertManagersByPermission(accessType); const availableAlertManagers = allAvailableAlertManagers.availableInternalDataSources.concat( allAvailableAlertManagers.availableExternalDataSources diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 6d8d2b14e26..cb56a761ab9 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -121,7 +121,7 @@ const BrowseDashboardsPage = memo(() => { onEditTitle={showEditTitle ? onEditTitle : undefined} actions={ <> - {config.featureToggles.dashboardRestore && config.featureToggles.dashboardRestoreUI && hasAdminRights && ( + {config.featureToggles.dashboardRestore && hasAdminRights && ( = {}) { + const props: Props = { + buildPanel: () => ({ id: 1, type: 'table', options: { showHeader: false } }), + onClose: jest.fn(), + options: undefined, + ...overrides, + }; + + const res = render(); + await act(() => Promise.resolve()); + return res; +} + +jest.mock('app/core/services/context_srv'); + +const mocks = { + contextSrv: jest.mocked(contextSrv), +}; + +describe('AddToDashboardButton', () => { + beforeAll(() => { + setEchoSrv(new Echo()); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(backendSrv, 'search').mockResolvedValue([]); + mocks.contextSrv.hasPermission.mockImplementation(() => true); + locationService.push('/'); + }); + + describe('navigation', () => { + it('Navigates to dashboard when clicking on "Open"', async () => { + // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. + const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); + + await setup(); + + await userEvent.click(screen.getByRole('button', { name: /open dashboard$/i })); + + expect(screen.queryByRole('dialog', { name: 'Add panel to dashboard' })).not.toBeInTheDocument(); + + expect(locationService.getLocation().pathname).toBe('/dashboard/new'); + expect(openSpy).not.toHaveBeenCalled(); + }); + + it('Navigates to dashboard in a new tab when clicking on "Open in a new tab"', async () => { + // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. + const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); + + await setup(); + + await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); + + expect(openSpy).toHaveBeenCalledWith(expect.anything(), '_blank'); + expect(locationService.getLocation().pathname).toBe('/'); + }); + }); + + describe('Add to new dashboard', () => { + describe('Navigate to correct dashboard when saving', () => { + it('Navigates to the new dashboard', async () => { + await setup(); + + await userEvent.click(screen.getByRole('button', { name: /open dashboard$/i })); + + expect(screen.queryByRole('dialog', { name: 'Add panel to dashboard' })).not.toBeInTheDocument(); + expect(locationService.getLocation().pathname).toBe('/dashboard/new'); + }); + }); + }); + + describe('Add to existing dashboard', () => { + it('Renders the dashboard picker when switching to "Existing Dashboard"', async () => { + await setup(); + + expect(screen.queryByRole('combobox', { name: /dashboard/ })).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); + expect(screen.getByRole('combobox', { name: /dashboard/ })).toBeInTheDocument(); + }); + + it('Does not submit if no dashboard is selected', async () => { + locationService.push = jest.fn(); + + await setup(); + + await userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); + await userEvent.click(screen.getByRole('button', { name: /open dashboard$/i })); + + locationService.push = jest.fn(); + expect(locationService.push).not.toHaveBeenCalled(); + }); + + describe('Navigate to correct dashboard when saving', () => { + it('Opens the selected dashboard in a new tab', async () => { + // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. + const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); + + jest.spyOn(backendSrv, 'getDashboardByUid').mockResolvedValue({ + dashboard: { ...defaultDashboard, templating: { list: [] }, title: 'Dashboard Title', uid: 'someUid' }, + meta: {}, + }); + + jest.spyOn(backendSrv, 'search').mockResolvedValue([ + { + uid: 'someUid', + isStarred: false, + title: 'Dashboard Title', + tags: [], + type: DashboardSearchItemType.DashDB, + uri: 'someUri', + url: 'someUrl', + }, + ]); + + await setup(); + + await userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); + await userEvent.click(screen.getByRole('combobox', { name: /dashboard/i })); + + await waitFor(async () => { + await screen.findByTestId(selectors.components.Select.option); + }); + + await userEvent.click(screen.getByTestId(selectors.components.Select.option)); + await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); + + await waitFor(async () => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + expect(openSpy).toBeCalledWith('d/someUid', '_blank'); + }); + + it('Navigates to the selected dashboard', async () => { + jest.spyOn(backendSrv, 'search').mockResolvedValue([ + { + uid: 'someUid', + isStarred: false, + title: 'Dashboard Title', + tags: [], + type: DashboardSearchItemType.DashDB, + uri: 'someUri', + url: 'someUrl', + }, + ]); + + await setup(); + + await userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); + await userEvent.click(screen.getByRole('combobox', { name: /dashboard/i })); + + await waitFor(async () => { + await screen.findByTestId(selectors.components.Select.option); + }); + + await userEvent.click(screen.getByTestId(selectors.components.Select.option)); + await userEvent.click(screen.getByRole('button', { name: /open dashboard$/i })); + + await waitFor(async () => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + expect(locationService.getLocation().pathname).toBe('/d/someUid'); + }); + }); + }); +}); + +describe('Permissions', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('Should only show existing dashboard option with no access to create', async () => { + mocks.contextSrv.hasPermission.mockImplementation((action) => { + if (action === 'dashboards:create') { + return false; + } else { + return true; + } + }); + + await setup(); + + expect(screen.queryByRole('radio')).not.toBeInTheDocument(); + }); + + it('Should only show new dashboard option with no access to write', async () => { + mocks.contextSrv.hasPermission.mockImplementation((action) => { + if (action === 'dashboards:write') { + return false; + } else { + return true; + } + }); + + await setup(); + + expect(screen.queryByRole('radio')).not.toBeInTheDocument(); + }); +}); + +describe('Error handling', () => { + beforeEach(() => { + mocks.contextSrv.hasPermission.mockImplementation(() => true); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('Shows an error if opening a new tab fails', async () => { + jest.spyOn(global, 'open').mockReturnValue(null); + const removeDashboardSpy = jest.spyOn(store, 'delete'); + + await setup(); + + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); + + await waitFor(async () => { + expect(await screen.findByRole('alert')).toBeInTheDocument(); + }); + + expect(removeDashboardSpy).toHaveBeenCalled(); + }); + + it('Shows an error if saving to localStorage fails', async () => { + jest.spyOn(store, 'setObject').mockImplementation(() => { + throw 'SOME ERROR'; + }); + + await setup(); + + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); + + await waitFor(async () => { + expect(await screen.findByRole('alert')).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/explore/extensions/AddToDashboard/AddToDashboardForm.tsx b/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx similarity index 61% rename from public/app/features/explore/extensions/AddToDashboard/AddToDashboardForm.tsx rename to public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx index 847a981a7bc..7c000604565 100644 --- a/public/app/features/explore/extensions/AddToDashboard/AddToDashboardForm.tsx +++ b/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx @@ -1,18 +1,16 @@ import { partial } from 'lodash'; -import { type ReactElement, useEffect, useState } from 'react'; -import { DeepMap, FieldError, FieldErrors, useForm, Controller } from 'react-hook-form'; +import { ReactElement, useEffect, useState } from 'react'; +import { Controller, DeepMap, FieldError, FieldErrors, useForm } from 'react-hook-form'; -import { locationUtil, SelectableValue } from '@grafana/data'; -import { config, locationService, reportInteraction } from '@grafana/runtime'; +import { SelectableValue, TimeRange } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import { Panel } from '@grafana/schema'; import { Alert, Button, Field, Modal, RadioButtonGroup } from '@grafana/ui'; import { DashboardPicker } from 'app/core/components/Select/DashboardPicker'; -import { contextSrv } from 'app/core/services/context_srv'; -import { removeDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard'; -import { AccessControlAction, useSelector } from 'app/types'; +import { contextSrv } from 'app/core/core'; +import { AccessControlAction } from 'app/types'; -import { getExploreItemSelector } from '../../state/selectors'; - -import { setDashboardInLocalStorage, AddToDashboardError } from './addToDashboard'; +import { addToDashboard, SubmissionError } from './addToDashboard'; enum SaveTarget { NewDashboard = 'new-dashboard', @@ -22,6 +20,7 @@ enum SaveTarget { interface SaveTargetDTO { saveTarget: SaveTarget; } + interface SaveToNewDashboardDTO extends SaveTargetDTO { saveTarget: SaveTarget.NewDashboard; } @@ -33,36 +32,21 @@ interface SaveToExistingDashboard extends SaveTargetDTO { type FormDTO = SaveToNewDashboardDTO | SaveToExistingDashboard; -function assertIsSaveToExistingDashboardError( - errors: FieldErrors -): asserts errors is DeepMap { - // the shape of the errors object is always compatible with the type above, but we need to - // explicitly assert its type so that TS can narrow down FormDTO to SaveToExistingDashboard - // when we use it in the form. -} - -function getDashboardURL(dashboardUid?: string) { - return dashboardUid ? `d/${dashboardUid}` : 'dashboard/new'; -} - -enum GenericError { - UNKNOWN = 'unknown-error', - NAVIGATION = 'navigation-error', -} - -interface SubmissionError { - error: AddToDashboardError | GenericError; - message: string; -} - -interface Props { +export interface Props { onClose: () => void; - exploreId: string; + buildPanel: (options: TOptions) => Panel; + timeRange?: TimeRange; + options: TOptions; + children?: React.ReactNode; } -export function AddToDashboardForm(props: Props): ReactElement { - const { exploreId, onClose } = props; - const exploreItem = useSelector(getExploreItemSelector(exploreId))!; +export function AddToDashboardForm({ + onClose, + buildPanel, + timeRange, + options, + children, +}: Props): ReactElement { const [submissionError, setSubmissionError] = useState(); const { handleSubmit, @@ -77,12 +61,14 @@ export function AddToDashboardForm(props: Props): ReactElement { const canWriteDashboard = contextSrv.hasPermission(AccessControlAction.DashboardsWrite); const saveTargets: Array> = []; + if (canCreateDashboard) { saveTargets.push({ label: 'New dashboard', value: SaveTarget.NewDashboard, }); } + if (canWriteDashboard) { saveTargets.push({ label: 'Existing dashboard', @@ -92,60 +78,24 @@ export function AddToDashboardForm(props: Props): ReactElement { const saveTarget = saveTargets.length > 1 ? watch('saveTarget') : saveTargets[0].value; - const onSubmit = async (openInNewTab: boolean, data: FormDTO) => { + const onSubmit = (openInNewTab: boolean, data: FormDTO) => { setSubmissionError(undefined); + const dashboardUid = data.saveTarget === SaveTarget.ExistingDashboard ? data.dashboardUid : undefined; + const panel = buildPanel(options); reportInteraction('e_2_d_submit', { newTab: openInNewTab, saveTarget: data.saveTarget, - queries: exploreItem.queries.length, + queries: panel.targets, }); - const { from, to } = exploreItem.range.raw; - - try { - await setDashboardInLocalStorage({ - dashboardUid, - datasource: exploreItem.datasourceInstance?.getRef(), - queries: exploreItem.queries, - queryResponse: exploreItem.queryResponse, - panelState: exploreItem?.panelsState, - time: { - from: typeof from === 'string' ? from : from.toISOString(), - to: typeof to === 'string' ? to : to.toISOString(), - }, - }); - } catch (error) { - switch (error) { - case AddToDashboardError.FETCH_DASHBOARD: - setSubmissionError({ error, message: 'Could not fetch dashboard information. Please try again.' }); - break; - case AddToDashboardError.SET_DASHBOARD_LS: - setSubmissionError({ error, message: 'Could not add panel to dashboard. Please try again.' }); - break; - default: - setSubmissionError({ error: GenericError.UNKNOWN, message: 'Something went wrong. Please try again.' }); - } + const error = addToDashboard({ dashboardUid, panel, openInNewTab, timeRange }); + if (error) { + setSubmissionError(error); return; } - const dashboardURL = getDashboardURL(dashboardUid); - if (!openInNewTab) { - onClose(); - locationService.push(locationUtil.stripBaseFromUrl(dashboardURL)); - return; - } - - const didTabOpen = !!global.open(config.appUrl + dashboardURL, '_blank'); - if (!didTabOpen) { - setSubmissionError({ - error: GenericError.NAVIGATION, - message: 'Could not navigate to the selected dashboard. Please try again.', - }); - removeDashboardToFetchFromLocalStorage(); - return; - } onClose(); }; @@ -155,6 +105,9 @@ export function AddToDashboardForm(props: Props): ReactElement { return ( + {/* For custom form options */} + {children} + {saveTargets.length > 1 && ( ); } + +function assertIsSaveToExistingDashboardError( + errors: FieldErrors +): asserts errors is DeepMap { + // the shape of the errors object is always compatible with the type above, but we need to + // explicitly assert its type so that TS can narrow down FormDTO to SaveToExistingDashboard + // when we use it in the form. +} diff --git a/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts b/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts new file mode 100644 index 00000000000..3847a027e11 --- /dev/null +++ b/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts @@ -0,0 +1,35 @@ +import { SceneTimeRange } from '@grafana/scenes'; +import store from 'app/core/store'; +import { DashboardModel } from 'app/features/dashboard/state'; +import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene'; + +export function addPanelsOnLoadBehavior(scene: DashboardScene) { + const dto = store.getObject(DASHBOARD_FROM_LS_KEY); + + if (dto) { + console.log('asd', dto); + const model = new DashboardModel(dto.dashboard); + + for (const panel of model.panels) { + const gridItem = buildGridItemForPanel(panel); + scene.addPanel(gridItem.state.body); + } + + if (dto.dashboard.time) { + const newTimeRange = new SceneTimeRange({ from: dto.dashboard.time.from, to: dto.dashboard.time.to }); + const timeRange = scene.state.$timeRange; + if (timeRange) { + timeRange.setState({ + value: newTimeRange.state.value, + from: newTimeRange.state.from, + to: newTimeRange.state.to, + }); + } + } + } + + store.delete(DASHBOARD_FROM_LS_KEY); +} diff --git a/public/app/features/dashboard-scene/addToDashboard/addToDashboard.test.ts b/public/app/features/dashboard-scene/addToDashboard/addToDashboard.test.ts new file mode 100644 index 00000000000..3add777f7cd --- /dev/null +++ b/public/app/features/dashboard-scene/addToDashboard/addToDashboard.test.ts @@ -0,0 +1,41 @@ +import { dateTime } from '@grafana/data'; +import store from 'app/core/store'; + +import { addToDashboard } from './addToDashboard'; + +describe('addToDashboard', () => { + let spy: jest.SpyInstance; + + beforeAll(() => { + spy = jest.spyOn(store, 'setObject'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('Should save dashboard with new panel in local storage', () => { + addToDashboard({ + panel: { + type: 'table', + gridPos: { x: 0, y: 0, w: 12, h: 12 }, + options: { showHeader: true }, + }, + }); + + const panel = spy.mock.calls[0][1].dashboard.panels[0]; + expect(panel.type).toEqual('table'); + expect(panel.options).toEqual({ showHeader: true }); + }); + + it('Correct time range is used', () => { + addToDashboard({ + panel: { type: 'table' }, + timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-5m', to: 'now' } }, + }); + + const dashboard = spy.mock.calls[0][1].dashboard; + expect(dashboard.time.from).toEqual('now-5m'); + expect(dashboard.time.to).toEqual('now'); + }); +}); diff --git a/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts b/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts new file mode 100644 index 00000000000..8abd8dc59d0 --- /dev/null +++ b/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts @@ -0,0 +1,85 @@ +import { locationUtil, TimeRange } from '@grafana/data'; +import { config, locationService } from '@grafana/runtime'; +import { Panel } from '@grafana/schema'; +import store from 'app/core/store'; +import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; +import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types'; + +export enum GenericError { + UNKNOWN = 'unknown-error', + NAVIGATION = 'navigation-error', +} + +export interface SubmissionError { + error: AddToDashboardError | GenericError; + message: string; +} + +export enum AddToDashboardError { + FETCH_DASHBOARD = 'fetch-dashboard', + SET_DASHBOARD_LS = 'set-dashboard-ls-error', +} + +interface AddPanelToDashboardOptions { + panel: Panel; + dashboardUid?: string; + openInNewTab?: boolean; + timeRange?: TimeRange; +} + +export function addToDashboard({ + panel, + dashboardUid, + openInNewTab, + timeRange, +}: AddPanelToDashboardOptions): SubmissionError | undefined { + let dto: DashboardDTO = { + meta: {}, + dashboard: { + title: '', + uid: '', + panels: [panel], + schemaVersion: DASHBOARD_SCHEMA_VERSION, + }, + }; + + if (timeRange) { + const raw = timeRange.raw; + dto.dashboard.time = { + from: typeof raw.from === 'string' ? raw.from : raw.from.toISOString(), + to: typeof raw.to === 'string' ? raw.to : raw.to.toISOString(), + }; + } + + try { + store.setObject(DASHBOARD_FROM_LS_KEY, dto); + } catch { + return { + error: AddToDashboardError.SET_DASHBOARD_LS, + message: 'Could not add panel to dashboard. Please try again.', + }; + } + + const dashboardURL = getDashboardURL(dashboardUid); + + if (openInNewTab) { + const didTabOpen = !!global.open(config.appUrl + dashboardURL, '_blank'); + + if (!didTabOpen) { + store.delete(DASHBOARD_FROM_LS_KEY); + return { + error: GenericError.NAVIGATION, + message: 'Could not navigate to the selected dashboard. Please try again.', + }; + } + + return; + } + + locationService.push(locationUtil.stripBaseFromUrl(dashboardURL)); + return; +} + +function getDashboardURL(dashboardUid?: string) { + return dashboardUid ? `d/${dashboardUid}` : 'dashboard/new'; +} diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index 54aa8f75151..26e3c0eb6d0 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -20,8 +20,7 @@ import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import store from 'app/core/store'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; -import { DASHBOARD_FROM_LS_KEY } from 'app/features/dashboard/state/initDashboard'; -import { DashboardRoutes } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; @@ -267,20 +266,6 @@ describe('DashboardScenePage', () => { }); }); - it('is in edit mode when coming from explore to an existing dashboard', async () => { - store.setObject(DASHBOARD_FROM_LS_KEY, { dashboard: simpleDashboard, meta: { slug: '123' } }); - - setup(); - - await waitForDashboardToRender(); - - const panelAMenu = await screen.findByLabelText('Menu for panel with title Panel A'); - expect(panelAMenu).toBeInTheDocument(); - await userEvent.click(panelAMenu); - const editMenuItem = await screen.findAllByText('Edit'); - expect(editMenuItem).toHaveLength(1); - }); - describe('home page', () => { it('should render the dashboard when the route is home', async () => { (useParams as jest.Mock).mockReturnValue({}); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index 59b0f8bd8b5..be740e26c9b 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -1,5 +1,5 @@ // Libraries -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { useParams } from 'react-router-dom-v5-compat'; import { usePrevious } from 'react-use'; @@ -9,10 +9,8 @@ import { Alert, Box } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import store from 'app/core/store'; import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types'; -import { DASHBOARD_FROM_LS_KEY } from 'app/features/dashboard/state/initDashboard'; -import { DashboardDTO, DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types'; import { DashboardPrompt } from '../saving/DashboardPrompt'; @@ -30,12 +28,6 @@ export function DashboardScenePage({ route, queryParams, history }: Props) { // After scene migration is complete and we get rid of old dashboard we should refactor dashboardWatcher so this route reload is not need const routeReloadCounter = (history.location.state as any)?.routeReloadCounter; - // Check if the user is coming from Explore, it's indicated byt the dashboard existence in local storage - const comingFromExplore = useMemo(() => { - return Boolean(store.getObject(DASHBOARD_FROM_LS_KEY)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [uid, slug, type]); - useEffect(() => { if (route.routeName === DashboardRoutes.Normal && type === 'snapshot') { stateManager.loadSnapshot(slug!); @@ -44,7 +36,6 @@ export function DashboardScenePage({ route, queryParams, history }: Props) { uid: uid ?? '', route: route.routeName as DashboardRoutes, urlFolderUid: queryParams.folderUid, - keepDashboardFromExploreInLocalStorage: false, }); } @@ -53,16 +44,6 @@ export function DashboardScenePage({ route, queryParams, history }: Props) { }; }, [stateManager, uid, route.routeName, queryParams.folderUid, routeReloadCounter, slug, type]); - // Effect that handles explore->dashboards workflow - useEffect(() => { - // When coming from explore and adding to an existing dashboard, we should enter edit mode - if (dashboard && comingFromExplore) { - if (route.routeName !== DashboardRoutes.New) { - dashboard.onEnterEditMode(comingFromExplore); - } - } - }, [dashboard, comingFromExplore, route.routeName]); - if (!dashboard) { return ( diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 25509df7fc3..124bc7b27e8 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -2,8 +2,7 @@ import { advanceBy } from 'jest-date-mock'; import { BackendSrv, setBackendSrv } from '@grafana/runtime'; import store from 'app/core/store'; -import { DASHBOARD_FROM_LS_KEY } from 'app/features/dashboard/state/initDashboard'; -import { DashboardRoutes } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types'; import { DashboardScene } from '../scene/DashboardScene'; import { setupLoadDashboardMock } from '../utils/test-utils'; @@ -56,17 +55,6 @@ describe('DashboardScenePageStateManager', () => { expect(loader.state.dashboard).toBeUndefined(); }); - it('shoud fetch dashboard from local storage and remove it after if it exists', async () => { - const loader = new DashboardScenePageStateManager({}); - const localStorageDashboard = { uid: 'fake-dash' }; - store.setObject(DASHBOARD_FROM_LS_KEY, localStorageDashboard); - - const result = await loader.fetchDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); - - expect(result).toEqual(localStorageDashboard); - expect(store.getObject(DASHBOARD_FROM_LS_KEY)).toBeUndefined(); - }); - it('should initialize the dashboard scene with the loaded dashboard', async () => { setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} }); @@ -215,36 +203,5 @@ describe('DashboardScenePageStateManager', () => { expect(loadDashSpy).toHaveBeenCalledTimes(2); }); }); - - describe('When coming from explore', () => { - it('shoud fetch dashboard from local storage and keep it there after when asked', async () => { - const loader = new DashboardScenePageStateManager({}); - const localStorageDashboard = { uid: 'fake-dash' }; - store.setObject(DASHBOARD_FROM_LS_KEY, { dashboard: localStorageDashboard }); - - const result = await loader.fetchDashboard({ - uid: 'fake-dash', - route: DashboardRoutes.Normal, - keepDashboardFromExploreInLocalStorage: true, - }); - - expect(result).toEqual({ dashboard: localStorageDashboard }); - expect(store.getObject(DASHBOARD_FROM_LS_KEY)).toEqual({ dashboard: localStorageDashboard }); - }); - - it('shoud not store dashboard in cache when coming from Explore', async () => { - const loader = new DashboardScenePageStateManager({}); - const localStorageDashboard = { uid: 'fake-dash' }; - store.setObject(DASHBOARD_FROM_LS_KEY, { dashboard: localStorageDashboard }); - - await loader.loadDashboard({ - uid: 'fake-dash', - route: DashboardRoutes.Normal, - keepDashboardFromExploreInLocalStorage: false, - }); - - expect(loader.getDashboardFromCache('fake-dash')).toBeNull(); - }); - }); }); }); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 1a04ca0c0d9..3f7ade11335 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -1,16 +1,11 @@ import { locationUtil } from '@grafana/data'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; -import { default as localStorageStore } from 'app/core/store'; import { getMessageFromError } from 'app/core/utils/errors'; import { startMeasure, stopMeasure } from 'app/core/utils/metrics'; import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor'; -import { - DASHBOARD_FROM_LS_KEY, - removeDashboardToFetchFromLocalStorage, -} from 'app/features/dashboard/state/initDashboard'; import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking'; import { getSelectedScopesNames } from 'app/features/scopes'; import { DashboardDTO, DashboardRoutes } from 'app/types'; @@ -47,12 +42,6 @@ export interface LoadDashboardOptions { uid: string; route: DashboardRoutes; urlFolderUid?: string; - // A temporary approach not to clean the dashboard from local storage when navigating from Explore to Dashboard - // We currently need it as there are two flows of fetching dashboard. The legacy one (initDashboard), uses the new one(DashboardScenePageStateManager.fetch) where the - // removal of the dashboard from local storage is implemented. So in the old flow we wouldn't be able to early return dashboard from local storage, if we prematurely - // removed it when prefetching the dashboard in DashboardPageProxy. - // This property will be removed when the old flow (initDashboard) is removed. - keepDashboardFromExploreInLocalStorage?: boolean; } export class DashboardScenePageStateManager extends StateManagerBase { @@ -63,21 +52,7 @@ export class DashboardScenePageStateManager extends StateManagerBase { - const model = localStorageStore.getObject(DASHBOARD_FROM_LS_KEY); - - if (model) { - if (!keepDashboardFromExploreInLocalStorage) { - removeDashboardToFetchFromLocalStorage(); - } - return model; - } - + public async fetchDashboard({ uid, route, urlFolderUid }: LoadDashboardOptions): Promise { const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid; const cachedDashboard = this.getDashboardFromCache(cacheKey); @@ -203,29 +178,20 @@ export class DashboardScenePageStateManager extends StateManagerBase { - const comingFromExplore = Boolean( - localStorageStore.getObject(DASHBOARD_FROM_LS_KEY) && - options.keepDashboardFromExploreInLocalStorage === false - ); - this.setState({ dashboard: undefined, isLoading: true }); const rsp = await this.fetchDashboard(options); const fromCache = this.getSceneFromCache(options.uid); - - // When coming from Explore, skip returnning scene from cache - if (!comingFromExplore) { - if (fromCache && fromCache.state.version === rsp?.dashboard.version) { - return fromCache; - } + if (fromCache && fromCache.state.version === rsp?.dashboard.version) { + return fromCache; } if (rsp?.dashboard) { const scene = transformSaveModelToScene(rsp); // Cache scene only if not coming from Explore, we don't want to cache temporary dashboard - if (options.uid && !comingFromExplore) { + if (options.uid) { this.setSceneCache(options.uid, scene); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx index 5a406406282..b4a8fd20698 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx @@ -18,6 +18,7 @@ import { import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { selectors } from '@grafana/e2e-selectors'; import { config, locationService, setPluginExtensionsHook } from '@grafana/runtime'; +import { PANEL_EDIT_LAST_USED_DATASOURCE } from 'app/features/dashboard/utils/dashboard'; import { InspectTab } from 'app/features/inspector/types'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types'; @@ -257,6 +258,7 @@ jest.mock('app/core/store', () => ({ get: jest.fn(), getObject: jest.fn((_a, b) => b), setObject: jest.fn(), + delete: jest.fn(), })); const store = jest.requireMock('app/core/store'); @@ -665,9 +667,14 @@ describe('PanelDataQueriesTab', () => { it('should load last used data source if no data source specified for a panel', async () => { store.exists.mockReturnValue(true); - store.getObject.mockReturnValue({ - dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f', - datasourceUid: 'gdev-testdata', + store.getObject.mockImplementation((key: string, def: unknown) => { + if (key === PANEL_EDIT_LAST_USED_DATASOURCE) { + return { + dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f', + datasourceUid: 'gdev-testdata', + }; + } + return def; }); const { queriesTab } = await setupScene('panel-5'); diff --git a/public/app/features/dashboard-scene/saving/shared.tsx b/public/app/features/dashboard-scene/saving/shared.tsx index 23c7fc22e00..7fd584b666e 100644 --- a/public/app/features/dashboard-scene/saving/shared.tsx +++ b/public/app/features/dashboard-scene/saving/shared.tsx @@ -39,7 +39,7 @@ export interface NameAlreadyExistsErrorProps { } export function NameAlreadyExistsError({ cancelButton, saveButton }: NameAlreadyExistsErrorProps) { - const isRestoreDashboardsEnabled = config.featureToggles.dashboardRestore && config.featureToggles.dashboardRestoreUI; + const isRestoreDashboardsEnabled = config.featureToggles.dashboardRestore; return isRestoreDashboardsEnabled ? (

diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx index aefb2953e2b..34c5f3fe057 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx @@ -1,22 +1,40 @@ -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; +import { toUtc } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { SceneDataLayerControls, SceneVariableSet, TextBoxVariable, VariableValueSelectors } from '@grafana/scenes'; import { DashboardControls, DashboardControlsState } from './DashboardControls'; import { DashboardScene } from './DashboardScene'; +const mockGetAnchorInfo = jest.fn((link) => ({ + href: `/dashboard/${link.title}`, + title: link.title, + tooltip: link.tooltip || null, +})); + +// Mock the getLinkSrv function +jest.mock('app/features/panel/panellinks/link_srv', () => ({ + getLinkSrv: jest.fn(() => ({ + getAnchorInfo: mockGetAnchorInfo, + })), +})); + describe('DashboardControls', () => { describe('Given a standard scene', () => { it('should initialize with default values', () => { - const scene = buildTestScene(); + const { controls: scene } = buildTestScene(); expect(scene.state.variableControls).toEqual([]); expect(scene.state.timePicker).toBeDefined(); expect(scene.state.refreshPicker).toBeDefined(); }); it('should return if time controls are hidden', () => { - const scene = buildTestScene({ hideTimeControls: false, hideVariableControls: false, hideLinksControls: false }); + const { controls: scene } = buildTestScene({ + hideTimeControls: false, + hideVariableControls: false, + hideLinksControls: false, + }); expect(scene.hasControls()).toBeTruthy(); scene.setState({ hideTimeControls: true }); expect(scene.hasControls()).toBeTruthy(); @@ -27,14 +45,14 @@ describe('DashboardControls', () => { describe('Component', () => { it('should render', () => { - const scene = buildTestScene(); + const { controls: scene } = buildTestScene(); expect(() => { render(); }).not.toThrow(); }); it('should render visible controls', async () => { - const scene = buildTestScene({ + const { controls: scene } = buildTestScene({ variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], }); const renderer = render(); @@ -47,7 +65,7 @@ describe('DashboardControls', () => { }); it('should render with hidden controls', async () => { - const scene = buildTestScene({ + const { controls: scene } = buildTestScene({ hideTimeControls: true, hideVariableControls: true, hideLinksControls: true, @@ -61,13 +79,13 @@ describe('DashboardControls', () => { describe('UrlSync', () => { it('should return keys', () => { - const scene = buildTestScene(); + const { controls: scene } = buildTestScene(); // @ts-expect-error expect(scene._urlSync.getKeys()).toEqual(['_dash.hideTimePicker', '_dash.hideVariables', '_dash.hideLinks']); }); it('should not return url state for hide flags', () => { - const scene = buildTestScene(); + const { controls: scene } = buildTestScene(); expect(scene.getUrlState()).toEqual({}); scene.setState({ hideTimeControls: true, @@ -78,7 +96,7 @@ describe('DashboardControls', () => { }); it('should update from url', () => { - const scene = buildTestScene(); + const { controls: scene } = buildTestScene(); scene.updateFromUrl({ '_dash.hideTimePicker': 'true', '_dash.hideVariables': 'true', @@ -98,7 +116,11 @@ describe('DashboardControls', () => { }); it('should not override state if no new state comes from url', () => { - const scene = buildTestScene({ hideTimeControls: true, hideVariableControls: true, hideLinksControls: true }); + const { controls: scene } = buildTestScene({ + hideTimeControls: true, + hideVariableControls: true, + hideLinksControls: true, + }); scene.updateFromUrl({}); expect(scene.state.hideTimeControls).toBeTruthy(); expect(scene.state.hideVariableControls).toBeTruthy(); @@ -106,7 +128,11 @@ describe('DashboardControls', () => { }); it('should not call setState if no changes', () => { - const scene = buildTestScene({ hideTimeControls: true, hideVariableControls: true, hideLinksControls: true }); + const { controls: scene } = buildTestScene({ + hideTimeControls: true, + hideVariableControls: true, + hideLinksControls: true, + }); const setState = jest.spyOn(scene, 'setState'); scene.updateFromUrl({ @@ -118,9 +144,34 @@ describe('DashboardControls', () => { expect(setState).toHaveBeenCalledTimes(0); }); }); + + it('Should update link hrefs when time range changes', () => { + const { controls, dashboard } = buildTestScene(); + render(); + + //clear initial calls to getAnchorInfo + mockGetAnchorInfo.mockClear(); + + act(() => { + // Update time range + dashboard.state.$timeRange?.setState({ + value: { + from: toUtc('2021-01-01'), + to: toUtc('2021-01-02'), + raw: { from: toUtc('2020-01-01'), to: toUtc('2020-01-02') }, + }, + }); + }); + + //expect getAnchorInfo to be called after time range change + expect(mockGetAnchorInfo).toHaveBeenCalledTimes(1); + }); }); -function buildTestScene(state?: Partial): DashboardControls { +function buildTestScene(state?: Partial): { + dashboard: DashboardScene; + controls: DashboardControls; +} { const variable = new TextBoxVariable({ name: 'A', label: 'A', @@ -155,5 +206,5 @@ function buildTestScene(state?: Partial): DashboardContr dashboard.activate(); variable.activate(); - return dashboard.state.controls as DashboardControls; + return { dashboard, controls: dashboard.state.controls as DashboardControls }; } diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 3805792f93b..41e125957a5 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -122,9 +122,10 @@ function DashboardControlsRenderer({ model }: SceneComponentProps { }); }); - describe('When coming from explore', () => { - // When coming from Explore the first panel in a dashboard is a temporary panel - it('should remove first panel from the grid when discarding changes', () => { - const layout = DefaultGridLayoutManager.fromVizPanels([ - new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - ]); - const scene = new DashboardScene({ - title: 'hello', - uid: 'dash-1', - description: 'hello description', - editable: true, - $timeRange: new SceneTimeRange({ - timeZone: 'browser', - }), - controls: new DashboardControls({}), - $behaviors: [new behaviors.CursorSync({})], - body: layout, - }); - - scene.onEnterEditMode(true); - expect(scene.state.isEditing).toBe(true); - expect(layout.state.grid.state.children.length).toBe(2); - - scene.exitEditMode({ skipConfirm: true }); - - const restoredGrid = scene.state.body as DefaultGridLayoutManager; - expect(scene.state.isEditing).toBe(false); - expect(restoredGrid.state.grid.state.children.length).toBe(1); - }); - }); - describe('When a dashboard contain angular panels', () => { it('should return true if the dashboard contains angular panels', () => { // create a scene with angular panels inside diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index aa3fee00f82..7d7fcc88e55 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -158,11 +158,6 @@ export class DashboardScene extends SceneObjectBase { */ private _changeTracker: DashboardSceneChangeTracker; - /** - * Flag to indicate if the user came from Explore - */ - private _fromExplore = false; - /** * A reference to the scopes facade */ @@ -244,8 +239,7 @@ export class DashboardScene extends SceneObjectBase { } } - public onEnterEditMode = (fromExplore = false) => { - this._fromExplore = fromExplore; + public onEnterEditMode = () => { // Save this state this._initialState = sceneUtils.cloneSceneObjectState(this.state); this._initialUrlState = locationService.getLocation(); @@ -334,10 +328,6 @@ export class DashboardScene extends SceneObjectBase { locationService.replace(locationUtil.stripBaseFromUrl(url)); - if (this._fromExplore) { - this.cleanupStateFromExplore(); - } - if (restoreInitialState) { // Restore initial state and disable editing this.setState({ ...this._initialState, isEditing: false }); @@ -357,18 +347,6 @@ export class DashboardScene extends SceneObjectBase { this.state.body.editModeChanged(false); } - private cleanupStateFromExplore() { - this._fromExplore = false; - // When coming from explore but discarding changes, remove the panel that explore is potentially adding. - if (this._initialSaveModel?.panels) { - this._initialSaveModel.panels = this._initialSaveModel.panels.slice(1); - } - - if (this._initialState) { - this._initialState.body.cleanUpStateFromExplore?.(); - } - } - public canDiscard() { return this._initialState !== undefined; } @@ -482,6 +460,10 @@ export class DashboardScene extends SceneObjectBase { this.onEnterEditMode(); } + const panelId = dashboardSceneGraph.getNextPanelId(this); + vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) }); + vizPanel.clearParent(); + this.state.body.addPanel(vizPanel); } @@ -532,7 +514,7 @@ export class DashboardScene extends SceneObjectBase { panel.setState({ key: getVizPanelKeyForPanelId(panelId) }); panel.clearParent(); - this.state.body.addPanel(panel); + this.addPanel(panel); store.delete(LS_PANEL_COPY_KEY); } diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index d338a64be03..88f2bdc68ed 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -3,10 +3,9 @@ import { useEffect, useMemo } from 'react'; import { useLocation } from 'react-router-dom-v5-compat'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; -import { config, useChromeHeaderHeight } from '@grafana/runtime'; +import { useChromeHeaderHeight } from '@grafana/runtime'; import { SceneComponentProps } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; -import { TOP_BAR_LEVEL_HEIGHT } from 'app/core/components/AppChrome/types'; import NativeScrollbar from 'app/core/components/NativeScrollbar'; import { Page } from 'app/core/components/Page/Page'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; @@ -15,7 +14,7 @@ import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty'; import { useSelector } from 'app/types'; import { DashboardScene } from './DashboardScene'; -import { NavToolbarActions, ToolbarActions } from './NavToolbarActions'; +import { NavToolbarActions } from './NavToolbarActions'; import { PanelSearchLayout } from './PanelSearchLayout'; import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner'; @@ -31,7 +30,6 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps { @@ -81,17 +79,12 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps : undefined} - > + {editPanel && } {!editPanel && (

- {!isSingleTopNav && } + {controls && (
@@ -140,7 +133,7 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) { position: 'sticky', zIndex: theme.zIndex.activePanel, background: theme.colors.background.canvas, - top: config.featureToggles.singleTopNav ? headerHeight + TOP_BAR_LEVEL_HEIGHT : headerHeight, + top: headerHeight, }, }), canvasContent: css({ diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 5b0fa84910a..d0aa2f56b9f 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -6,9 +6,10 @@ import { PluginExtensionTypes, getDefaultTimeRange, toDataFrame, + urlUtil, } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; -import { getPluginLinkExtensions, locationService } from '@grafana/runtime'; +import { config, getPluginLinkExtensions, locationService } from '@grafana/runtime'; import { LocalValueVariable, SceneQueryRunner, @@ -19,6 +20,10 @@ import { } from '@grafana/scenes'; import { contextSrv } from 'app/core/services/context_srv'; import { GetExploreUrlArguments } from 'app/core/utils/explore'; +import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; +import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form'; +import * as storeModule from 'app/store/store'; +import { AccessControlAction } from 'app/types'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; @@ -30,6 +35,7 @@ import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutMana const mocks = { contextSrv: jest.mocked(contextSrv), getExploreUrl: jest.fn(), + notifyApp: jest.fn(), }; jest.mock('app/core/utils/explore', () => ({ @@ -47,6 +53,10 @@ jest.mock('@grafana/runtime', () => ({ getPluginLinkExtensions: jest.fn(), })); +jest.mock('app/store/store', () => ({ + dispatch: jest.fn(), +})); + const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions); describe('panelMenuBehavior', () => { @@ -102,6 +112,9 @@ describe('panelMenuBehavior', () => { mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); + config.unifiedAlertingEnabled = true; + grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate]); + menu.activate(); await new Promise((r) => setTimeout(r, 1)); @@ -548,6 +561,112 @@ describe('panelMenuBehavior', () => { expect(menu.state.items?.[0].text).toBe('Explore'); }); }); + + describe('onCreateAlert', () => { + beforeEach(() => { + jest.spyOn(storeModule, 'dispatch').mockImplementation(() => {}); + jest.spyOn(locationService, 'push').mockImplementation(() => {}); + jest.spyOn(urlUtil, 'renderUrl').mockImplementation((url, params) => `${url}?${JSON.stringify(params)}`); + }); + + it('should navigate to alert creation page on success', async () => { + const { menu, panel } = await buildTestScene({}); + const mockFormValues = { someKey: 'someValue' }; + + config.unifiedAlertingEnabled = true; + grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate]); + + jest + .spyOn(require('app/features/alerting/unified/utils/rule-form'), 'scenesPanelToRuleFormValues') + .mockResolvedValue(mockFormValues); + + // activate the menu + menu.activate(); + // wait for the menu to be activated + await new Promise((r) => setTimeout(r, 1)); + // use userEvent mechanism to click the menu item + const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu; + const alertMenuItem = moreMenu?.find((i) => i.text === 'New alert rule')?.onClick; + expect(alertMenuItem).toBeDefined(); + + alertMenuItem?.({} as React.MouseEvent); + expect(scenesPanelToRuleFormValues).toHaveBeenCalledWith(panel); + }); + + it('should show error notification on failure', async () => { + const { menu, panel } = await buildTestScene({}); + const mockError = new Error('Test error'); + jest + .spyOn(require('app/features/alerting/unified/utils/rule-form'), 'scenesPanelToRuleFormValues') + .mockRejectedValue(mockError); + // Don't make notifyApp throw an error, just mock it + + menu.activate(); + await new Promise((r) => setTimeout(r, 1)); + + const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu; + const alertMenuItem = moreMenu?.find((i) => i.text === 'New alert rule')?.onClick; + expect(alertMenuItem).toBeDefined(); + + await alertMenuItem?.({} as React.MouseEvent); + + await new Promise((r) => setTimeout(r, 0)); + + expect(scenesPanelToRuleFormValues).toHaveBeenCalledWith(panel); + }); + + it('should render "New alert rule" menu item when user has permissions to read and update alerts', async () => { + const { menu } = await buildTestScene({}); + config.unifiedAlertingEnabled = true; + grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate]); + + menu.activate(); + await new Promise((r) => setTimeout(r, 1)); + + const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu; + expect(moreMenu?.find((i) => i.text === 'New alert rule')).toBeDefined(); + }); + + it('should not contain "New alert rule" menu item when user does not have permissions to read and update alerts', async () => { + const { menu } = await buildTestScene({}); + config.unifiedAlertingEnabled = true; + grantUserPermissions([AccessControlAction.AlertingRuleRead]); + + menu.activate(); + await new Promise((r) => setTimeout(r, 1)); + + const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu; + expect(moreMenu?.find((i) => i.text === 'New alert rule')).toBeUndefined(); + }); + + it('should not contain "New alert rule" menu item when unifiedAlertingEnabled is false', async () => { + const { menu } = await buildTestScene({}); + config.unifiedAlertingEnabled = false; + + menu.activate(); + await new Promise((r) => setTimeout(r, 1)); + + const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu; + expect(moreMenu?.find((i) => i.text === 'New alert rule')).toBeUndefined(); + }); + + it('should not contain "New alert rule" menu item when user does not have permissions to read and update alerts', async () => { + const { menu } = await buildTestScene({}); + config.unifiedAlertingEnabled = true; + grantUserPermissions([AccessControlAction.AlertingRuleRead]); + + menu.activate(); + await new Promise((r) => setTimeout(r, 1)); + + const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu; + const alertMenuItem = moreMenu?.find((i) => i.text === 'New alert rule')?.onClick; + expect(alertMenuItem).toBeUndefined(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + }); }); interface SceneOptions { diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 982bd12b6b2..a3e1706ec3f 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -12,14 +12,19 @@ import { config, getPluginLinkExtensions, locationService } from '@grafana/runti import { LocalValueVariable, sceneGraph, SceneGridRow, VizPanel, VizPanelMenu } from '@grafana/scenes'; import { DataQuery, OptionsWithLegend } from '@grafana/schema'; import appEvents from 'app/core/app_events'; +import { createErrorNotification } from 'app/core/copy/appNotification'; import { t } from 'app/core/internationalization'; +import { notifyApp } from 'app/core/reducers/appNotification'; import { contextSrv } from 'app/core/services/context_srv'; +import { getMessageFromError } from 'app/core/utils/errors'; +import { getCreateAlertInMenuAvailability } from 'app/features/alerting/unified/utils/access-control'; import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; import { InspectTab } from 'app/features/inspector/types'; import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration'; +import { dispatch } from 'app/store/store'; import { ShowConfirmModalEvent } from 'app/types/events'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; @@ -214,11 +219,15 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) { } } - moreSubMenu.push({ - text: t('panel.header-menu.new-alert-rule', `New alert rule`), - iconClassName: 'bell', - onClick: (e) => onCreateAlert(panel), - }); + const isCreateAlertMenuOptionAvailable = getCreateAlertInMenuAvailability(); + + if (isCreateAlertMenuOptionAvailable) { + moreSubMenu.push({ + text: t('panel.header-menu.new-alert-rule', `New alert rule`), + iconClassName: 'bell', + onClick: (e) => onCreateAlert(panel), + }); + } if (hasLegendOptions(panel.state.options) && !isEditingPanel) { moreSubMenu.push({ @@ -482,12 +491,18 @@ export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) { } const onCreateAlert = async (panel: VizPanel) => { - const formValues = await scenesPanelToRuleFormValues(panel); - const ruleFormUrl = urlUtil.renderUrl('/alerting/new', { - defaults: JSON.stringify(formValues), - returnTo: location.pathname + location.search, - }); - locationService.push(ruleFormUrl); + try { + const formValues = await scenesPanelToRuleFormValues(panel); + const ruleFormUrl = urlUtil.renderUrl('/alerting/new', { + defaults: JSON.stringify(formValues), + returnTo: location.pathname + location.search, + }); + locationService.push(ruleFormUrl); + } catch (err) { + const message = `Error getting rule values from the panel: ${getMessageFromError(err)}`; + dispatch(notifyApp(createErrorNotification(message))); + return; + } }; export function toggleVizPanelLegend(vizPanel: VizPanel): void { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index e23afc410d6..f16d3f3b6db 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -23,6 +23,7 @@ import { import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardDTO, DashboardDataDTO } from 'app/types'; +import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; @@ -248,6 +249,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, registerPanelInteractionsReporter, new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), preserveDashboardSceneStateInLocalStorage, + addPanelsOnLoadBehavior, new DashboardScopesFacade({ reloadOnScopesChange: oldModel.meta.reloadOnScopesChange, uid: oldModel.uid, diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx index 0e0b0976a2a..72510de5130 100644 --- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx @@ -1,11 +1,11 @@ import { AnnotationQuery, getDataSourceRef, NavModel, NavModelItem, PageLayoutType } from '@grafana/data'; -import { config, getDataSourceSrv } from '@grafana/runtime'; +import { getDataSourceSrv } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase, VizPanel, dataLayers } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardScene } from '../scene/DashboardScene'; -import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions'; +import { NavToolbarActions } from '../scene/NavToolbarActions'; import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; @@ -133,7 +133,6 @@ function AnnotationsSettingsView({ model }: SceneComponentProps : undefined} - > - {!isSingleTopNav && } + + : undefined} - > - {!isSingleTopNav && } + + : undefined} - > - {!isSingleTopNav && } + + : undefined} - > - {!isSingleTopNav && } + + ); diff --git a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx index 19494d5663d..de55bc59a64 100644 --- a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx @@ -33,7 +33,7 @@ export function DeleteDashboardButton({ dashboard }: ButtonProps) { dashboard: 1, }, source: 'dashboard_scene_settings', - restore_enabled: config.featureToggles.dashboardRestoreUI, + restore_enabled: config.featureToggles.dashboardRestore, }); toggleModal(); if (dashboard.state.uid) { diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index 1540c2fbbd0..73b1d8cdceb 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -25,7 +25,7 @@ import { GenAIDashTitleButton } from 'app/features/dashboard/components/GenAI/Ge import { updateNavModel } from '../pages/utils'; import { DashboardScene } from '../scene/DashboardScene'; -import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions'; +import { NavToolbarActions } from '../scene/NavToolbarActions'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; @@ -177,16 +177,10 @@ export class GeneralSettingsEditView const { intervals } = model.getRefreshPicker().useState(); const { hideTimeControls } = model.getDashboardControls().useState(); const { enabled: liveNow } = model.getLiveNowTimer().useState(); - const isSingleTopNav = config.featureToggles.singleTopNav; return ( - : undefined} - > - {!isSingleTopNav && } + +
i const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const canSave = dashboard.useState().meta.canSave; const { jsonText } = model.useState(); - const isSingleTopNav = config.featureToggles.singleTopNav; const onSave = async (overwrite: boolean) => { const result = await onSaveDashboard(dashboard, JSON.parse(model.state.jsonText), { @@ -176,13 +174,8 @@ export class JsonModelEditView extends SceneObjectBase i ); } return ( - : undefined} - > - {!isSingleTopNav && } + +
The JSON model below is the data structure that defines the dashboard. This includes dashboard settings, diff --git a/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx b/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx index 727122e9ecb..5dfa1a49659 100644 --- a/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx @@ -1,5 +1,4 @@ import { PageLayoutType } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { Permissions } from 'app/core/components/AccessControl'; import { Page } from 'app/core/components/Page/Page'; @@ -7,7 +6,7 @@ import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; import { DashboardScene } from '../scene/DashboardScene'; -import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions'; +import { NavToolbarActions } from '../scene/NavToolbarActions'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; @@ -35,16 +34,10 @@ function PermissionsEditorSettings({ model }: SceneComponentProps : undefined} - > - {!isSingleTopNav && } + + ); diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx index 993bcd0fb10..ffdfe4e7856 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx @@ -1,10 +1,9 @@ import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase, SceneVariable, SceneVariables, sceneGraph } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; import { DashboardScene } from '../scene/DashboardScene'; -import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions'; +import { NavToolbarActions } from '../scene/NavToolbarActions'; import { getDashboardSceneFor } from '../utils/utils'; import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync'; @@ -207,7 +206,6 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps : undefined} - > - {!isSingleTopNav && } + + : undefined} - > - {!isSingleTopNav && } + + 1; const hasMore = model.versions.length >= model.limit; const isLastPage = model.versions.find((rev) => rev.version === 1); - const isSingleTopNav = config.featureToggles.singleTopNav; const viewModeCompare = ( <> @@ -239,13 +237,8 @@ function VersionsEditorSettingsListView({ model }: SceneComponentProps : undefined} - > - {!isSingleTopNav && } + + {viewMode === 'compare' ? viewModeCompare : viewModeList} ); diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditorList.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditorList.tsx index 8686eb8bcb9..8a579c1d792 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditorList.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditorList.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd'; +import classNames from 'classnames'; import { ReactElement } from 'react'; import { selectors } from '@grafana/e2e-selectors'; @@ -30,76 +31,66 @@ export function VariableEditorList({ onEdit, }: Props): ReactElement { const styles = useStyles2(getStyles); + const onDragEnd = (result: DropResult) => { if (!result.destination || !result.source) { return; } + reportInteraction('Variable drag and drop'); onChangeOrder(result.source.index, result.destination.index); }; - return ( -
-
- {variables.length === 0 && } - - {variables.length > 0 && ( - -
- - - - - - - - - - {(provided) => ( - - {variables.map((variableScene, index) => { - const variableState = variableScene.state; - return ( - - ); - })} - {provided.placeholder} - - )} - - -
VariableDefinition -
-
- - - - -
- )} -
-
+ return variables.length <= 0 ? ( + + ) : ( + + + + + + + + + + + {(provided) => ( + + {variables.map((variableScene, index) => { + const variableState = variableScene.state; + return ( + + ); + })} + {provided.placeholder} + + )} + + +
VariableDefinition +
+ + + + +
); } -function EmptyVariablesList({ onAdd }: { onAdd: () => void }): ReactElement { +function EmptyVariablesList({ onAdd }: { onAdd: () => void }) { return ( void }): ReactElement { const getStyles = () => ({ tableContainer: css({ - overflow: 'scroll', - width: '100%', + overflow: 'auto', }), }); diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 3d92e3731c3..86a3986be31 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -16,6 +16,7 @@ import { Badge, } from '@grafana/ui'; import { updateNavIndex } from 'app/core/actions'; +import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator'; import config from 'app/core/config'; import { useAppNotification } from 'app/core/copy/appNotification'; @@ -82,7 +83,6 @@ export const DashNav = memo((props) => { // this ensures the component rerenders when the location changes useLocation(); const forceUpdate = useForceUpdate(); - const isSingleTopNav = config.featureToggles.singleTopNav; // We don't really care about the event payload here only that it triggeres a re-render of this component useBusEvent(props.dashboard.events, DashboardMetaChangedEvent); @@ -357,11 +357,15 @@ export const DashNav = memo((props) => { }; return ( - <> - {renderLeftActions()} - {!isSingleTopNav && } - {renderRightActions()} - + + {renderLeftActions()} + + {renderRightActions()} + + } + /> ); }); diff --git a/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx index 303cae3ac2b..6b31090b447 100644 --- a/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx +++ b/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx @@ -5,12 +5,12 @@ import { AccessControlAction } from 'app/types'; import { SettingsPageProps } from '../DashboardSettings/types'; -export const AccessControlDashboardPermissions = ({ dashboard, sectionNav, toolbar }: SettingsPageProps) => { +export const AccessControlDashboardPermissions = ({ dashboard, sectionNav }: SettingsPageProps) => { const canSetPermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsWrite); const pageNav = sectionNav.node.parentItem; return ( - + ); diff --git a/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx index e345cf1533d..158d80b9dcc 100644 --- a/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx @@ -7,7 +7,7 @@ import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } fro import { SettingsPageProps } from './types'; -export function AnnotationsSettings({ dashboard, editIndex, sectionNav, toolbar }: SettingsPageProps) { +export function AnnotationsSettings({ dashboard, editIndex, sectionNav }: SettingsPageProps) { const onNew = () => { const newAnnotation: AnnotationQuery = { name: newAnnotationName, @@ -27,7 +27,7 @@ export function AnnotationsSettings({ dashboard, editIndex, sectionNav, toolbar const isEditing = editIndex != null && editIndex < dashboard.annotations.list.length; return ( - + {!isEditing && } {isEditing && } diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index ab2fd4d6a21..adb6af01c37 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom-v5-compat'; import { locationUtil, NavModel, NavModelItem } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { config, locationService } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { Button, Stack, Text, ToolbarButtonRow } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { Page } from 'app/core/components/Page/Page'; @@ -36,7 +36,6 @@ const onClose = () => locationService.partial({ editview: null, editIndex: null export function DashboardSettings({ dashboard, editview, pageNav, sectionNav }: Props) { const [updateId, setUpdateId] = useState(0); - const isSingleTopNav = config.featureToggles.singleTopNav; useEffect(() => { dashboard.events.subscribe(DashboardMetaChangedEvent, () => setUpdateId((v) => v + 1)); }, [dashboard]); @@ -82,15 +81,8 @@ export function DashboardSettings({ dashboard, editview, pageNav, sectionNav }: return ( <> - {!isSingleTopNav && ( - {actions}} /> - )} - {actions} : undefined} - sectionNav={subSectionNav} - dashboard={dashboard} - editIndex={editIndex} - /> + {actions}} /> + ); } @@ -217,9 +209,9 @@ function getSectionNav( }; } -function MakeEditable({ dashboard, sectionNav, toolbar }: SettingsPageProps) { +function MakeEditable({ dashboard, sectionNav }: SettingsPageProps) { return ( - + Dashboard not editable diff --git a/public/app/features/dashboard/components/HelpWizard/SupportSnapshotService.ts b/public/app/features/dashboard/components/HelpWizard/SupportSnapshotService.ts index e9f68ad556d..145cec475ca 100644 --- a/public/app/features/dashboard/components/HelpWizard/SupportSnapshotService.ts +++ b/public/app/features/dashboard/components/HelpWizard/SupportSnapshotService.ts @@ -1,7 +1,6 @@ import saveAs from 'file-saver'; import { dateTimeFormat, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { SceneObject } from '@grafana/scenes'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { Randomize } from 'app/features/dashboard-scene/inspect/HelpWizard/randomizer'; @@ -9,7 +8,6 @@ import { createDashboardSceneFromDashboardModel } from 'app/features/dashboard-s import { getTimeSrv } from '../../services/TimeSrv'; import { DashboardModel, PanelModel } from '../../state'; -import { setDashboardToFetchFromLocalStorage } from '../../state/initDashboard'; import { getDebugDashboard, getGithubMarkdown } from './utils'; @@ -136,12 +134,4 @@ export class SupportSnapshotService extends StateManagerBase { - const { snapshot } = this.state; - if (snapshot) { - setDashboardToFetchFromLocalStorage({ meta: {}, dashboard: snapshot }); - global.open(config.appUrl + 'dashboard/new', '_blank'); - } - }; } diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index c90641f96be..a77c269d0ca 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -6,7 +6,7 @@ import { Subscription } from 'rxjs'; import { FieldConfigSource, GrafanaTheme2, NavModel, NavModelItem, PageLayoutType } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { config, locationService } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { Button, HorizontalGroup, @@ -432,7 +432,6 @@ export class PanelEditorUnconnected extends PureComponent { render() { const { initDone, uiState, theme, sectionNav, pageNav, className, updatePanelEditorUIState } = this.props; - const isSingleTopNav = config.featureToggles.singleTopNav; const styles = getStyles(theme, this.props); if (!initDone) { @@ -446,17 +445,10 @@ export class PanelEditorUnconnected extends PureComponent { data-testid={selectors.components.PanelEditor.General.content} layout={PageLayoutType.Custom} className={className} - toolbar={ - isSingleTopNav ? ( - {this.renderEditorActions()} - ) : undefined - } > - {!isSingleTopNav && ( - {this.renderEditorActions()}} - /> - )} + {this.renderEditorActions()}} + />
{!uiState.isPanelOptionsVisible ? ( diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx index 2a5941c82e9..d65ad7a9cb0 100644 --- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx @@ -31,7 +31,7 @@ export const SaveDashboardErrorProxy = ({ setErrorIsHandled, }: SaveDashboardErrorProxyProps) => { const { onDashboardSave } = useDashboardSave(); - const isRestoreDashboardsEnabled = config.featureToggles.dashboardRestore && config.featureToggles.dashboardRestoreUI; + const isRestoreDashboardsEnabled = config.featureToggles.dashboardRestore; return ( <> {error.data && error.data.status === 'version-mismatch' && ( diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index cf838fa2aca..7d82ebbda2b 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -7,7 +7,6 @@ import { selectors } from '@grafana/e2e-selectors'; import { config, locationService } from '@grafana/runtime'; import { Themeable2, withTheme2 } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; -import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { ScrollRefElement } from 'app/core/components/NativeScrollbar'; import { Page } from 'app/core/components/Page/Page'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; @@ -362,7 +361,6 @@ export class UnthemedDashboardPage extends PureComponent { const { editPanel, viewPanel, pageNav, sectionNav } = this.state; const kioskMode = getKioskMode(this.props.queryParams); const styles = getStyles(theme); - const isSingleTopNav = config.featureToggles.singleTopNav; if (!dashboard || !pageNav || !sectionNav) { return ; @@ -438,8 +436,9 @@ export class UnthemedDashboardPage extends PureComponent { layout={PageLayoutType.Canvas} className={pageClassName} onSetScrollRef={this.setScrollRef} - toolbar={ - isSingleTopNav ? ( + > + {showToolbar && ( +
{ kioskMode={kioskMode} hideTimePicker={dashboard.timepicker.hidden} /> - ) : undefined - } - > - {showToolbar && ( -
- - } - />
)} diff --git a/public/app/features/dashboard/containers/DashboardPageProxy.tsx b/public/app/features/dashboard/containers/DashboardPageProxy.tsx index e45bb442aee..e51a8ce9ad8 100644 --- a/public/app/features/dashboard/containers/DashboardPageProxy.tsx +++ b/public/app/features/dashboard/containers/DashboardPageProxy.tsx @@ -40,11 +40,7 @@ function DashboardPageProxy(props: DashboardPageProxyProps) { return null; } - return stateManager.fetchDashboard({ - route: props.route.routeName as DashboardRoutes, - uid: params.uid ?? '', - keepDashboardFromExploreInLocalStorage: true, - }); + return stateManager.fetchDashboard({ route: props.route.routeName as DashboardRoutes, uid: params.uid ?? '' }); }, [params.uid, props.route.routeName]); if (!config.featureToggles.dashboardSceneForViewers) { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 599c4ad2a8b..cf58c391fd3 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -19,7 +19,15 @@ import { getFolderByUid } from 'app/features/folders/state/actions'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { toStateKey } from 'app/features/variables/utils'; -import { DashboardDTO, DashboardInitPhase, DashboardRoutes, StoreState, ThunkDispatch, ThunkResult } from 'app/types'; +import { + DASHBOARD_FROM_LS_KEY, + DashboardDTO, + DashboardInitPhase, + DashboardRoutes, + StoreState, + ThunkDispatch, + ThunkResult, +} from 'app/types'; import { createDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner'; import { initVariablesTransaction } from '../../variables/state/actions'; @@ -50,13 +58,6 @@ async function fetchDashboard( dispatch: ThunkDispatch, getState: () => StoreState ): Promise { - // When creating new or adding panels to a dashboard from explore we load it from local storage - const model = store.getObject(DASHBOARD_FROM_LS_KEY); - if (model) { - removeDashboardToFetchFromLocalStorage(); - return model; - } - try { switch (args.routeName) { case DashboardRoutes.Home: { @@ -184,7 +185,6 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult { // fetch dashboard data const dashDTO = await fetchDashboard(args, dispatch, getState); - const versionBeforeMigration = dashDTO?.dashboard?.version; // returns null if there was a redirect or error @@ -192,6 +192,8 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult { return; } + addPanelsFromLocalStorage(dashDTO); + // set initializing state dispatch(dashboardInitServices()); @@ -298,12 +300,18 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult { }; } -export const DASHBOARD_FROM_LS_KEY = 'DASHBOARD_FROM_LS_KEY'; +function addPanelsFromLocalStorage(model: DashboardDTO) { + // When creating new or adding panels to a dashboard from explore we load it from local storage + const fromLS = store.getObject(DASHBOARD_FROM_LS_KEY); + if (fromLS) { + if (fromLS.dashboard.panels) { + model.dashboard.panels = fromLS.dashboard.panels.concat(model.dashboard.panels); + } -export function setDashboardToFetchFromLocalStorage(model: DashboardDTO) { - store.setObject(DASHBOARD_FROM_LS_KEY, model); -} + if (fromLS.dashboard.time) { + model.dashboard.time = fromLS.dashboard.time; + } -export function removeDashboardToFetchFromLocalStorage() { - store.delete(DASHBOARD_FROM_LS_KEY); + store.delete(DASHBOARD_FROM_LS_KEY); + } } diff --git a/public/app/features/dashboard/utils/dashboard.ts b/public/app/features/dashboard/utils/dashboard.ts index 5627bb9209c..d11a154feca 100644 --- a/public/app/features/dashboard/utils/dashboard.ts +++ b/public/app/features/dashboard/utils/dashboard.ts @@ -119,7 +119,7 @@ type LastUsedDatasource = } | undefined; -const PANEL_EDIT_LAST_USED_DATASOURCE = 'grafana.dashboards.panelEdit.lastUsedDatasource'; +export const PANEL_EDIT_LAST_USED_DATASOURCE = 'grafana.dashboards.panelEdit.lastUsedDatasource'; // Function that returns last used datasource from local storage export function getLastUsedDatasourceFromStorage(dashboardUid: string): LastUsedDatasource { diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 1ab96c05414..f5f4c9b6ccc 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -1,10 +1,11 @@ import { css, cx } from '@emotion/css'; import { useEffect, useState } from 'react'; +import { useLocalStorage } from 'react-use'; import { CoreApp, GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema/dist/esm/index'; -import { ErrorBoundaryAlert, Modal, useStyles2, useTheme2 } from '@grafana/ui'; +import { Badge, ErrorBoundaryAlert, Modal, useStyles2, useTheme2 } from '@grafana/ui'; import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; import { useGrafana } from 'app/core/context/GrafanaContext'; @@ -21,6 +22,7 @@ import { ExploreActions } from './ExploreActions'; import { ExploreDrawer } from './ExploreDrawer'; import { ExplorePaneContainer } from './ExplorePaneContainer'; import { QueriesDrawerContextProvider, useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext'; +import { QUERY_LIBRARY_LOCAL_STORAGE_KEYS } from './QueryLibrary/QueryLibrary'; import { queryLibraryTrackAddFromQueryRow } from './QueryLibrary/QueryLibraryAnalyticsEvents'; import { QueryTemplateForm } from './QueryLibrary/QueryTemplateForm'; import RichHistoryContainer from './RichHistory/RichHistoryContainer'; @@ -63,6 +65,10 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa const { drawerOpened, setDrawerOpened, queryLibraryAvailable } = useQueriesDrawerContext(); const showCorrelationEditorBar = config.featureToggles.correlations && (correlationDetails?.editorMode || false); const [queryToAdd, setQueryToAdd] = useState(); + const [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton] = useLocalStorage( + QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.newButton, + true + ); useEffect(() => { //This is needed for breadcrumbs and topnav. @@ -77,19 +83,32 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa if (hasQueryLibrary) { RowActionComponents.addKeyedExtraRenderAction(QUERY_LIBRARY_ACTION_KEY, { scope: CoreApp.Explore, - queryActionComponent: (props) => ( - { - setQueryToAdd(props.query); - }} - /> - ), + queryActionComponent: (props) => + showQueryLibraryBadgeButton ? ( + { + setQueryToAdd(props.query); + setShowQueryLibraryBadgeButton(false); + }} + style={{ cursor: 'pointer' }} + /> + ) : ( + { + setQueryToAdd(props.query); + }} + /> + ), }); } - }, []); + }, [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton]); useKeyboardShortcuts(); diff --git a/public/app/features/explore/QueryLibrary/QueryLibrary.tsx b/public/app/features/explore/QueryLibrary/QueryLibrary.tsx index 9a296b41fbb..fffc454ee3a 100644 --- a/public/app/features/explore/QueryLibrary/QueryLibrary.tsx +++ b/public/app/features/explore/QueryLibrary/QueryLibrary.tsx @@ -1,3 +1,6 @@ +import { useLocalStorage } from 'react-use'; + +import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo'; import { QueryTemplatesList } from './QueryTemplatesList'; export interface QueryLibraryProps { @@ -6,6 +9,26 @@ export interface QueryLibraryProps { activeDatasources?: string[]; } +export const QUERY_LIBRARY_LOCAL_STORAGE_KEYS = { + explore: { + notifyUserAboutQueryLibrary: 'grafana.explore.query-library.notifyUserAboutQueryLibrary', + newButton: 'grafana.explore.query-library.newButton', + }, +}; + export function QueryLibrary({ activeDatasources }: QueryLibraryProps) { - return ; + const [notifyUserAboutQueryLibrary, setNotifyUserAboutQueryLibrary] = useLocalStorage( + QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.notifyUserAboutQueryLibrary, + true + ); + + return ( + <> + setNotifyUserAboutQueryLibrary(false)} + /> + + + ); } diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryExpmInfo.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryExpmInfo.tsx new file mode 100644 index 00000000000..7c88af4a555 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryLibraryExpmInfo.tsx @@ -0,0 +1,23 @@ +import { Alert, Modal } from '@grafana/ui'; + +interface Props { + isOpen: boolean; + onDismiss: () => void; +} + +export function QueryLibraryExpmInfo({ isOpen, onDismiss }: Props) { + return ( + + + + + + ); +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx index ed654fa378d..eefab87c7d1 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx @@ -66,9 +66,7 @@ export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData } .then(() => { getAppEvents().publish({ type: AppEvents.alertSuccess.name, - payload: [ - t('explore.query-library.query-template-added', 'Query template successfully added to the library'), - ], + payload: [t('explore.query-library.query-template-added', 'Query successfully saved to the library')], }); return true; }) @@ -76,7 +74,7 @@ export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData } getAppEvents().publish({ type: AppEvents.alertError.name, payload: [ - t('explore.query-library.query-template-add-error', 'Error attempting to add this query to the library'), + t('explore.query-library.query-template-add-error', 'Error attempting to save this query to the library'), ], }); return false; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx index ead35b647c6..48fd253e0f1 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'; import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data'; import { getAppEvents, getDataSourceSrv } from '@grafana/runtime'; -import { EmptyState, FilterInput, InlineLabel, MultiSelect, Spinner, useStyles2, Stack } from '@grafana/ui'; +import { EmptyState, FilterInput, InlineLabel, MultiSelect, Spinner, useStyles2, Stack, Badge } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { createQueryText } from 'app/core/utils/richHistory'; import { useAllQueryTemplatesQuery } from 'app/features/query-library'; @@ -15,6 +15,7 @@ import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { QueryLibraryProps } from './QueryLibrary'; import { queryLibraryTrackFilterDatasource } from './QueryLibraryAnalyticsEvents'; +import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo'; import QueryTemplatesTable from './QueryTemplatesTable'; import { QueryTemplateRow } from './QueryTemplatesTable/types'; import { searchQueryLibrary } from './utils/search'; @@ -23,6 +24,7 @@ interface QueryTemplatesListProps extends QueryLibraryProps {} export function QueryTemplatesList(props: QueryTemplatesListProps) { const { data, isLoading, error } = useAllQueryTemplatesQuery(); + const [isModalOpen, setIsModalOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [datasourceFilters, setDatasourceFilters] = useState>>( props.activeDatasources?.map((ds) => ({ value: ds, label: ds })) || [] @@ -163,6 +165,7 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) { return ( <> + setIsModalOpen(false)} /> + setIsModalOpen(true)} + /> diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index cb00212d199..7ddb1d05c90 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -11,6 +11,7 @@ import { RichHistorySettings, createDatasourcesList, } from 'app/core/utils/richHistory'; +import { QUERY_LIBRARY_GET_LIMIT, queryLibraryApi } from 'app/features/query-library/api/factory'; import { useSelector } from 'app/types'; import { RichHistoryQuery } from 'app/types/explore'; @@ -96,8 +97,10 @@ export function RichHistory(props: RichHistoryProps) { .map((eDs) => listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name) .filter((name): name is string => !!name); + const queryTemplatesCount = useSelector(queryLibraryApi.endpoints.allQueryTemplates.select()).data?.length || 0; + const QueryLibraryTab: TabConfig = { - label: i18n.queryLibrary, + label: `${i18n.queryLibrary} (${queryTemplatesCount}/${QUERY_LIBRARY_GET_LIMIT})`, value: Tabs.QueryLibrary, content: , icon: 'book', diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx index 3c30c6eae7c..cbbcac5f6fe 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { DataQuery } from '@grafana/schema'; import { Button, Modal } from '@grafana/ui'; -import { isQueryLibraryEnabled } from 'app/features/query-library'; +import { isQueryLibraryEnabled, useAllQueryTemplatesQuery } from 'app/features/query-library'; import { queryLibraryTrackAddFromQueryHistory, @@ -16,6 +16,7 @@ type Props = { }; export const RichHistoryAddToLibrary = ({ query }: Props) => { + const { refetch } = useAllQueryTemplatesQuery(); const [isOpen, setIsOpen] = useState(false); const [hasBeenSaved, setHasBeenSaved] = useState(false); @@ -45,6 +46,7 @@ export const RichHistoryAddToLibrary = ({ query }: Props) => { if (isSuccess) { setIsOpen(false); setHasBeenSaved(true); + refetch(); queryLibraryTrackAddFromQueryHistory(query.datasource?.type || ''); } }} diff --git a/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx b/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx new file mode 100644 index 00000000000..87dbc64f5ac --- /dev/null +++ b/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx @@ -0,0 +1,31 @@ +import { type ReactElement } from 'react'; + +import { AddToDashboardForm } from 'app/features/dashboard-scene/addToDashboard/AddToDashboardForm'; +import { useSelector } from 'app/types'; + +import { getExploreItemSelector } from '../../state/selectors'; + +import { buildDashboardPanelFromExploreState } from './addToDashboard'; + +interface Props { + onClose: () => void; + exploreId: string; +} + +export function ExploreToDashboardPanel(props: Props): ReactElement { + const { exploreId, onClose } = props; + const exploreItem = useSelector(getExploreItemSelector(exploreId))!; + + const buildPanel = () => { + return buildDashboardPanelFromExploreState({ + datasource: exploreItem.datasourceInstance?.getRef(), + queries: exploreItem.queries, + queryResponse: exploreItem.queryResponse, + panelState: exploreItem?.panelsState, + }); + }; + + return ( + + ); +} diff --git a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts index 5b6d9c91196..1c7c8912a17 100644 --- a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts +++ b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts @@ -1,115 +1,29 @@ -import { MutableDataFrame } from '@grafana/data'; -import { DataQuery, defaultDashboard } from '@grafana/schema'; -import * as api from 'app/features/dashboard/state/initDashboard'; +import { getDefaultTimeRange, MutableDataFrame } from '@grafana/data'; +import { DataQuery, LoadingState } from '@grafana/schema'; import { ExplorePanelData } from 'app/types'; -import { createEmptyQueryResponse } from '../../state/utils'; - -import { setDashboardInLocalStorage } from './addToDashboard'; - -let mockDashboard = {} as unknown; -jest.mock('app/features/dashboard/api/dashboard_api', () => ({ - getDashboardAPI: () => ({ - getDashboardDTO: () => { - return Promise.resolve(mockDashboard); - }, - }), -})); - -describe('addPanelToDashboard', () => { - let spy: jest.SpyInstance; - beforeAll(() => { - spy = jest.spyOn(api, 'setDashboardToFetchFromLocalStorage'); - }); +import { buildDashboardPanelFromExploreState } from './addToDashboard'; +describe('buildDashboardPanelFromExploreState', () => { afterEach(() => { jest.resetAllMocks(); }); - it('Correct datasource ref is used', async () => { - await setDashboardInLocalStorage({ + it('Correct datasource ref is used', () => { + const result = buildDashboardPanelFromExploreState({ queries: [], queryResponse: createEmptyQueryResponse(), datasource: { type: 'loki', uid: 'someUid' }, - time: { from: 'now-1h', to: 'now' }, - }); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - dashboard: expect.objectContaining({ - panels: expect.arrayContaining([expect.objectContaining({ datasource: { type: 'loki', uid: 'someUid' } })]), - }), - }) - ); - }); - - it('Correct time range is used', async () => { - await setDashboardInLocalStorage({ - queries: [], - queryResponse: createEmptyQueryResponse(), - datasource: { type: 'loki', uid: 'someUid' }, - time: { from: 'now-10h', to: 'now' }, }); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - dashboard: expect.objectContaining({ - time: expect.objectContaining({ from: 'now-10h', to: 'now' }), - }), - }) - ); + expect(result.datasource).toEqual({ type: 'loki', uid: 'someUid' }); }); - it('All queries are correctly passed through', async () => { + it('All queries are correctly passed through', () => { const queries: DataQuery[] = [{ refId: 'A' }, { refId: 'B', hide: true }]; - await setDashboardInLocalStorage({ - queries, - queryResponse: createEmptyQueryResponse(), - time: { from: 'now-1h', to: 'now' }, - }); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - dashboard: expect.objectContaining({ - panels: expect.arrayContaining([expect.objectContaining({ targets: expect.arrayContaining(queries) })]), - }), - }) - ); - }); - - it('Previous panels should not be removed', async () => { - const queries: DataQuery[] = [{ refId: 'A' }]; - const existingPanel = { prop: 'this should be kept' }; - - // Set the mocked dashboard - mockDashboard = { - dashboard: { - ...defaultDashboard, - templating: { list: [] }, - title: 'Previous panels should not be removed', - uid: 'someUid', - panels: [existingPanel], - }, - meta: {}, - }; - - await setDashboardInLocalStorage({ - queries, - queryResponse: createEmptyQueryResponse(), - dashboardUid: 'someUid', - datasource: { type: '' }, - time: { from: 'now-1h', to: 'now' }, - }); - - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - dashboard: expect.objectContaining({ - panels: expect.arrayContaining([ - expect.objectContaining({ targets: expect.arrayContaining(queries) }), - existingPanel, - ]), - }), - }) - ); + const result = buildDashboardPanelFromExploreState({ queries, queryResponse: createEmptyQueryResponse() }); + expect(result.targets).toEqual(queries); }); describe('Setting visualization type', () => { @@ -125,14 +39,8 @@ describe('addPanelToDashboard', () => { ]; it.each(cases)('%s', async (_, queries, queryResponse) => { - await setDashboardInLocalStorage({ queries, queryResponse, time: { from: 'now-1h', to: 'now' } }); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - dashboard: expect.objectContaining({ - panels: expect.arrayContaining([expect.objectContaining({ type: 'table' })]), - }), - }) - ); + const result = buildDashboardPanelFromExploreState({ queries, queryResponse }); + expect(result.type).toBe('table'); }); }); @@ -157,14 +65,8 @@ describe('addPanelToDashboard', () => { [framesType]: [new MutableDataFrame({ refId: 'A', fields: [] })], }; - await setDashboardInLocalStorage({ queries, queryResponse, time: { from: 'now-1h', to: 'now' } }); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - dashboard: expect.objectContaining({ - panels: expect.arrayContaining([expect.objectContaining({ type: expectedPanel })]), - }), - }) - ); + const result = buildDashboardPanelFromExploreState({ queries, queryResponse }); + expect(result.type).toBe(expectedPanel); } ); @@ -181,15 +83,29 @@ describe('addPanelToDashboard', () => { ], }; - await setDashboardInLocalStorage({ queries, queryResponse, time: { from: 'now-1h', to: 'now' } }); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - dashboard: expect.objectContaining({ - panels: expect.arrayContaining([expect.objectContaining({ type: 'someCustomPluginId' })]), - }), - }) - ); + const result = buildDashboardPanelFromExploreState({ queries, queryResponse }); + expect(result.type).toBe('someCustomPluginId'); }); }); }); }); + +function createEmptyQueryResponse(): ExplorePanelData { + return { + state: LoadingState.NotStarted, + series: [], + timeRange: getDefaultTimeRange(), + graphFrames: [], + logsFrames: [], + traceFrames: [], + nodeGraphFrames: [], + flameGraphFrames: [], + customFrames: [], + tableFrames: [], + rawPrometheusFrames: [], + rawPrometheusResult: null, + graphResult: null, + logsResult: null, + tableResult: null, + }; +} diff --git a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts index 51c927801c7..db647d4b1eb 100644 --- a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts +++ b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts @@ -1,23 +1,14 @@ import { DataFrame, ExplorePanelsState } from '@grafana/data'; -import { Dashboard, DataQuery, DataSourceRef } from '@grafana/schema'; +import { DataQuery, DataSourceRef, Panel } from '@grafana/schema'; import { DataTransformerConfig } from '@grafana/schema/dist/esm/raw/dashboard/x/dashboard_types.gen'; -import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; -import { setDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard'; -import { buildNewDashboardSaveModel } from 'app/features/dashboard-scene/serialization/buildNewDashboardSaveModel'; -import { DashboardDTO, ExplorePanelData } from 'app/types'; +import { ExplorePanelData } from 'app/types'; -export enum AddToDashboardError { - FETCH_DASHBOARD = 'fetch-dashboard', - SET_DASHBOARD_LS = 'set-dashboard-ls-error', -} - -interface AddPanelToDashboardOptions { +interface ExploreToDashboardPanelOptions { queries: DataQuery[]; queryResponse: ExplorePanelData; datasource?: DataSourceRef; dashboardUid?: string; panelState?: ExplorePanelsState; - time: Dashboard['time']; } /** @@ -27,7 +18,10 @@ interface AddPanelToDashboardOptions { * @param panelType * @param options */ -function getLogsTableTransformations(panelType: string, options: AddPanelToDashboardOptions): DataTransformerConfig[] { +function getLogsTableTransformations( + panelType: string, + options: ExploreToDashboardPanelOptions +): DataTransformerConfig[] { let transformations: DataTransformerConfig[] = []; if (panelType === 'table' && options.panelState?.logs?.columns) { // If we have a labels column, we need to extract the fields from it @@ -64,10 +58,11 @@ function getLogsTableTransformations(panelType: string, options: AddPanelToDashb return transformations; } -export async function setDashboardInLocalStorage(options: AddPanelToDashboardOptions) { +export function buildDashboardPanelFromExploreState(options: ExploreToDashboardPanelOptions): Panel { const panelType = getPanelType(options.queries, options.queryResponse, options?.panelState); - const panel = { + return { + //@ts-ignore targets: options.queries, type: panelType, title: 'New Panel', @@ -75,28 +70,6 @@ export async function setDashboardInLocalStorage(options: AddPanelToDashboardOpt datasource: options.datasource, transformations: getLogsTableTransformations(panelType, options), }; - - let dto: DashboardDTO; - - if (options.dashboardUid) { - try { - dto = await getDashboardAPI().getDashboardDTO(options.dashboardUid); - } catch (e) { - throw AddToDashboardError.FETCH_DASHBOARD; - } - } else { - dto = await buildNewDashboardSaveModel(); - } - - dto.dashboard.panels = [panel, ...(dto.dashboard.panels ?? [])]; - - dto.dashboard.time = options.time; - - try { - setDashboardToFetchFromLocalStorage(dto); - } catch { - throw AddToDashboardError.SET_DASHBOARD_LS; - } } const isVisible = (query: DataQuery) => !query.hide; diff --git a/public/app/features/explore/extensions/AddToDashboard/index.test.tsx b/public/app/features/explore/extensions/AddToDashboard/index.test.tsx index 6c38ba8b471..79a51c929cb 100644 --- a/public/app/features/explore/extensions/AddToDashboard/index.test.tsx +++ b/public/app/features/explore/extensions/AddToDashboard/index.test.tsx @@ -1,25 +1,25 @@ -import { act, render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ReactNode } from 'react'; import { Provider } from 'react-redux'; -import { selectors } from '@grafana/e2e-selectors'; -import { locationService, setEchoSrv } from '@grafana/runtime'; -import { DataQuery, defaultDashboard } from '@grafana/schema'; -import { backendSrv } from 'app/core/services/backend_srv'; +import { setEchoSrv } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { Echo } from 'app/core/services/echo/Echo'; -import * as initDashboard from 'app/features/dashboard/state/initDashboard'; -import { DashboardSearchItemType } from 'app/features/search/types'; import { configureStore } from 'app/store/configureStore'; import { ExploreState } from 'app/types'; import { createEmptyQueryResponse } from '../../state/utils'; -import * as api from './addToDashboard'; - import { AddToDashboard } from '.'; +jest.mock('app/core/services/context_srv'); + +const mocks = { + contextSrv: jest.mocked(contextSrv), +}; + const setup = (children: ReactNode, queries: DataQuery[] = [{ refId: 'A' }]) => { const store = configureStore({ explore: { @@ -40,15 +40,8 @@ const setup = (children: ReactNode, queries: DataQuery[] = [{ refId: 'A' }]) => return render({children}); }; -jest.mock('app/core/services/context_srv'); - -const mocks = { - contextSrv: jest.mocked(contextSrv), -}; - const openModal = async (nameOverride?: string) => { await userEvent.click(screen.getByRole('button', { name: /add to dashboard/i })); - expect(await screen.findByRole('dialog', { name: nameOverride || 'Add panel to dashboard' })).toBeInTheDocument(); }; @@ -57,12 +50,8 @@ describe('AddToDashboardButton', () => { setEchoSrv(new Echo()); }); - /* The Add to dashboard form brings in the DashboardPicker, which will call backendSrv.search as part of its instantiation - If we do not need a list of dashboards for the test, return an empty array. */ beforeEach(() => { - // Mock the search response so we don't get any refused connection errors - // from this test (as the fetch polyfill means this logic would actually try and call the API) - jest.spyOn(backendSrv, 'search').mockResolvedValue([]); + mocks.contextSrv.hasPermission.mockImplementation(() => true); }); afterEach(() => { @@ -81,21 +70,9 @@ describe('AddToDashboardButton', () => { }); describe('Success path', () => { - const addToDashboardResponse = Promise.resolve(); - - const waitForAddToDashboardResponse = async () => { - return act(async () => { - await addToDashboardResponse; - }); - }; - - beforeEach(() => { - jest.spyOn(api, 'setDashboardInLocalStorage').mockReturnValue(addToDashboardResponse); - mocks.contextSrv.hasPermission.mockImplementation(() => true); - }); - afterEach(() => { jest.restoreAllMocks(); + mocks.contextSrv.hasPermission.mockImplementation(() => true); }); it('Opens and closes the modal correctly', async () => { @@ -104,321 +81,7 @@ describe('AddToDashboardButton', () => { await openModal(); await userEvent.click(screen.getByRole('button', { name: /cancel/i })); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); - - describe('navigation', () => { - it('Navigates to dashboard when clicking on "Open"', async () => { - // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. - const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); - const pushSpy = jest.spyOn(locationService, 'push'); - - setup(); - - await openModal(); - - await userEvent.click(screen.getByRole('button', { name: /open dashboard$/i })); - - await waitForAddToDashboardResponse(); - - expect(screen.queryByRole('dialog', { name: 'Add panel to dashboard' })).not.toBeInTheDocument(); - - expect(pushSpy).toHaveBeenCalled(); - expect(openSpy).not.toHaveBeenCalled(); - }); - - it('Navigates to dashboard in a new tab when clicking on "Open in a new tab"', async () => { - // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. - const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); - const pushSpy = jest.spyOn(locationService, 'push'); - - setup(); - - await openModal(); - - await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); - - await waitForAddToDashboardResponse(); - - expect(openSpy).toHaveBeenCalledWith(expect.anything(), '_blank'); - expect(pushSpy).not.toHaveBeenCalled(); - }); - }); - - describe('Save to new dashboard', () => { - describe('Navigate to correct dashboard when saving', () => { - it('Opens the new dashboard in a new tab', async () => { - // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. - const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); - - setup(); - - await openModal(); - - await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); - - await waitForAddToDashboardResponse(); - - expect(openSpy).toHaveBeenCalledWith('dashboard/new', '_blank'); - }); - - it('Navigates to the new dashboard', async () => { - const pushSpy = jest.spyOn(locationService, 'push'); - - setup(); - - await openModal(); - - await userEvent.click(screen.getByRole('button', { name: /open dashboard$/i })); - - await waitForAddToDashboardResponse(); - - expect(screen.queryByRole('dialog', { name: 'Add panel to dashboard' })).not.toBeInTheDocument(); - - expect(pushSpy).toHaveBeenCalledWith('dashboard/new'); - }); - }); - }); - - describe('Save to existing dashboard', () => { - it('Renders the dashboard picker when switching to "Existing Dashboard"', async () => { - setup(); - - await openModal(); - - expect(screen.queryByRole('combobox', { name: /dashboard/ })).not.toBeInTheDocument(); - - await userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); - expect(screen.getByRole('combobox', { name: /dashboard/ })).toBeInTheDocument(); - }); - - it('Does not submit if no dashboard is selected', async () => { - locationService.push = jest.fn(); - - setup(); - - await openModal(); - - await userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); - - await userEvent.click(screen.getByRole('button', { name: /open dashboard$/i })); - await waitForAddToDashboardResponse(); - - expect(locationService.push).not.toHaveBeenCalled(); - }); - - describe('Navigate to correct dashboard when saving', () => { - it('Opens the selected dashboard in a new tab', async () => { - // @ts-expect-error global.open should return a Window, but is not implemented in js-dom. - const openSpy = jest.spyOn(global, 'open').mockReturnValue(true); - - jest.spyOn(backendSrv, 'getDashboardByUid').mockResolvedValue({ - dashboard: { ...defaultDashboard, templating: { list: [] }, title: 'Dashboard Title', uid: 'someUid' }, - meta: {}, - }); - jest.spyOn(backendSrv, 'search').mockResolvedValue([ - { - uid: 'someUid', - isStarred: false, - title: 'Dashboard Title', - tags: [], - type: DashboardSearchItemType.DashDB, - uri: 'someUri', - url: 'someUrl', - }, - ]); - - setup(); - - await openModal(); - - await userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); - - await userEvent.click(screen.getByRole('combobox', { name: /dashboard/i })); - - await waitFor(async () => { - await screen.findByTestId(selectors.components.Select.option); - }); - await userEvent.click(screen.getByTestId(selectors.components.Select.option)); - - await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); - - await waitFor(async () => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - - expect(openSpy).toBeCalledWith('d/someUid', '_blank'); - }); - - it('Navigates to the selected dashboard', async () => { - const pushSpy = jest.spyOn(locationService, 'push'); - - jest.spyOn(backendSrv, 'getDashboardByUid').mockResolvedValue({ - dashboard: { ...defaultDashboard, templating: { list: [] }, title: 'Dashboard Title', uid: 'someUid' }, - meta: {}, - }); - jest.spyOn(backendSrv, 'search').mockResolvedValue([ - { - uid: 'someUid', - isStarred: false, - title: 'Dashboard Title', - tags: [], - type: DashboardSearchItemType.DashDB, - uri: 'someUri', - url: 'someUrl', - }, - ]); - - setup(); - - await openModal(); - - await userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); - - await userEvent.click(screen.getByRole('combobox', { name: /dashboard/i })); - - await waitFor(async () => { - await screen.findByTestId(selectors.components.Select.option); - }); - await userEvent.click(screen.getByTestId(selectors.components.Select.option)); - - await userEvent.click(screen.getByRole('button', { name: /open dashboard$/i })); - - await waitFor(async () => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - - expect(pushSpy).toBeCalledWith('d/someUid'); - }); - }); - }); - }); - - describe('Permissions', () => { - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('Should only show existing dashboard option with no access to create', async () => { - mocks.contextSrv.hasPermission.mockImplementation((action) => { - if (action === 'dashboards:create') { - return false; - } else { - return true; - } - }); - setup(); - await openModal('Add panel to existing dashboard'); - expect(screen.queryByRole('radio')).not.toBeInTheDocument(); - }); - - it('Should only show new dashboard option with no access to write', async () => { - mocks.contextSrv.hasPermission.mockImplementation((action) => { - if (action === 'dashboards:write') { - return false; - } else { - return true; - } - }); - setup(); - await openModal('Add panel to new dashboard'); - expect(screen.queryByRole('radio')).not.toBeInTheDocument(); - }); - }); - - describe('Error handling', () => { - beforeEach(() => { - mocks.contextSrv.hasPermission.mockImplementation(() => true); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('Shows an error if opening a new tab fails', async () => { - jest.spyOn(global, 'open').mockReturnValue(null); - const removeDashboardSpy = jest.spyOn(initDashboard, 'removeDashboardToFetchFromLocalStorage'); - - setup(); - - await openModal(); - expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - - await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); - - await waitFor(async () => { - expect(await screen.findByRole('alert')).toBeInTheDocument(); - }); - - expect(removeDashboardSpy).toHaveBeenCalled(); - }); - - it('Shows an error if saving to localStorage fails', async () => { - jest.spyOn(initDashboard, 'setDashboardToFetchFromLocalStorage').mockImplementation(() => { - throw 'SOME ERROR'; - }); - - setup(); - - await openModal(); - expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - - await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); - - await waitFor(async () => { - expect(await screen.findByRole('alert')).toBeInTheDocument(); - }); - }); - - it('Shows an error if fetching dashboard fails', async () => { - jest.spyOn(backendSrv, 'getDashboardByUid').mockRejectedValue('SOME ERROR'); - jest.spyOn(backendSrv, 'search').mockResolvedValue([ - { - uid: 'someUid', - isStarred: false, - title: 'Dashboard Title', - tags: [], - type: DashboardSearchItemType.DashDB, - uri: 'someUri', - url: 'someUrl', - }, - ]); - - setup(); - - await openModal(); - expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - - await userEvent.click(screen.getByRole('radio', { name: /existing dashboard/i })); - - await userEvent.click(screen.getByRole('combobox', { name: /dashboard/i })); - - await waitFor(async () => { - await screen.findByTestId(selectors.components.Select.option); - }); - await userEvent.click(screen.getByTestId(selectors.components.Select.option)); - - await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); - - await waitFor(async () => { - expect(await screen.findByRole('alert')).toBeInTheDocument(); - }); - }); - - it('Shows an error if an unknown error happens', async () => { - jest.spyOn(api, 'setDashboardInLocalStorage').mockRejectedValue('SOME ERROR'); - - setup(); - - await openModal(); - expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - - await userEvent.click(screen.getByRole('button', { name: /open in new tab/i })); - - await waitFor(async () => { - expect(await screen.findByRole('alert')).toBeInTheDocument(); - }); - }); }); }); diff --git a/public/app/features/explore/extensions/AddToDashboard/index.tsx b/public/app/features/explore/extensions/AddToDashboard/index.tsx index 4b6b4d7f9ad..65326ca0d13 100644 --- a/public/app/features/explore/extensions/AddToDashboard/index.tsx +++ b/public/app/features/explore/extensions/AddToDashboard/index.tsx @@ -6,7 +6,7 @@ import { useSelector } from 'app/types'; import { getExploreItemSelector } from '../../state/selectors'; -import { AddToDashboardForm } from './AddToDashboardForm'; +import { ExploreToDashboardPanel } from './ExploreToDashboardPanel'; import { getAddToDashboardTitle } from './getAddToDashboardTitle'; interface Props { @@ -35,7 +35,7 @@ export const AddToDashboard = ({ exploreId }: Props) => { {isOpen && ( - + )} diff --git a/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx b/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx index d842e451aa0..4ad812d2e5a 100644 --- a/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx +++ b/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx @@ -3,11 +3,12 @@ import { contextSrv } from 'app/core/core'; import { dispatch } from 'app/store/store'; import { AccessControlAction } from 'app/types'; -import { createAddedLinkConfig, logWarning } from '../../plugins/extensions/utils'; +import { log } from '../../plugins/extensions/logs/log'; +import { createAddedLinkConfig } from '../../plugins/extensions/utils'; import { changeCorrelationEditorDetails } from '../state/main'; import { runQueries } from '../state/query'; -import { AddToDashboardForm } from './AddToDashboard/AddToDashboardForm'; +import { ExploreToDashboardPanel } from './AddToDashboard/ExploreToDashboardPanel'; import { getAddToDashboardTitle } from './AddToDashboard/getAddToDashboardTitle'; import { type PluginExtensionExploreContext } from './ToolbarExtensionPoint'; @@ -35,7 +36,7 @@ export function getExploreExtensionConfigs(): PluginExtensionAddedLinkConfig[] { onClick: (_, { context, openModal }) => { openModal({ title: getAddToDashboardTitle(), - body: ({ onDismiss }) => , + body: ({ onDismiss }) => , }); }, }), @@ -54,7 +55,7 @@ export function getExploreExtensionConfigs(): PluginExtensionAddedLinkConfig[] { }), ]; } catch (error) { - logWarning(`Could not configure extensions for Explore due to: "${error}"`); + log.warning(`Could not configure extensions for Explore due to: "${error}"`); return []; } } diff --git a/public/app/features/explore/spec/queryLibrary.test.tsx b/public/app/features/explore/spec/queryLibrary.test.tsx index d69cc001f16..f2422f450d7 100644 --- a/public/app/features/explore/spec/queryLibrary.test.tsx +++ b/public/app/features/explore/spec/queryLibrary.test.tsx @@ -137,7 +137,7 @@ describe('QueryLibrary', () => { expect(testEventBus.publish).toHaveBeenCalledWith( expect.objectContaining({ type: 'alert-success', - payload: ['Query template successfully added to the library'], + payload: ['Query successfully saved to the library'], }) ); await assertAddToQueryLibraryButtonExists(false); diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 464e4b0ecdf..8b2c6888c3f 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -23,7 +23,7 @@ import { import { config, getDataSourceSrv } from '@grafana/runtime'; import { DataQuery, DataSourceJsonData, DataSourceRef, TimeZone } from '@grafana/schema'; import { getLocalRichHistoryStorage } from 'app/core/history/richHistoryStorageProvider'; -import { SortOrder } from 'app/core/utils/richHistory'; +import { SortOrder } from 'app/core/utils/richHistoryTypes'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { ExplorePanelData, StoreState } from 'app/types'; import { ExploreItemState, RichHistoryQuery } from 'app/types/explore'; diff --git a/public/app/features/gops/configuration-tracker/alerting/hooks.ts b/public/app/features/gops/configuration-tracker/alerting/hooks.ts index 3dbc9844d18..f0cde843803 100644 --- a/public/app/features/gops/configuration-tracker/alerting/hooks.ts +++ b/public/app/features/gops/configuration-tracker/alerting/hooks.ts @@ -35,20 +35,9 @@ export function isOnCallContactPointReady(contactPoints: Receiver[]) { ); } -export function useGetContactPoints() { - const alertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery( - GRAFANA_RULES_SOURCE_NAME, - { - refetchOnFocus: true, - refetchOnReconnect: true, - refetchOnMountOrArgChange: true, - } - ); - - const contactPoints = alertmanagerConfiguration.data?.alertmanager_config?.receivers ?? []; - return { contactPoints, isLoading: alertmanagerConfiguration.isLoading }; -} - +/** + * @deprecated Will be removed when notification policies is moved to k8s API. Do not use! + */ export function useGetDefaultContactPoint() { const alertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery( GRAFANA_RULES_SOURCE_NAME, diff --git a/public/app/features/gops/configuration-tracker/irmHooks.ts b/public/app/features/gops/configuration-tracker/irmHooks.ts index a18bc7fd35c..39121db0e6e 100644 --- a/public/app/features/gops/configuration-tracker/irmHooks.ts +++ b/public/app/features/gops/configuration-tracker/irmHooks.ts @@ -1,14 +1,10 @@ import { useMemo } from 'react'; import { locationService } from '@grafana/runtime'; +import { useGrafanaContactPoints } from 'app/features/alerting/unified/components/contact-points/useContactPoints'; import { RelativeUrl, createRelativeUrl } from 'app/features/alerting/unified/utils/url'; -import { - isOnCallContactPointReady, - useGetContactPoints, - useGetDefaultContactPoint, - useIsCreateAlertRuleDone, -} from './alerting/hooks'; +import { isOnCallContactPointReady, useGetDefaultContactPoint, useIsCreateAlertRuleDone } from './alerting/hooks'; import { isContactPointReady } from './alerting/utils'; import { ConfigurationStepsEnum, DataSourceConfigurationData, IrmCardConfiguration } from './components/ConfigureIRM'; import { useGetIncidentPluginConfig } from './incidents/hooks'; @@ -53,7 +49,8 @@ export interface EssentialsConfigurationData { function useGetConfigurationForApps() { // configuration checks for alerting - const { contactPoints, isLoading: isLoadingContactPoints } = useGetContactPoints(); + const { contactPoints, isLoading: isLoadingContactPoints } = useGrafanaContactPoints(); + // TODO: Switch to k8s API/refactored notification policies hook when available const { defaultContactpoint, isLoading: isLoadingDefaultContactPoint } = useGetDefaultContactPoint(); const { isDone: isCreateAlertRuleDone, isLoading: isLoadingAlertCreatedDone } = useIsCreateAlertRuleDone(); // configuration checks for incidents @@ -132,8 +129,8 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: `/alerting/notifications/receivers/${defaultContactpoint}/edit`, - queryParams: { alertmanager: 'grafana' }, + url: `/alerting/notifications`, + queryParams: { search: defaultContactpoint, alertmanager: 'grafana' }, }, label: 'Edit', labelOnDone: 'View', diff --git a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx index 3b22052cf67..b978b1d1b27 100644 --- a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx @@ -37,16 +37,9 @@ function ResourceInfo({ data }: { data: ResourceTableItem }) { return ; case 'LIBRARY_ELEMENT': return ; - case 'ALERT_RULE': - return null; - case 'CONTACT_POINT': - return null; - case 'NOTIFICATION_POLICY': - return null; - case 'NOTIFICATION_TEMPLATE': - return null; - case 'MUTE_TIMING': - return null; + // Starting from 11.4.x, new resources have both `name` and optionally a `parentName`, so we can use this catch-all component. + default: + return ; } } @@ -200,23 +193,35 @@ function InfoSkeleton() { ); } +function BasicResourceInfo({ data }: { data: ResourceTableItem }) { + return ( + <> + {data.name} + {data.parentName && {data.parentName}} + + ); +} + function ResourceIcon({ resource }: { resource: ResourceTableItem }) { const styles = useStyles2(getIconStyles); const datasource = useDatasource(resource.type === 'DATASOURCE' ? resource.refId : undefined); - if (resource.type === 'DASHBOARD') { - return ; - } else if (resource.type === 'FOLDER') { - return ; - } else if (resource.type === 'DATASOURCE' && datasource?.meta?.info?.logos?.small) { - return ; - } else if (resource.type === 'DATASOURCE') { - return ; - } else if (resource.type === 'LIBRARY_ELEMENT') { - return ; - } + switch (resource.type) { + case 'DASHBOARD': + return ; + case 'FOLDER': + return ; + case 'DATASOURCE': + if (datasource?.meta?.info?.logos?.small) { + return ; + } - return undefined; + return ; + case 'LIBRARY_ELEMENT': + return ; + default: + return undefined; + } } function getIconStyles() { diff --git a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx index 8e34f417e02..705da66b693 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -5,6 +5,9 @@ import { useAppNotification } from 'app/core/copy/appNotification'; import { GetSnapshotResponseDto, SnapshotDto } from '../api'; +// After the number of distinct resource types migrated exceeeds this value, we display a generic success message. +const SUCCESS_MESSAGE_ITEM_TYPES_THRESHOLD = 4; + export function useNotifySuccessful(snapshot: GetSnapshotResponseDto | undefined) { const previousStatusRef = useRef(undefined); const notifyApp = useAppNotification(); @@ -32,6 +35,8 @@ export function useNotifySuccessful(snapshot: GetSnapshotResponseDto | undefined function getTranslatedMessage(snapshot: GetSnapshotResponseDto) { const types: string[] = []; + let distinctItems = 0; + for (const [type, count] of Object.entries(snapshot.stats?.types ?? {})) { if (count <= 0) { continue; @@ -48,15 +53,23 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) { } else if (type === 'LIBRARY_ELEMENT') { types.push(t('migrate-to-cloud.migrated-counts.library_elements', 'library elements')); } + + distinctItems += 1; } const successCount = snapshot?.stats?.statuses?.['OK'] ?? 0; - const message = t( + if (distinctItems > SUCCESS_MESSAGE_ITEM_TYPES_THRESHOLD) { + return t( + 'migrate-to-cloud.onprem.success-message-generic', + 'Successfully migrated {{successCount}} resources to your Grafana Cloud instance.', + { successCount } + ); + } + + return t( 'migrate-to-cloud.onprem.success-message', 'Successfully migrated {{successCount}} {{types, list}} to your Grafana Cloud instance.', { successCount, types } ); - - return message; } diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index 951aaa08a12..a283fc4d862 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -1,7 +1,6 @@ -import { getDefaultNormalizer, render, RenderResult, SelectorMatcherOptions, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { Route } from 'react-router-dom'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { getDefaultNormalizer, RenderResult, SelectorMatcherOptions, waitFor } from '@testing-library/react'; +import { Routes, Route } from 'react-router-dom-v5-compat'; +import { render } from 'test/test-utils'; import { PluginErrorCode, @@ -11,7 +10,7 @@ import { WithAccessControlMetadata, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { config, locationService } from '@grafana/runtime'; +import { config } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; import { mockPluginApis, getCatalogPluginMock, getPluginsStateMock, mockUserPermissions } from '../__mocks__'; @@ -65,19 +64,19 @@ const renderPluginDetails = ( pageId?: PluginTabIds; pluginsStateOverride?: ReducerState; } = {} -): RenderResult => { +) => { const plugin = getCatalogPluginMock(pluginOverride); const { id } = plugin; const store = configureStore({ plugins: pluginsStateOverride || getPluginsStateMock([plugin]), }); - locationService.push({ pathname: `/plugins/${id}`, search: pageId ? `?page=${pageId}` : '' }); - return render( - - - + + } /> + , + + { store, historyOptions: { initialEntries: [`/plugins/${id}${pageId ? `?page=${pageId}` : ''}`] } } ); }; @@ -445,7 +444,7 @@ describe('Plugin details page', () => { // @ts-ignore api.uninstallPlugin = jest.fn(); - const { queryByText, getByRole, findByRole } = renderPluginDetails({ + const { queryByText, getByRole, findByRole, user } = renderPluginDetails({ id, name: 'Akumuli', isInstalled: true, @@ -468,14 +467,14 @@ describe('Plugin details page', () => { expect(await findByRole('tab', { name: PluginTabLabels.OVERVIEW })).toBeInTheDocument(); // Open the confirmation modal - await userEvent.click(getByRole('button', { name: /uninstall/i })); + await user.click(getByRole('button', { name: /uninstall/i })); expect(queryByText('Uninstall Akumuli')).toBeInTheDocument(); expect(queryByText('Are you sure you want to uninstall this plugin?')).toBeInTheDocument(); expect(api.uninstallPlugin).toHaveBeenCalledTimes(0); // Confirm the uninstall - await userEvent.click(getByRole('button', { name: /confirm/i })); + await user.click(getByRole('button', { name: /confirm/i })); expect(api.uninstallPlugin).toHaveBeenCalledTimes(1); expect(api.uninstallPlugin).toHaveBeenCalledWith(id); @@ -647,7 +646,7 @@ describe('Plugin details page', () => { }, }); - const { queryByText, getByRole } = renderPluginDetails({ + const { queryByText, getByRole, user } = renderPluginDetails({ id, name, isInstalled: true, @@ -658,7 +657,7 @@ describe('Plugin details page', () => { await waitFor(() => queryByText('Uninstall')); // Click on "Enable" - await userEvent.click(getByRole('button', { name: /enable/i })); + await user.click(getByRole('button', { name: /enable/i })); // Check if the API request was initiated expect(api.updatePluginSettings).toHaveBeenCalledTimes(1); @@ -687,7 +686,7 @@ describe('Plugin details page', () => { }, }); - const { queryByText, getByRole } = renderPluginDetails({ + const { queryByText, getByRole, user } = renderPluginDetails({ id, name, isInstalled: true, @@ -698,7 +697,7 @@ describe('Plugin details page', () => { await waitFor(() => queryByText('Uninstall')); // Click on "Disable" - await userEvent.click(getByRole('button', { name: /disable/i })); + await user.click(getByRole('button', { name: /disable/i })); // Check if the API request was initiated expect(api.updatePluginSettings).toHaveBeenCalledTimes(1); diff --git a/public/app/features/plugins/admin/pages/PluginDetails.tsx b/public/app/features/plugins/admin/pages/PluginDetails.tsx index 7040c1cad5b..004fc0a87be 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.tsx @@ -1,9 +1,9 @@ -import { useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom-v5-compat'; import { PluginDetailsPage } from '../components/PluginDetailsPage'; export default function PluginDetails(): JSX.Element { - const { pluginId } = useParams<{ pluginId: string }>(); + const { pluginId = '' } = useParams<{ pluginId: string }>(); return ; } diff --git a/public/app/features/plugins/extensions/getPluginExtensions.test.tsx b/public/app/features/plugins/extensions/getPluginExtensions.test.tsx index 23d44c6f540..925b0cfbd7b 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.test.tsx +++ b/public/app/features/plugins/extensions/getPluginExtensions.test.tsx @@ -4,6 +4,8 @@ import { PluginExtensionAddedComponentConfig, PluginExtensionAddedLinkConfig } f import { reportInteraction } from '@grafana/runtime'; import { getPluginExtensions } from './getPluginExtensions'; +import { log } from './logs/log'; +import { resetLogMock } from './logs/testUtils'; import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry'; import { AddedLinksRegistry } from './registry/AddedLinksRegistry'; import { isReadOnlyProxy } from './utils'; @@ -16,6 +18,16 @@ jest.mock('@grafana/runtime', () => { }; }); +jest.mock('./logs/log', () => { + const { createLogMock } = jest.requireActual('./logs/testUtils'); + const original = jest.requireActual('./logs/log'); + + return { + ...original, + log: createLogMock(), + }; +}); + async function createRegistries( preloadResults: Array<{ pluginId: string; @@ -77,8 +89,8 @@ describe('getPluginExtensions()', () => { }, }; - global.console.warn = jest.fn(); jest.mocked(reportInteraction).mockReset(); + resetLogMock(log); }); test('should return the extensions for the given placement', async () => { @@ -279,7 +291,7 @@ describe('getPluginExtensions()', () => { expect(context.title).toBe('New title from the context!'); }); - test('should catch errors in the configure() function and log them as warnings', async () => { + test('should catch errors in the configure() function and log them as error', async () => { link2.configure = jest.fn().mockImplementation(() => { throw new Error('Something went wrong!'); }); @@ -291,8 +303,11 @@ describe('getPluginExtensions()', () => { }).not.toThrow(); expect(link2.configure).toHaveBeenCalledTimes(1); - expect(global.console.warn).toHaveBeenCalledTimes(1); - expect(global.console.warn).toHaveBeenCalledWith('[Plugin Extensions] Something went wrong!'); + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith('Failed to configure link with title "Link 2"', { + message: 'Something went wrong!', + stack: expect.stringContaining('Error: Something went wrong!'), + }); }); test('should skip the link extension if the configure() function returns with an invalid path', async () => { @@ -320,7 +335,7 @@ describe('getPluginExtensions()', () => { expect(link1.configure).toHaveBeenCalledTimes(1); expect(link2.configure).toHaveBeenCalledTimes(1); - expect(global.console.warn).toHaveBeenCalledTimes(2); + expect(log.error).toHaveBeenCalledTimes(2); }); test('should skip the extension if any of the updated props returned by the configure() function are invalid', async () => { @@ -336,7 +351,7 @@ describe('getPluginExtensions()', () => { expect(extensions).toHaveLength(0); expect(link2.configure).toHaveBeenCalledTimes(1); - expect(global.console.warn).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledTimes(1); }); test('should skip the extension if the configure() function returns a promise', async () => { @@ -347,7 +362,7 @@ describe('getPluginExtensions()', () => { expect(extensions).toHaveLength(0); expect(link2.configure).toHaveBeenCalledTimes(1); - expect(global.console.warn).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledTimes(1); }); test('should skip (hide) the extension if the configure() function returns undefined', async () => { @@ -357,7 +372,7 @@ describe('getPluginExtensions()', () => { const { extensions } = getPluginExtensions({ ...registries, extensionPointId: extensionPoint2 }); expect(extensions).toHaveLength(0); - expect(global.console.warn).toHaveBeenCalledTimes(0); // As this is intentional, no warning should be logged + expect(log.warning).toHaveBeenCalledTimes(0); // As this is intentional, no warning should be logged }); test('should pass event, context and helper to extension onClick()', async () => { @@ -386,7 +401,7 @@ describe('getPluginExtensions()', () => { ); }); - test('should catch errors in async/promise-based onClick function and log them as warnings', async () => { + test('should catch errors in async/promise-based onClick function and log them as errors', async () => { link2.path = undefined; link2.onClick = jest.fn().mockRejectedValue(new Error('testing')); @@ -400,10 +415,10 @@ describe('getPluginExtensions()', () => { expect(extensions).toHaveLength(1); expect(link2.onClick).toHaveBeenCalledTimes(1); - expect(global.console.warn).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledTimes(1); }); - test('should catch errors in the onClick() function and log them as warnings', async () => { + test('should catch errors in the onClick() function and log them as errors', async () => { link2.path = undefined; link2.onClick = jest.fn().mockImplementation(() => { throw new Error('Something went wrong!'); @@ -417,8 +432,11 @@ describe('getPluginExtensions()', () => { extension.onClick?.({} as React.MouseEvent); expect(link2.onClick).toHaveBeenCalledTimes(1); - expect(global.console.warn).toHaveBeenCalledTimes(1); - expect(global.console.warn).toHaveBeenCalledWith('[Plugin Extensions] Something went wrong!'); + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith('Something went wrong!', { + message: 'Something went wrong!', + stack: expect.stringContaining('Error: Something went wrong!'), + }); }); test('should pass a read only context to the onClick() function', async () => { diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index bd7d55d6e0d..acaaf8d97ac 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -8,13 +8,13 @@ import { } from '@grafana/data'; import { GetPluginExtensions } from '@grafana/runtime'; +import { log } from './logs/log'; import { AddedComponentRegistryItem } from './registry/AddedComponentsRegistry'; import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry'; import { RegistryType } from './registry/Registry'; import type { PluginExtensionRegistries } from './registry/types'; import { getReadOnlyProxy, - logWarning, generateExtensionId, wrapWithPluginContext, getLinkExtensionOnClick, @@ -78,8 +78,16 @@ export const getPluginExtensions: GetExtensions = ({ extensionsByPlugin[pluginId] = 0; } + const linkLog = log.child({ + pluginId, + extensionPointId, + path: addedLink.path ?? '', + title: addedLink.title, + description: addedLink.description, + onClick: typeof addedLink.onClick, + }); // Run the configure() function with the current context, and apply the ovverides - const overrides = getLinkExtensionOverrides(pluginId, addedLink, frozenContext); + const overrides = getLinkExtensionOverrides(pluginId, addedLink, linkLog, frozenContext); // configure() returned an `undefined` -> hide the extension if (addedLink.configure && overrides === undefined) { @@ -91,7 +99,7 @@ export const getPluginExtensions: GetExtensions = ({ id: generateExtensionId(pluginId, extensionPointId, addedLink.title), type: PluginExtensionTypes.link, pluginId: pluginId, - onClick: getLinkExtensionOnClick(pluginId, extensionPointId, addedLink, frozenContext), + onClick: getLinkExtensionOnClick(pluginId, extensionPointId, addedLink, linkLog, frozenContext), // Configurable properties icon: overrides?.icon || addedLink.icon, @@ -105,7 +113,10 @@ export const getPluginExtensions: GetExtensions = ({ extensionsByPlugin[pluginId] += 1; } catch (error) { if (error instanceof Error) { - logWarning(error.message); + log.error(error.message, { + stack: error.stack ?? '', + message: error.message, + }); } } } @@ -120,13 +131,20 @@ export const getPluginExtensions: GetExtensions = ({ if (extensionsByPlugin[addedComponent.pluginId] === undefined) { extensionsByPlugin[addedComponent.pluginId] = 0; } + + const componentLog = log.child({ + title: addedComponent.title, + description: addedComponent.description, + pluginId: addedComponent.pluginId, + }); + const extension: PluginExtensionComponent = { id: generateExtensionId(addedComponent.pluginId, extensionPointId, addedComponent.title), type: PluginExtensionTypes.component, pluginId: addedComponent.pluginId, title: addedComponent.title, description: addedComponent.description, - component: wrapWithPluginContext(addedComponent.pluginId, addedComponent.component), + component: wrapWithPluginContext(addedComponent.pluginId, addedComponent.component, componentLog), }; extensions.push(extension); diff --git a/public/app/features/plugins/extensions/logs/LogViewFilters.tsx b/public/app/features/plugins/extensions/logs/LogViewFilters.tsx new file mode 100644 index 00000000000..549af8fcbee --- /dev/null +++ b/public/app/features/plugins/extensions/logs/LogViewFilters.tsx @@ -0,0 +1,199 @@ +import { isEmpty } from 'lodash'; +import { ReactElement, useMemo } from 'react'; + +import { DataFrame, MatcherConfig, SelectableValue } from '@grafana/data'; +import { SceneDataProvider } from '@grafana/scenes'; +import { InlineField, InlineFieldRow, MultiSelect } from '@grafana/ui'; + +export type LogFilter = { + pluginIds?: Set; + extensionPointIds?: Set; + severity?: Set; + initial?: string; +}; + +type LogViewFiltersProps = { + provider: SceneDataProvider; + filteredProvider: SceneDataProvider; + filter: LogFilter; + onChange: (filter: LogFilter) => void; +}; + +export function LogViewFilters({ provider, filteredProvider, filter, onChange }: LogViewFiltersProps): ReactElement { + const { pluginIds, extensionPointIds, severity } = useLogFilters(provider, filteredProvider, filter); + + const onChangePluginIds = (values: Array>) => { + const update = { + ...filter, + pluginIds: mapToSet(values), + }; + + if (isEmpty(filter.extensionPointIds) && isEmpty(filter.severity)) { + update.initial = isEmpty(values) ? undefined : 'pluginId'; + } + + onChange(update); + }; + + const onChangeExtensionPointIds = (values: Array>) => { + const update = { + ...filter, + extensionPointIds: mapToSet(values), + }; + + if (isEmpty(filter.pluginIds) && isEmpty(filter.severity)) { + update.initial = isEmpty(values) ? undefined : 'extensionPointId'; + } + + onChange(update); + }; + + const onChangeSeverity = (values: Array>) => { + const update = { + ...filter, + severity: mapToSet(values), + }; + + if (isEmpty(filter.pluginIds) && isEmpty(filter.extensionPointIds)) { + update.initial = isEmpty(values) ? undefined : 'severity'; + } + + onChange(update); + }; + + return ( + + + + + + + + + + + + ); +} + +export type FilterConfig = { + fieldName: string; + config: MatcherConfig; +}; + +type LogFilterOptions = { + pluginIds: Array>; + extensionPointIds: Array>; + severity: Array>; +}; + +function useLogFilters( + provider: SceneDataProvider, + filteredProvider: SceneDataProvider, + filter: LogFilter +): LogFilterOptions { + const { data } = provider.useState(); + const { data: filteredData } = filteredProvider.useState(); + + return useMemo(() => { + if (data && data?.series.length > 1) { + console.warn('LogViewFilter does not support multiple series in query result.'); + } + + const frame = data?.series[0]; + const filteredFrame = filteredData?.series[0]; + + if (!frame) { + return { + pluginIds: [], + extensionPointIds: [], + severity: [], + }; + } + + if (!filteredFrame) { + return toFilterOptions({ + severity: frame, + pluginId: frame, + extensionPointId: frame, + }); + } + + switch (filter.initial) { + case 'extensionPointId': + return toFilterOptions({ + severity: filteredFrame, + pluginId: filteredFrame, + extensionPointId: frame, + }); + + case 'severity': + return toFilterOptions({ + severity: frame, + pluginId: filteredFrame, + extensionPointId: filteredFrame, + }); + + case 'pluginId': + return toFilterOptions({ + severity: filteredFrame, + pluginId: frame, + extensionPointId: filteredFrame, + }); + + default: + return toFilterOptions({ + severity: frame, + pluginId: frame, + extensionPointId: frame, + }); + } + }, [data, filteredData, filter]); +} + +function mapToSet(selected: Array>): Set | undefined { + if (selected.length <= 0) { + return undefined; + } + + return selected.reduce((set, selectable) => { + if (selectable.value) { + set.add(selectable.value); + } + return set; + }, new Set()); +} + +function toSelectableArray(source: Set): Array> { + return Array.from(source).reduce((all: Array>, current) => { + if (!current) { + return all; + } + all.push({ + value: current, + label: current, + }); + return all; + }, []); +} + +function toFilterOptions(sources: { + severity: DataFrame; + pluginId: DataFrame; + extensionPointId: DataFrame; +}): LogFilterOptions { + const { severity, pluginId, extensionPointId } = sources; + const severityIndex = severity.fields.findIndex((f) => f.name === 'severity'); + const pluginIdIndex = pluginId.fields.findIndex((f) => f.name === 'pluginId'); + const extensionPointIdIndex = extensionPointId.fields.findIndex((f) => f.name === 'extensionPointId'); + + const severities = new Set(severity.fields[severityIndex].values); + const pluginIds = new Set(pluginId.fields[pluginIdIndex].values); + const extensionPointIds = new Set(extensionPointId.fields[extensionPointIdIndex].values); + + return { + severity: toSelectableArray(severities), + pluginIds: toSelectableArray(pluginIds), + extensionPointIds: toSelectableArray(extensionPointIds), + }; +} diff --git a/public/app/features/plugins/extensions/logs/LogViewer.tsx b/public/app/features/plugins/extensions/logs/LogViewer.tsx new file mode 100644 index 00000000000..155a97611d5 --- /dev/null +++ b/public/app/features/plugins/extensions/logs/LogViewer.tsx @@ -0,0 +1,62 @@ +import { nanoid } from 'nanoid'; +import { ReactElement, useState } from 'react'; + +import { sceneUtils, VizConfigBuilders } from '@grafana/scenes'; +import { + SceneContextProvider, + useDataTransformer, + useQueryRunner, + VizGridLayout, + VizPanel, +} from '@grafana/scenes-react'; +import { Page } from 'app/core/components/Page/Page'; + +import { LogFilter, LogViewFilters } from './LogViewFilters'; +import { ExtensionsLogDataSource } from './dataSource'; +import { createFilterTransformation } from './filterTransformation'; +import { log } from './log'; + +const DATASOURCE_REF = { + uid: nanoid(), + type: 'grafana-extensionslog-datasource', +}; + +const logsViz = VizConfigBuilders.logs().build(); + +sceneUtils.registerRuntimeDataSource({ + dataSource: new ExtensionsLogDataSource(DATASOURCE_REF.type, DATASOURCE_REF.uid, log), +}); + +export default function LogViewer(): ReactElement { + return ( + + + + ); +} + +function LogViewScene(): ReactElement | null { + const [filter, setFilter] = useState({}); + + const data = useQueryRunner({ + datasource: DATASOURCE_REF, + queries: [{ refId: 'A' }], + liveStreaming: true, + }); + + const filteredData = useDataTransformer({ + transformations: [createFilterTransformation(filter)], + data: data, + }); + + return ( + } + > + + + + + ); +} diff --git a/public/app/features/plugins/extensions/logs/dataSource.test.ts b/public/app/features/plugins/extensions/logs/dataSource.test.ts new file mode 100644 index 00000000000..e6b2db93a2c --- /dev/null +++ b/public/app/features/plugins/extensions/logs/dataSource.test.ts @@ -0,0 +1,127 @@ +import { nanoid } from 'nanoid'; +import { lastValueFrom, of } from 'rxjs'; + +import { DataQueryRequest, dateTime, LoadingState } from '@grafana/data'; + +import { ExtensionsLogDataSource } from './dataSource'; +import { log } from './log'; + +jest.mock('./log', () => { + const original = jest.requireActual('./log'); + return { + ...original, + log: { + asObservable: () => + of( + { + level: 'info', + labels: { + test: 'test', + }, + timestamp: Date.now(), + id: nanoid(), + message: 'a message', + pluginId: 'grafana-k8-app', + extensionPointId: 'grafana/dashboards/panel/menu', + }, + { + level: 'debug', + labels: { + title: 'a link', + onClick: 'function', + }, + timestamp: Date.now(), + id: nanoid(), + message: 'another message', + } + ), + }, + }; +}); + +describe('ExtensionsLogDataSource', () => { + const dataSource = new ExtensionsLogDataSource('pluginId', 'ds-uid', log); + + it('should return a stream when querying for data', async () => { + const response = await lastValueFrom(dataSource.query(createRequest())); + expect(response.state).toBe(LoadingState.Streaming); + }); + + it('should return logs as data frames when querying for data', async () => { + const { data } = await lastValueFrom(dataSource.query(createRequest())); + expect(data).toStrictEqual([ + { + refId: 'A', + meta: { + type: 'log-lines', + }, + length: 2, + fields: [ + { + config: expect.any(Object), + name: 'timestamp', + type: 'time', + values: [expect.any(Number), expect.any(Number)], + }, + { + config: expect.any(Object), + name: 'body', + type: 'string', + values: ['another message', 'a message'], + }, + { + config: expect.any(Object), + name: 'severity', + type: 'string', + values: ['debug', 'info'], + }, + { + config: expect.any(Object), + name: 'id', + type: 'string', + values: [expect.any(String), expect.any(String)], + }, + { + config: expect.any(Object), + name: 'labels', + type: 'other', + values: [{ onClick: 'function', title: 'a link' }, { test: 'test' }], + }, + { + config: expect.any(Object), + name: 'pluginId', + type: 'string', + values: [null, 'grafana-k8-app'], + }, + { + config: expect.any(Object), + name: 'extensionPointId', + type: 'string', + values: [null, 'grafana/dashboards/panel/menu'], + }, + ], + }, + ]); + }); +}); + +function createRequest(): DataQueryRequest { + return { + requestId: '', + interval: '', + intervalMs: 0, + range: { + from: dateTime(), + to: dateTime(), + raw: { + from: '', + to: '', + }, + }, + scopedVars: {}, + targets: [{ refId: 'A' }], + timezone: '', + app: '', + startTime: Date.now(), + }; +} diff --git a/public/app/features/plugins/extensions/logs/dataSource.ts b/public/app/features/plugins/extensions/logs/dataSource.ts new file mode 100644 index 00000000000..8b5ba2a4df1 --- /dev/null +++ b/public/app/features/plugins/extensions/logs/dataSource.ts @@ -0,0 +1,104 @@ +import { Observable, scan } from 'rxjs'; + +import { + createDataFrame, + DataFrame, + DataFrameType, + DataQueryRequest, + DataQueryResponse, + FieldType, + LoadingState, + TestDataSourceResponse, +} from '@grafana/data'; +import { RuntimeDataSource, SceneDataQuery } from '@grafana/scenes'; + +import { ExtensionsLog, ExtensionsLogItem } from './log'; + +export class ExtensionsLogDataSource extends RuntimeDataSource { + constructor( + public readonly pluginId: string, + public readonly uid: string, + private readonly extensionsLog: ExtensionsLog + ) { + super(pluginId, uid); + } + + query(request: DataQueryRequest): Observable { + const [query] = request.targets; + + return this.extensionsLog.asObservable().pipe( + scan( + (response, item) => { + const [existing] = response.data; + + return { + data: [createFrame(query, item, existing)], + key: query.key ?? query.refId, + state: LoadingState.Streaming, + }; + }, + { + data: [], + key: query.key ?? query.refId, + state: LoadingState.Streaming, + } + ) + ); + } + + testDatasource(): Promise { + return Promise.resolve({ status: 'success', message: 'OK' }); + } +} + +function createFrame(query: SceneDataQuery, item: ExtensionsLogItem, existing?: DataFrame): DataFrame { + const timestamps = existing?.fields?.[0]?.values ?? []; + const messages = existing?.fields?.[1]?.values ?? []; + const levels = existing?.fields?.[2]?.values ?? []; + const ids = existing?.fields?.[3]?.values ?? []; + const labels = existing?.fields?.[4]?.values ?? []; + const pluginIds = existing?.fields?.[5]?.values ?? []; + const extensionPointIds = existing?.fields?.[6]?.values ?? []; + + return createDataFrame({ + refId: query.refId, + meta: { type: DataFrameType.LogLines }, + fields: [ + { + name: 'timestamp', + type: FieldType.time, + values: [item.timestamp, ...timestamps], + }, + { + name: 'body', + type: FieldType.string, + values: [item.message, ...messages], + }, + { + name: 'severity', + type: FieldType.string, + values: [item.level, ...levels], + }, + { + name: 'id', + type: FieldType.string, + values: [item.id, ...ids], + }, + { + name: 'labels', + type: FieldType.other, + values: [item.labels, ...labels], + }, + { + name: 'pluginId', + type: FieldType.string, + values: [item.pluginId ?? null, ...pluginIds], + }, + { + name: 'extensionPointId', + type: FieldType.string, + values: [item.extensionPointId ?? null, ...extensionPointIds], + }, + ], + }); +} diff --git a/public/app/features/plugins/extensions/logs/filterTransformation.test.ts b/public/app/features/plugins/extensions/logs/filterTransformation.test.ts new file mode 100644 index 00000000000..17ed1800535 --- /dev/null +++ b/public/app/features/plugins/extensions/logs/filterTransformation.test.ts @@ -0,0 +1,361 @@ +import { lastValueFrom, Observable } from 'rxjs'; + +import { DataFrame, FieldType, toDataFrame } from '@grafana/data'; + +import { LogFilter } from './LogViewFilters'; +import { createFilterTransformation } from './filterTransformation'; + +const data = [ + toDataFrame({ + name: 'A', + length: 3, + fields: [ + { name: 'pluginId', type: FieldType.string, values: ['grafana-k8s-app', 'grafana', 'mckn-funnel-panel'] }, + { + name: 'extensionPointId', + type: FieldType.string, + values: [ + 'grafana/explore/toolbar/actions', + 'grafana-k8s-app/clusters/view/v1', + 'grafana/dashboards/panel/menu/v1', + ], + }, + { name: 'severity', type: FieldType.string, values: ['info', 'info', 'info'] }, + ], + }), + toDataFrame({ + name: 'B', + length: 3, + fields: [ + { name: 'pluginId', type: FieldType.string, values: ['grafana-k8s-app', 'grafana', 'mckn-funnel-panel'] }, + { + name: 'extensionPointId', + type: FieldType.string, + values: [ + 'grafana-k8s-app/clusters/view/v1', + 'grafana/dashboards/panel/menu/v1', + 'grafana/explore/toolbar/actions', + ], + }, + { name: 'severity', type: FieldType.string, values: ['debug', 'warning', 'error'] }, + ], + }), +]; + +describe('Transform data frames by filtering', () => { + it('should keep all rows when no filter is applied', async () => { + const [a, b] = await runTransformationWithFilter({}, data); + expect(a.length).toBe(data[0].length); + expect(b.length).toBe(data[1].length); + }); + + it('should exclude all rows not matching pluginId', async () => { + const filter = { + pluginIds: new Set(['grafana-k8s-app']), + }; + const [a, b] = await runTransformationWithFilter(filter, data); + + expect(a.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: ['grafana-k8s-app'], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: ['grafana/explore/toolbar/actions'], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['info'], + }, + ]); + + expect(b.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: ['grafana-k8s-app'], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: ['grafana-k8s-app/clusters/view/v1'], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['debug'], + }, + ]); + }); + + it('should exclude all rows not matching severity', async () => { + const filter = { + severity: new Set(['debug']), + }; + const [a, b] = await runTransformationWithFilter(filter, data); + + expect(a.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: [], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: [], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: [], + }, + ]); + + expect(b.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: ['grafana-k8s-app'], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: ['grafana-k8s-app/clusters/view/v1'], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['debug'], + }, + ]); + }); + + it('should exclude all rows not matching extensionPointId', async () => { + const filter = { + extensionPointIds: new Set(['grafana/dashboards/panel/menu/v1']), + }; + const [a, b] = await runTransformationWithFilter(filter, data); + + expect(a.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: ['mckn-funnel-panel'], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: ['grafana/dashboards/panel/menu/v1'], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['info'], + }, + ]); + + expect(b.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: ['grafana'], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: ['grafana/dashboards/panel/menu/v1'], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['warning'], + }, + ]); + }); + + it('should exclude all rows not matching pluginId and severity', async () => { + const filter = { + pluginIds: new Set(['grafana-k8s-app']), + severity: new Set(['debug']), + }; + const [a, b] = await runTransformationWithFilter(filter, data); + + expect(a.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: [], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: [], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: [], + }, + ]); + + expect(b.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: ['grafana-k8s-app'], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: ['grafana-k8s-app/clusters/view/v1'], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['debug'], + }, + ]); + }); + + it('should exclude all rows not matching pluginId and severity', async () => { + const filter = { + pluginIds: new Set(['grafana-k8s-app', 'grafana']), + severity: new Set(['info']), + }; + const [a, b] = await runTransformationWithFilter(filter, data); + + expect(a.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: ['grafana-k8s-app', 'grafana'], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: ['grafana/explore/toolbar/actions', 'grafana-k8s-app/clusters/view/v1'], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['info', 'info'], + }, + ]); + + expect(b.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: [], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: [], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: [], + }, + ]); + }); + + it('should exclude all rows not matching one of pluginId with severity and extensionPointId', async () => { + const filter = { + pluginIds: new Set(['grafana-k8s-app', 'grafana']), + severity: new Set(['info']), + extensionPointIds: new Set(['grafana/explore/toolbar/actions']), + }; + const [a, b] = await runTransformationWithFilter(filter, data); + + expect(a.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: ['grafana-k8s-app'], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: ['grafana/explore/toolbar/actions'], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['info'], + }, + ]); + + expect(b.fields).toStrictEqual([ + { + config: {}, + name: 'pluginId', + type: FieldType.string, + values: [], + }, + { + config: {}, + name: 'extensionPointId', + type: FieldType.string, + values: [], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: [], + }, + ]); + }); +}); + +function runTransformationWithFilter(filter: LogFilter, frames: DataFrame[]): Promise { + const transformation = createFilterTransformation(filter); + const operator = transformation({ interpolate: () => '' }); + + return lastValueFrom( + new Observable((sub) => { + sub.next(frames); + sub.complete(); + }).pipe(operator) + ); +} diff --git a/public/app/features/plugins/extensions/logs/filterTransformation.ts b/public/app/features/plugins/extensions/logs/filterTransformation.ts new file mode 100644 index 00000000000..32852c38bdc --- /dev/null +++ b/public/app/features/plugins/extensions/logs/filterTransformation.ts @@ -0,0 +1,86 @@ +import { isEmpty } from 'lodash'; +import { Observable, scan } from 'rxjs'; + +import { createDataFrame, CustomTransformOperator, DataFrame, PartialDataFrame } from '@grafana/data'; + +import { LogFilter } from './LogViewFilters'; + +export function createFilterTransformation(filter: LogFilter): CustomTransformOperator { + return function cascadingFilterTransformation() { + return function (source: Observable) { + return source.pipe( + scan((filtered: DataFrame[], current) => { + if (isEmpty(filter.extensionPointIds) && isEmpty(filter.pluginIds) && isEmpty(filter.severity)) { + return current; + } + + for (const frame of current) { + const pluginIdIndex = frame.fields.findIndex((f) => f.name === 'pluginId'); + const extensionPointIdIndex = frame.fields.findIndex((f) => f.name === 'extensionPointId'); + const severityIndex = frame.fields.findIndex((f) => f.name === 'severity'); + + if (pluginIdIndex === -1 && !isEmpty(filter.pluginIds)) { + continue; + } + + if (extensionPointIdIndex === -1 && !isEmpty(filter.extensionPointIds)) { + continue; + } + + if (severityIndex === -1 && !isEmpty(filter.severity)) { + continue; + } + + const target: PartialDataFrame = { + ...frame, + fields: frame.fields.map((f) => ({ + ...f, + values: [], + })), + }; + + for (let index = 0; index < frame.length; index++) { + const pluginId = frame.fields[pluginIdIndex].values[index]; + const extensionPointId = frame.fields[extensionPointIdIndex].values[index]; + const severity = frame.fields[severityIndex].values[index]; + + if (!isEmpty(filter.pluginIds) && !filter.pluginIds?.has(pluginId)) { + continue; + } + + if (!isEmpty(filter.extensionPointIds) && !filter.extensionPointIds?.has(extensionPointId)) { + continue; + } + + if (!isEmpty(filter.severity) && !filter.severity?.has(severity)) { + continue; + } + + copyRow(frame, target, index); + } + + filtered.push(createDataFrame(target)); + } + + return filtered; + }, []) + ); + }; + }; +} + +function copyRow(source: DataFrame, target: PartialDataFrame, rowIndex: number) { + for (let index = 0; index < source.fields.length; index++) { + const field = source.fields[index]; + + if (!target.fields[index]) { + target.fields[index] = { + ...field, + values: [], + }; + } + + const value = source.fields[index].values[rowIndex]; + target.fields[index].values?.push(value); + } +} diff --git a/public/app/features/plugins/extensions/logs/log.ts b/public/app/features/plugins/extensions/logs/log.ts new file mode 100644 index 00000000000..a78d42ccea0 --- /dev/null +++ b/public/app/features/plugins/extensions/logs/log.ts @@ -0,0 +1,94 @@ +import { isString } from 'lodash'; +import { nanoid } from 'nanoid'; +import { Observable, ReplaySubject } from 'rxjs'; + +import { Labels, LogLevel } from '@grafana/data'; + +export type ExtensionsLogItem = { + level: LogLevel; + timestamp: number; + labels: Labels; + message: string; + id: string; + pluginId?: string; + extensionPointId?: string; +}; + +const channelName = 'ui-extension-logs'; + +export class ExtensionsLog { + private baseLabels: Labels | undefined; + private subject: ReplaySubject | undefined; + private channel: BroadcastChannel; + + constructor(baseLabels?: Labels, subject?: ReplaySubject, channel?: BroadcastChannel) { + this.baseLabels = baseLabels; + this.channel = channel ?? new BroadcastChannel(channelName); + this.subject = subject; + } + + info(message: string, labels?: Labels): void { + this.log(LogLevel.info, message, labels); + } + + warning(message: string, labels?: Labels): void { + this.log(LogLevel.warning, message, labels); + } + + error(message: string, labels?: Labels): void { + this.log(LogLevel.error, message, labels); + } + + debug(message: string, labels?: Labels): void { + this.log(LogLevel.debug, message, labels); + } + + trace(message: string, labels?: Labels): void { + this.log(LogLevel.trace, message, labels); + } + + fatal(message: string, labels?: Labels): void { + this.log(LogLevel.fatal, message, labels); + } + + private log(level: LogLevel, message: string, labels?: Labels): void { + const combinedLabels = { ...labels, ...this.baseLabels }; + const { pluginId, extensionPointId } = combinedLabels; + + const item: ExtensionsLogItem = { + level: level, + labels: combinedLabels, + timestamp: Date.now(), + id: nanoid(), + message: message, + pluginId: isString(pluginId) ? pluginId : undefined, + extensionPointId: isString(extensionPointId) ? extensionPointId : undefined, + }; + + this.channel.postMessage(item); + } + + asObservable(): Observable { + if (!this.subject) { + // Lazily create the subject on first subscription to prevent + // to create buffers when no subscribers exists + this.subject = new ReplaySubject(1000, 1000 * 60 * 10); + this.channel.onmessage = (msg: MessageEvent) => this.subject?.next(msg.data); + } + + return this.subject.asObservable(); + } + + child(labels: Labels): ExtensionsLog { + return new ExtensionsLog( + { + ...labels, + ...this.baseLabels, + }, + this.subject, + this.channel + ); + } +} + +export const log = new ExtensionsLog(); diff --git a/public/app/features/plugins/extensions/logs/testUtils.ts b/public/app/features/plugins/extensions/logs/testUtils.ts new file mode 100644 index 00000000000..2877b9075c7 --- /dev/null +++ b/public/app/features/plugins/extensions/logs/testUtils.ts @@ -0,0 +1,31 @@ +import { ExtensionsLog } from './log'; + +export function createLogMock(): ExtensionsLog { + const { log: original } = jest.requireActual('./log'); + + const logMock = { + error: jest.fn(), + warning: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + fatal: jest.fn(), + child: jest.fn(), + }; + + logMock.child.mockReturnValue(logMock); + + return { + ...original, + ...logMock, + }; +} + +export function resetLogMock(log: ExtensionsLog): void { + jest.mocked(log.error).mockReset(); + jest.mocked(log.warning).mockReset(); + jest.mocked(log.info).mockReset(); + jest.mocked(log.debug).mockReset(); + jest.mocked(log.trace).mockReset(); + jest.mocked(log.fatal).mockReset(); +} diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts index 8e0041d185c..fcf3d4616bb 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts @@ -4,6 +4,8 @@ import { firstValueFrom } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { log } from '../logs/log'; +import { resetLogMock } from '../logs/testUtils'; import { isGrafanaDevMode } from '../utils'; import { AddedComponentsRegistry } from './AddedComponentsRegistry'; @@ -17,8 +19,17 @@ jest.mock('../utils', () => ({ isGrafanaDevMode: jest.fn().mockReturnValue(false), })); +jest.mock('../logs/log', () => { + const { createLogMock } = jest.requireActual('../logs/testUtils'); + const original = jest.requireActual('../logs/log'); + + return { + ...original, + log: createLogMock(), + }; +}); + describe('AddedComponentsRegistry', () => { - const consoleWarn = jest.fn(); const originalApps = config.apps; const pluginId = 'grafana-basic-app'; const appPluginConfig = { @@ -47,8 +58,7 @@ describe('AddedComponentsRegistry', () => { }; beforeEach(() => { - global.console.warn = consoleWarn; - consoleWarn.mockReset(); + resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); config.apps = { [pluginId]: appPluginConfig, @@ -363,8 +373,8 @@ describe('AddedComponentsRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalledWith( - `[Plugin Extensions] Added component "Component 1 title": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.` + expect(log.warning).toHaveBeenCalledWith( + `Added component "Component 1 title": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.` ); const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); @@ -386,8 +396,8 @@ describe('AddedComponentsRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalledWith( - "[Plugin Extensions] Could not register added component with title 'Component 1 title'. Reason: Description is missing." + expect(log.error).toHaveBeenCalledWith( + "Could not register added component with title 'Component 1 title'. Reason: Description is missing." ); const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); @@ -401,7 +411,7 @@ describe('AddedComponentsRegistry', () => { pluginId, configs: [ { - title: 'Component 1 title', + title: '', description: '', targets: [extensionPointId], component: () => React.createElement('div', null, 'Hello World1'), @@ -409,9 +419,7 @@ describe('AddedComponentsRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalledWith( - "[Plugin Extensions] Could not register added component with title 'Component 1 title'. Reason: Description is missing." - ); + expect(log.error).toHaveBeenCalledWith('Could not register added component. Reason: Title is missing.'); const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); @@ -497,7 +505,7 @@ describe('AddedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); - expect(consoleWarn).toHaveBeenCalled(); + expect(log.warning).toHaveBeenCalled(); }); it('should register a component added by a core Grafana in dev-mode even if the meta-info is missing', async () => { @@ -520,7 +528,7 @@ describe('AddedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(consoleWarn).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should register a component added by a plugin in production mode even if the meta-info is missing', async () => { @@ -546,7 +554,7 @@ describe('AddedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(consoleWarn).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should register a component added by a plugin in dev-mode if the meta-info is present', async () => { @@ -572,6 +580,6 @@ describe('AddedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(consoleWarn).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts index 1760f3d36cf..a57f56c6008 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts @@ -2,7 +2,7 @@ import { ReplaySubject } from 'rxjs'; import { PluginExtensionAddedComponentConfig } from '@grafana/data'; -import { isAddedComponentMetaInfoMissing, isGrafanaDevMode, logWarning, wrapWithPluginContext } from '../utils'; +import { isAddedComponentMetaInfoMissing, isGrafanaDevMode, wrapWithPluginContext } from '../utils'; import { extensionPointEndsWithVersion, isGrafanaCoreExtensionPoint, isReactComponent } from '../validators'; import { PluginExtensionConfigs, Registry, RegistryType } from './Registry'; @@ -34,42 +34,58 @@ export class AddedComponentsRegistry extends Registry< const { pluginId, configs } = item; for (const config of configs) { + const configLog = this.logger.child({ + description: config.description, + title: config.title, + pluginId, + }); + if (!isReactComponent(config.component)) { - logWarning( - `Could not register added component with title '${config.title}'. Reason: The provided component is not a valid React component.` + configLog.error( + `Could not register added component. Reason: The provided component is not a valid React component.` ); continue; } if (!config.title) { - logWarning(`Could not register added component with title '${config.title}'. Reason: Title is missing.`); + configLog.error(`Could not register added component. Reason: Title is missing.`); continue; } if (!config.description) { - logWarning(`Could not register added component with title '${config.title}'. Reason: Description is missing.`); + configLog.error( + `Could not register added component with title '${config.title}'. Reason: Description is missing.` + ); continue; } - if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedComponentMetaInfoMissing(pluginId, config)) { + if ( + pluginId !== 'grafana' && + isGrafanaDevMode() && + isAddedComponentMetaInfoMissing(pluginId, config, configLog) + ) { continue; } const extensionPointIds = Array.isArray(config.targets) ? config.targets : [config.targets]; for (const extensionPointId of extensionPointIds) { + const pointIdLog = configLog.child({ extensionPointId }); + if (!isGrafanaCoreExtensionPoint(extensionPointId) && !extensionPointEndsWithVersion(extensionPointId)) { - logWarning( + pointIdLog.warning( `Added component "${config.title}": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.` ); } const result = { pluginId, - component: wrapWithPluginContext(pluginId, config.component), + component: wrapWithPluginContext(pluginId, config.component, pointIdLog), description: config.description, title: config.title, }; + pointIdLog.debug(`Added component from '${pluginId}' to '${extensionPointId}'`); + if (!(extensionPointId in registry)) { registry[extensionPointId] = [result]; } else { diff --git a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts index 45735806af9..3dd5b7c7f7d 100644 --- a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts @@ -3,6 +3,8 @@ import { firstValueFrom } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { log } from '../logs/log'; +import { resetLogMock } from '../logs/testUtils'; import { isGrafanaDevMode } from '../utils'; import { AddedLinksRegistry } from './AddedLinksRegistry'; @@ -16,9 +18,18 @@ jest.mock('../utils', () => ({ isGrafanaDevMode: jest.fn().mockReturnValue(false), })); +jest.mock('../logs/log', () => { + const { createLogMock } = jest.requireActual('../logs/testUtils'); + const original = jest.requireActual('../logs/log'); + + return { + ...original, + log: createLogMock(), + }; +}); + describe('AddedLinksRegistry', () => { const originalApps = config.apps; - const consoleWarn = jest.fn(); const pluginId = 'grafana-basic-app'; const appPluginConfig = { id: pluginId, @@ -46,8 +57,7 @@ describe('AddedLinksRegistry', () => { }; beforeEach(() => { - global.console.warn = consoleWarn; - consoleWarn.mockReset(); + resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); config.apps = { [pluginId]: appPluginConfig, @@ -503,7 +513,7 @@ describe('AddedLinksRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); observable.subscribe(subscribeCallback); expect(subscribeCallback).toHaveBeenCalledTimes(1); @@ -531,7 +541,7 @@ describe('AddedLinksRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); observable.subscribe(subscribeCallback); expect(subscribeCallback).toHaveBeenCalledTimes(1); @@ -559,7 +569,7 @@ describe('AddedLinksRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); observable.subscribe(subscribeCallback); expect(subscribeCallback).toHaveBeenCalledTimes(1); @@ -651,7 +661,7 @@ describe('AddedLinksRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); - expect(consoleWarn).toHaveBeenCalled(); + expect(log.warning).toHaveBeenCalled(); }); it('should register a link added by core Grafana in dev-mode even if the meta-info is missing', async () => { @@ -675,7 +685,7 @@ describe('AddedLinksRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(consoleWarn).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should register a link added by a plugin in production mode even if the meta-info is missing', async () => { @@ -702,7 +712,7 @@ describe('AddedLinksRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(consoleWarn).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should register a link added by a plugin in dev-mode if the meta-info is present', async () => { @@ -729,6 +739,6 @@ describe('AddedLinksRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(consoleWarn).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts index d2f6207e496..0e978b993c6 100644 --- a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts +++ b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts @@ -3,7 +3,7 @@ import { ReplaySubject } from 'rxjs'; import { IconName, PluginExtensionAddedLinkConfig } from '@grafana/data'; import { PluginAddedLinksConfigureFunc, PluginExtensionEventHelpers } from '@grafana/data/src/types/pluginExtensions'; -import { isAddedLinkMetaInfoMissing, isGrafanaDevMode, logWarning } from '../utils'; +import { isAddedLinkMetaInfoMissing, isGrafanaDevMode } from '../utils'; import { extensionPointEndsWithVersion, isConfigureFnValid, @@ -43,43 +43,53 @@ export class AddedLinksRegistry extends Registry ({ isGrafanaDevMode: jest.fn().mockReturnValue(false), })); +jest.mock('../logs/log', () => { + const { createLogMock } = jest.requireActual('../logs/testUtils'); + const original = jest.requireActual('../logs/log'); + + return { + ...original, + log: createLogMock(), + }; +}); + describe('ExposedComponentsRegistry', () => { - const consoleWarn = jest.fn(); const originalApps = config.apps; const pluginId = 'grafana-basic-app'; const appPluginConfig = { @@ -47,8 +58,7 @@ describe('ExposedComponentsRegistry', () => { }; beforeEach(() => { - global.console.warn = consoleWarn; - consoleWarn.mockReset(); + resetLogMock(log); jest.mocked(isGrafanaDevMode).mockReturnValue(false); config.apps = { [pluginId]: appPluginConfig, @@ -282,7 +292,7 @@ describe('ExposedComponentsRegistry', () => { }); registry.register({ - pluginId: 'grafana-basic-app2', + pluginId: 'grafana-basic-app1', configs: [ { id: 'grafana-basic-app1/hello-world/v1', // incorrectly scoped @@ -293,8 +303,8 @@ describe('ExposedComponentsRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalledWith( - "[Plugin Extensions] Could not register exposed component with id 'grafana-basic-app1/hello-world/v1'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'." + expect(log.error).toHaveBeenCalledWith( + "Could not register exposed component with 'grafana-basic-app1/hello-world/v1'. Reason: An exposed component with the same id already exists." ); const currentState2 = await registry.getState(); expect(Object.keys(currentState2)).toHaveLength(1); @@ -314,14 +324,14 @@ describe('ExposedComponentsRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalledWith( - "[Plugin Extensions] Could not register exposed component with id 'hello-world/v1'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'." + expect(log.error).toHaveBeenCalledWith( + "Could not register exposed component with 'hello-world/v1'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'." ); const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); }); - it('should log a warning when exposed component id is not suffixed with component version', async () => { + it('should log a error when exposed component id is not suffixed with component version', async () => { const registry = new ExposedComponentsRegistry(); registry.register({ pluginId: 'grafana-basic-app1', @@ -335,8 +345,8 @@ describe('ExposedComponentsRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalledWith( - "[Plugin Extensions] Exposed component with id 'grafana-basic-app1/hello-world' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'." + expect(log.error).toHaveBeenCalledWith( + "Exposed component does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'." ); const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); @@ -357,8 +367,8 @@ describe('ExposedComponentsRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalledWith( - "[Plugin Extensions] Could not register exposed component with id 'grafana-basic-app/hello-world/v1'. Reason: Description is missing." + expect(log.error).toHaveBeenCalledWith( + "Could not register exposed component with id 'grafana-basic-app/hello-world/v1'. Reason: Description is missing." ); const currentState = await registry.getState(); @@ -380,8 +390,8 @@ describe('ExposedComponentsRegistry', () => { ], }); - expect(consoleWarn).toHaveBeenCalledWith( - "[Plugin Extensions] Could not register exposed component with id 'grafana-basic-app/hello-world/v1'. Reason: Title is missing." + expect(log.error).toHaveBeenCalledWith( + "Could not register exposed component with id 'grafana-basic-app/hello-world/v1'. Reason: Title is missing." ); const currentState = await registry.getState(); @@ -468,7 +478,7 @@ describe('ExposedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(0); - expect(consoleWarn).toHaveBeenCalled(); + expect(log.warning).toHaveBeenCalled(); }); it('should register an exposed component added by a core Grafana in dev-mode even if the meta-info is missing', async () => { @@ -491,7 +501,7 @@ describe('ExposedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(consoleWarn).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should register an exposed component added by a plugin in production mode even if the meta-info is missing', async () => { @@ -517,7 +527,7 @@ describe('ExposedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(consoleWarn).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should register an exposed component added by a plugin in dev-mode if the meta-info is present', async () => { @@ -543,6 +553,6 @@ describe('ExposedComponentsRegistry', () => { const currentState = await registry.getState(); expect(Object.keys(currentState)).toHaveLength(1); - expect(consoleWarn).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts index c84c89ab199..85f3776f5b9 100644 --- a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts +++ b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.ts @@ -2,7 +2,7 @@ import { ReplaySubject } from 'rxjs'; import { PluginExtensionExposedComponentConfig } from '@grafana/data'; -import { isExposedComponentMetaInfoMissing, isGrafanaDevMode, logWarning } from '../utils'; +import { isExposedComponentMetaInfoMissing, isGrafanaDevMode } from '../utils'; import { extensionPointEndsWithVersion } from '../validators'; import { Registry, RegistryType, PluginExtensionConfigs } from './Registry'; @@ -37,41 +37,53 @@ export class ExposedComponentsRegistry extends Registry< for (const config of configs) { const { id, description, title } = config; + const pointIdLog = this.logger.child({ + extensionPointId: id, + description, + title, + pluginId, + }); if (!id.startsWith(pluginId)) { - logWarning( - `Could not register exposed component with id '${id}'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'.` + pointIdLog.error( + `Could not register exposed component with '${id}'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id. e.g 'myorg-basic-app/my-component-id/v1'.` ); continue; } if (!extensionPointEndsWithVersion(id)) { - logWarning( - `Exposed component with id '${id}' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.` + pointIdLog.error( + `Exposed component does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.` ); } if (registry[id]) { - logWarning( - `Could not register exposed component with id '${id}'. Reason: An exposed component with the same id already exists.` + pointIdLog.error( + `Could not register exposed component with '${id}'. Reason: An exposed component with the same id already exists.` ); continue; } if (!title) { - logWarning(`Could not register exposed component with id '${id}'. Reason: Title is missing.`); + pointIdLog.error(`Could not register exposed component with id '${id}'. Reason: Title is missing.`); continue; } if (!description) { - logWarning(`Could not register exposed component with id '${id}'. Reason: Description is missing.`); + pointIdLog.error(`Could not register exposed component with id '${id}'. Reason: Description is missing.`); continue; } - if (pluginId !== 'grafana' && isGrafanaDevMode() && isExposedComponentMetaInfoMissing(pluginId, config)) { + if ( + pluginId !== 'grafana' && + isGrafanaDevMode() && + isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog) + ) { continue; } + pointIdLog.debug(`Exposed component from '${pluginId}' to '${id}'`); + registry[id] = { ...config, pluginId }; } diff --git a/public/app/features/plugins/extensions/registry/Registry.ts b/public/app/features/plugins/extensions/registry/Registry.ts index 33470990e37..b2fbfbc09d8 100644 --- a/public/app/features/plugins/extensions/registry/Registry.ts +++ b/public/app/features/plugins/extensions/registry/Registry.ts @@ -1,5 +1,6 @@ import { Observable, ReplaySubject, Subject, firstValueFrom, map, scan, startWith } from 'rxjs'; +import { ExtensionsLog, log } from '../logs/log'; import { deepFreeze } from '../utils'; export const MSG_CANNOT_REGISTER_READ_ONLY = 'Cannot register to a read-only registry'; @@ -19,6 +20,7 @@ export abstract class Registry { private isReadOnly: boolean; // This is the subject that receives extension configs for a loaded plugin. private resultSubject: Subject>; + protected logger: ExtensionsLog; // This is the subject that we expose. // (It will buffer the last value on the stream - the registry - and emit it to new subscribers immediately.) protected registrySubject: ReplaySubject>; @@ -26,8 +28,10 @@ export abstract class Registry { constructor(options: { registrySubject?: ReplaySubject>; initialState?: RegistryType; + log?: ExtensionsLog; }) { this.resultSubject = new Subject>(); + this.logger = options.log ?? log; this.isReadOnly = false; // If the registry subject (observable) is provided, it means that all the registry updates are taken care of outside of this class -> it is read-only. @@ -41,7 +45,7 @@ export abstract class Registry { this.registrySubject = new ReplaySubject>(1); this.resultSubject .pipe( - scan(this.mapToRegistry, options.initialState ?? {}), + scan(this.mapToRegistry.bind(this), options.initialState ?? {}), // Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values) startWith(options.initialState ?? {}), map((registry) => deepFreeze(registry)) diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index 7cf467b5e38..365dd520e91 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -5,6 +5,8 @@ import { PluginContextProvider, PluginLoadingStrategy, PluginMeta, PluginType } import { config } from '@grafana/runtime'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; +import { log } from './logs/log'; +import { resetLogMock } from './logs/testUtils'; import { setupPluginExtensionRegistries } from './registry/setup'; import { PluginExtensionRegistries } from './registry/types'; import { usePluginComponent } from './usePluginComponent'; @@ -30,11 +32,20 @@ jest.mock('./utils', () => ({ wrapWithPluginContext: jest.fn().mockImplementation((_, component: React.ReactNode) => component), })); +jest.mock('./logs/log', () => { + const { createLogMock } = jest.requireActual('./logs/testUtils'); + const original = jest.requireActual('./logs/log'); + + return { + ...original, + log: createLogMock(), + }; +}); + describe('usePluginComponent()', () => { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; - let consoleWarnSpy: jest.SpyInstance; const originalApps = config.apps; const pluginId = 'myorg-extensions-app'; const exposedComponentId = `${pluginId}/exposed-component/v1`; @@ -74,7 +85,7 @@ describe('usePluginComponent()', () => { beforeEach(() => { registries = setupPluginExtensionRegistries(); jest.mocked(isGrafanaDevMode).mockReturnValue(false); - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + resetLogMock(log); jest.mocked(wrapWithPluginContext).mockClear(); @@ -221,7 +232,7 @@ describe('usePluginComponent()', () => { // (No restrictions due to isGrafanaDevMode() = false) let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper }); expect(result.current.component).not.toBe(null); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should not validate the meta-info in core Grafana', () => { @@ -245,7 +256,7 @@ describe('usePluginComponent()', () => { }); expect(result.current.component).not.toBe(null); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should validate the meta-info in dev mode and if inside a plugin', () => { @@ -277,7 +288,7 @@ describe('usePluginComponent()', () => { // Shouldn't return the component, as it's not present in the plugin.json dependencies let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper }); expect(result.current.component).toBe(null); - expect(consoleWarnSpy).toHaveBeenCalled(); + expect(log.warning).toHaveBeenCalled(); }); it('should return the exposed component if the meta-info is correct and in dev mode', () => { @@ -307,6 +318,6 @@ describe('usePluginComponent()', () => { let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper }); expect(result.current.component).not.toBe(null); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/usePluginComponent.tsx b/public/app/features/plugins/extensions/usePluginComponent.tsx index 9ae43c27bbf..a32b6aa974c 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.tsx @@ -2,9 +2,10 @@ import { useMemo } from 'react'; import { useObservable } from 'react-use'; import { usePluginContext } from '@grafana/data'; -import { logWarning, UsePluginComponentResult } from '@grafana/runtime'; +import { UsePluginComponentResult } from '@grafana/runtime'; import { useExposedComponentsRegistry } from './ExtensionRegistriesContext'; +import { log } from './logs/log'; import { isExposedComponentDependencyMissing, isGrafanaDevMode, wrapWithPluginContext } from './utils'; // Returns a component exposed by a plugin. @@ -18,16 +19,6 @@ export function usePluginComponent(id: string): UsePl // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana. const enableRestrictions = isGrafanaDevMode() && pluginContext; - if (enableRestrictions && isExposedComponentDependencyMissing(id, pluginContext)) { - logWarning( - `usePluginComponent("${id}") - The exposed component ("${id}") is missing from the dependencies[] in the "plugin.json" file.` - ); - return { - isLoading: false, - component: null, - }; - } - if (!registryState?.[id]) { return { isLoading: false, @@ -36,10 +27,25 @@ export function usePluginComponent(id: string): UsePl } const registryItem = registryState[id]; + const componentLog = log.child({ + title: registryItem.title, + description: registryItem.description, + pluginId: registryItem.pluginId, + }); + + if (enableRestrictions && isExposedComponentDependencyMissing(id, pluginContext, componentLog)) { + componentLog.warning( + `usePluginComponent("${id}") - The exposed component ("${id}") is missing from the dependencies[] in the "plugin.json" file.` + ); + return { + isLoading: false, + component: null, + }; + } return { isLoading: false, - component: wrapWithPluginContext(registryItem.pluginId, registryItem.component), + component: wrapWithPluginContext(registryItem.pluginId, registryItem.component, componentLog), }; }, [id, pluginContext, registryState]); } diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index 0c37e90e549..d174a871cc7 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -4,6 +4,8 @@ import { renderHook } from '@testing-library/react-hooks'; import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; +import { log } from './logs/log'; +import { resetLogMock } from './logs/testUtils'; import { setupPluginExtensionRegistries } from './registry/setup'; import { PluginExtensionRegistries } from './registry/types'; import { usePluginComponents } from './usePluginComponents'; @@ -29,18 +31,27 @@ jest.mock('./utils', () => ({ wrapWithPluginContext: jest.fn().mockImplementation((_, component: React.ReactNode) => component), })); +jest.mock('./logs/log', () => { + const { createLogMock } = jest.requireActual('./logs/testUtils'); + const original = jest.requireActual('./logs/log'); + + return { + ...original, + log: createLogMock(), + }; +}); + describe('usePluginComponents()', () => { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; - let consoleWarnSpy: jest.SpyInstance; const pluginId = 'myorg-extensions-app'; const extensionPointId = `${pluginId}/extension-point/v1`; beforeEach(() => { jest.mocked(isGrafanaDevMode).mockReturnValue(false); + resetLogMock(log); registries = setupPluginExtensionRegistries(); - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); jest.mocked(wrapWithPluginContext).mockClear(); @@ -251,7 +262,7 @@ describe('usePluginComponents()', () => { // (No restrictions due to isGrafanaDevMode() = false) let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); expect(result.current.components.length).toBe(1); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should not validate the extension point id in production mode', () => { @@ -276,7 +287,7 @@ describe('usePluginComponents()', () => { wrapper, }); expect(result.current.components.length).toBe(0); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should not validate the extension point meta-info if used in Grafana core (no plugin context)', () => { @@ -305,7 +316,7 @@ describe('usePluginComponents()', () => { wrapper, }); expect(result.current.components.length).toBe(1); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should not validate the extension point id if used in Grafana core (no plugin context)', () => { @@ -321,7 +332,7 @@ describe('usePluginComponents()', () => { wrapper, }); expect(result.current.components.length).toBe(0); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should validate if the extension point meta-info is correct if in dev-mode and used by a plugin', () => { @@ -359,7 +370,7 @@ describe('usePluginComponents()', () => { // Trying to render an extension point that is not defined in the plugin meta let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); expect(result.current.components.length).toBe(0); - expect(consoleWarnSpy).toHaveBeenCalled(); + expect(log.warning).toHaveBeenCalled(); }); it('should not log a warning if the extension point meta-info is correct if in dev-mode and used by a plugin', () => { @@ -403,6 +414,6 @@ describe('usePluginComponents()', () => { // Trying to render an extension point that is not defined in the plugin meta let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); expect(result.current.components.length).toBe(0); - expect(consoleWarnSpy).toHaveBeenCalled(); + expect(log.warning).toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/usePluginComponents.tsx b/public/app/features/plugins/extensions/usePluginComponents.tsx index 92dbc4e676c..45a6c6d9487 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.tsx @@ -8,7 +8,8 @@ import { } from '@grafana/runtime/src/services/pluginExtensions/getPluginExtensions'; import { useAddedComponentsRegistry } from './ExtensionRegistriesContext'; -import { isExtensionPointMetaInfoMissing, isGrafanaDevMode, logWarning } from './utils'; +import { log } from './logs/log'; +import { isExtensionPointMetaInfoMissing, isGrafanaDevMode } from './utils'; import { isExtensionPointIdValid } from './validators'; // Returns an array of component extensions for the given extension point @@ -26,9 +27,13 @@ export function usePluginComponents({ const components: Array> = []; const extensionsByPlugin: Record = {}; const pluginId = pluginContext?.meta.id ?? ''; + const pointLog = log.child({ + pluginId, + extensionPointId, + }); if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { - logWarning( + pointLog.warning( `Extension point usePluginComponents("${extensionPointId}") - the id should be prefixed with your plugin id ("${pluginId}/").` ); return { @@ -37,8 +42,8 @@ export function usePluginComponents({ }; } - if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { - logWarning( + if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, pointLog)) { + pointLog.warning( `usePluginComponents("${extensionPointId}") - The extension point is missing from the "plugin.json" file.` ); return { diff --git a/public/app/features/plugins/extensions/usePluginExtensions.tsx b/public/app/features/plugins/extensions/usePluginExtensions.tsx index 8b5c3e69f37..2ab882108de 100644 --- a/public/app/features/plugins/extensions/usePluginExtensions.tsx +++ b/public/app/features/plugins/extensions/usePluginExtensions.tsx @@ -6,8 +6,9 @@ import { GetPluginExtensionsOptions, UsePluginExtensionsResult } from '@grafana/ import { useSidecar } from 'app/core/context/SidecarContext'; import { getPluginExtensions } from './getPluginExtensions'; +import { log } from './logs/log'; import { PluginExtensionRegistries } from './registry/types'; -import { isExtensionPointMetaInfoMissing, isGrafanaDevMode, logWarning } from './utils'; +import { isExtensionPointMetaInfoMissing, isGrafanaDevMode } from './utils'; import { isExtensionPointIdValid } from './validators'; export function createUsePluginExtensions(registries: PluginExtensionRegistries) { @@ -25,13 +26,17 @@ export function createUsePluginExtensions(registries: PluginExtensionRegistries) // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana. const enableRestrictions = isGrafanaDevMode() && pluginContext !== null; const pluginId = pluginContext?.meta.id ?? ''; + const pointLog = log.child({ + pluginId, + extensionPointId, + }); if (!addedLinksRegistry && !addedComponentsRegistry) { return { extensions: [], isLoading: false }; } if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { - logWarning( + pointLog.warning( `Extension point usePluginExtensions("${extensionPointId}") - the id should be prefixed with your plugin id ("${pluginId}/").` ); return { @@ -40,8 +45,8 @@ export function createUsePluginExtensions(registries: PluginExtensionRegistries) }; } - if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { - logWarning( + if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, pointLog)) { + pointLog.warning( `Invalid extension point. Reason: The extension point is not declared in the "plugin.json" file. ExtensionPointId: "${extensionPointId}"` ); return { diff --git a/public/app/features/plugins/extensions/usePluginLinks.test.tsx b/public/app/features/plugins/extensions/usePluginLinks.test.tsx index d4d0cc6a843..8fd92bd944a 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.test.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.test.tsx @@ -4,6 +4,8 @@ import { renderHook } from '@testing-library/react-hooks'; import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; +import { log } from './logs/log'; +import { resetLogMock } from './logs/testUtils'; import { setupPluginExtensionRegistries } from './registry/setup'; import { PluginExtensionRegistries } from './registry/types'; import { usePluginLinks } from './usePluginLinks'; @@ -28,18 +30,27 @@ jest.mock('./utils', () => ({ isGrafanaDevMode: jest.fn().mockReturnValue(false), })); +jest.mock('./logs/log', () => { + const { createLogMock } = jest.requireActual('./logs/testUtils'); + const original = jest.requireActual('./logs/log'); + + return { + ...original, + log: createLogMock(), + }; +}); + describe('usePluginLinks()', () => { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; - let consoleWarnSpy: jest.SpyInstance; const pluginId = 'myorg-extensions-app'; const extensionPointId = `${pluginId}/extension-point/v1`; beforeEach(() => { jest.mocked(isGrafanaDevMode).mockReturnValue(false); registries = setupPluginExtensionRegistries(); - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + resetLogMock(log); pluginMeta = { id: pluginId, @@ -194,7 +205,7 @@ describe('usePluginLinks()', () => { // (No restrictions due to isGrafanaDevMode() = false) let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper }); expect(result.current.links.length).toBe(1); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should not validate the extension point id in production mode', () => { @@ -217,7 +228,7 @@ describe('usePluginLinks()', () => { // (No restrictions due to isGrafanaDevMode() = false) let { result } = renderHook(() => usePluginLinks({ extensionPointId: 'invalid-extension-point-id' }), { wrapper }); expect(result.current.links.length).toBe(0); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should not validate the extension point meta-info if used in Grafana core (no plugin context)', () => { @@ -244,7 +255,7 @@ describe('usePluginLinks()', () => { let { result } = renderHook(() => usePluginLinks({ extensionPointId: 'grafana/extension-point/v1' }), { wrapper }); expect(result.current.links.length).toBe(1); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should not validate the extension point id if used in Grafana core (no plugin context)', () => { @@ -258,7 +269,7 @@ describe('usePluginLinks()', () => { let { result } = renderHook(() => usePluginLinks({ extensionPointId: 'invalid-extension-point-id' }), { wrapper }); expect(result.current.links.length).toBe(0); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(log.warning).not.toHaveBeenCalled(); }); it('should validate if the extension point meta-info is correct if in dev-mode and used by a plugin', () => { @@ -296,7 +307,7 @@ describe('usePluginLinks()', () => { // Trying to render an extension point that is not defined in the plugin meta let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper }); expect(result.current.links.length).toBe(0); - expect(consoleWarnSpy).toHaveBeenCalled(); + expect(log.warning).toHaveBeenCalled(); }); it('should not log a warning if the extension point meta-info is correct if in dev-mode and used by a plugin', () => { @@ -334,6 +345,6 @@ describe('usePluginLinks()', () => { // Trying to render an extension point that is not defined in the plugin meta let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper }); expect(result.current.links.length).toBe(0); - expect(consoleWarnSpy).toHaveBeenCalled(); + expect(log.warning).toHaveBeenCalled(); }); }); diff --git a/public/app/features/plugins/extensions/usePluginLinks.tsx b/public/app/features/plugins/extensions/usePluginLinks.tsx index 1f868b01c77..bd9bba9bcc9 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.tsx @@ -9,6 +9,7 @@ import { } from '@grafana/runtime/src/services/pluginExtensions/getPluginExtensions'; import { useAddedLinksRegistry } from './ExtensionRegistriesContext'; +import { log } from './logs/log'; import { generateExtensionId, getLinkExtensionOnClick, @@ -17,7 +18,6 @@ import { getReadOnlyProxy, isExtensionPointMetaInfoMissing, isGrafanaDevMode, - logWarning, } from './utils'; import { isExtensionPointIdValid } from './validators'; @@ -35,9 +35,13 @@ export function usePluginLinks({ // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana. const enableRestrictions = isGrafanaDevMode() && pluginContext !== null; const pluginId = pluginContext?.meta.id ?? ''; + const pointLog = log.child({ + pluginId, + extensionPointId, + }); if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { - logWarning( + pointLog.warning( `Extension point usePluginLinks("${extensionPointId}") - the id should be prefixed with your plugin id ("${pluginId}/").` ); return { @@ -46,8 +50,8 @@ export function usePluginLinks({ }; } - if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { - logWarning( + if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, pointLog)) { + pointLog.warning( `Invalid extension point. Reason: The extension point is not declared in the "plugin.json" file. ExtensionPointId: "${extensionPointId}"` ); return { @@ -78,8 +82,14 @@ export function usePluginLinks({ extensionsByPlugin[pluginId] = 0; } + const linkLog = pointLog.child({ + path: addedLink.path ?? '', + title: addedLink.title, + description: addedLink.description, + onClick: typeof addedLink.onClick, + }); // Run the configure() function with the current context, and apply the ovverides - const overrides = getLinkExtensionOverrides(pluginId, addedLink, frozenContext); + const overrides = getLinkExtensionOverrides(pluginId, addedLink, linkLog, frozenContext); // configure() returned an `undefined` -> hide the extension if (addedLink.configure && overrides === undefined) { @@ -91,7 +101,7 @@ export function usePluginLinks({ id: generateExtensionId(pluginId, extensionPointId, addedLink.title), type: PluginExtensionTypes.link, pluginId: pluginId, - onClick: getLinkExtensionOnClick(pluginId, extensionPointId, addedLink, frozenContext), + onClick: getLinkExtensionOnClick(pluginId, extensionPointId, addedLink, linkLog, frozenContext), // Configurable properties icon: overrides?.icon || addedLink.icon, diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index 3697fe0b71f..c090e2ac232 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -13,6 +13,8 @@ import { config } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import { ShowModalReactEvent } from 'app/types/events'; +import { log } from './logs/log'; +import { createLogMock } from './logs/testUtils'; import { deepFreeze, handleErrorsInFn, @@ -441,7 +443,7 @@ describe('Plugin Extensions / Utils', () => { it('should make the plugin context available for the wrapped component', async () => { const pluginId = 'grafana-worldmap-panel'; - const Component = wrapWithPluginContext(pluginId, ExampleComponent); + const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); render(); @@ -451,7 +453,7 @@ describe('Plugin Extensions / Utils', () => { it('should pass the properties into the wrapped component', async () => { const pluginId = 'grafana-worldmap-panel'; - const Component = wrapWithPluginContext(pluginId, ExampleComponent); + const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); render(); @@ -461,7 +463,6 @@ describe('Plugin Extensions / Utils', () => { }); describe('isAddedLinkMetaInfoMissing()', () => { - let consoleWarnSpy: jest.SpyInstance; const originalApps = config.apps; const pluginId = 'myorg-extensions-app'; const appPluginConfig = { @@ -495,7 +496,6 @@ describe('Plugin Extensions / Utils', () => { }; beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); config.apps = { [pluginId]: appPluginConfig, }; @@ -506,63 +506,75 @@ describe('Plugin Extensions / Utils', () => { }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { + const log = createLogMock(); config.apps[pluginId].extensions.addedLinks.push(extensionConfig); - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig); + const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); expect(returnValue).toBe(false); - expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + expect(log.warning).toHaveBeenCalledTimes(0); }); it('should return TRUE and log a warning if the app config is not found', () => { + const log = createLogMock(); delete config.apps[pluginId]; - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig); + const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch("couldn't find app plugin"); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch("couldn't find app plugin"); }); it('should return TRUE and log a warning if the link has no meta-info in the plugin.json', () => { + const log = createLogMock(); config.apps[pluginId].extensions.addedLinks = []; - const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig); + const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig, log); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch('not registered in the plugin.json'); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('not registered in the plugin.json'); }); it('should return TRUE and log a warning if the "targets" do not match', () => { + const log = createLogMock(); config.apps[pluginId].extensions.addedLinks.push(extensionConfig); - const returnValue = isAddedLinkMetaInfoMissing(pluginId, { - ...extensionConfig, - targets: [PluginExtensionPoints.DashboardPanelMenu, PluginExtensionPoints.ExploreToolbarAction], - }); + const returnValue = isAddedLinkMetaInfoMissing( + pluginId, + { + ...extensionConfig, + targets: [PluginExtensionPoints.DashboardPanelMenu, PluginExtensionPoints.ExploreToolbarAction], + }, + log + ); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"targets" don\'t match'); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"targets" don\'t match'); }); it('should return TRUE and log a warning if the "description" does not match', () => { + const log = createLogMock(); config.apps[pluginId].extensions.addedLinks.push(extensionConfig); - const returnValue = isAddedLinkMetaInfoMissing(pluginId, { - ...extensionConfig, - description: 'Link description UPDATED', - }); + const returnValue = isAddedLinkMetaInfoMissing( + pluginId, + { + ...extensionConfig, + description: 'Link description UPDATED', + }, + log + ); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"description" doesn\'t match'); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"description" doesn\'t match'); }); }); describe('isAddedComponentMetaInfoMissing()', () => { - let consoleWarnSpy: jest.SpyInstance; const originalApps = config.apps; const pluginId = 'myorg-extensions-app'; const appPluginConfig = { @@ -597,7 +609,6 @@ describe('Plugin Extensions / Utils', () => { }; beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); config.apps = { [pluginId]: appPluginConfig, }; @@ -608,63 +619,75 @@ describe('Plugin Extensions / Utils', () => { }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { + const log = createLogMock(); config.apps[pluginId].extensions.addedComponents.push(extensionConfig); - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig); + const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); expect(returnValue).toBe(false); - expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + expect(log.warning).toHaveBeenCalledTimes(0); }); it('should return TRUE and log a warning if the app config is not found', () => { + const log = createLogMock(); delete config.apps[pluginId]; - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig); + const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch("couldn't find app plugin"); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch("couldn't find app plugin"); }); it('should return TRUE and log a warning if the Component has no meta-info in the plugin.json', () => { + const log = createLogMock(); config.apps[pluginId].extensions.addedComponents = []; - const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig); + const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig, log); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch('not registered in the plugin.json'); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('not registered in the plugin.json'); }); it('should return TRUE and log a warning if the "targets" do not match', () => { + const log = createLogMock(); config.apps[pluginId].extensions.addedComponents.push(extensionConfig); - const returnValue = isAddedComponentMetaInfoMissing(pluginId, { - ...extensionConfig, - targets: [PluginExtensionPoints.ExploreToolbarAction], - }); + const returnValue = isAddedComponentMetaInfoMissing( + pluginId, + { + ...extensionConfig, + targets: [PluginExtensionPoints.ExploreToolbarAction], + }, + log + ); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"targets" don\'t match'); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"targets" don\'t match'); }); it('should return TRUE and log a warning if the "description" does not match', () => { + const log = createLogMock(); config.apps[pluginId].extensions.addedComponents.push(extensionConfig); - const returnValue = isAddedComponentMetaInfoMissing(pluginId, { - ...extensionConfig, - description: 'UPDATED', - }); + const returnValue = isAddedComponentMetaInfoMissing( + pluginId, + { + ...extensionConfig, + description: 'UPDATED', + }, + log + ); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"description" doesn\'t match'); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"description" doesn\'t match'); }); }); describe('isExposedComponentMetaInfoMissing()', () => { - let consoleWarnSpy: jest.SpyInstance; const originalApps = config.apps; const pluginId = 'myorg-extensions-app'; const appPluginConfig = { @@ -699,7 +722,6 @@ describe('Plugin Extensions / Utils', () => { }; beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); config.apps = { [pluginId]: appPluginConfig, }; @@ -710,69 +732,80 @@ describe('Plugin Extensions / Utils', () => { }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { + const log = createLogMock(); config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig); + const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); expect(returnValue).toBe(false); - expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + expect(log.warning).toHaveBeenCalledTimes(0); }); it('should return TRUE and log a warning if the app config is not found', () => { + const log = createLogMock(); delete config.apps[pluginId]; - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig); + const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch("couldn't find app plugin"); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch("couldn't find app plugin"); }); it('should return TRUE and log a warning if the exposed component has no meta-info in the plugin.json', () => { + const log = createLogMock(); config.apps[pluginId].extensions.exposedComponents = []; - const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig); + const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig, log); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch('not registered in the plugin.json'); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('not registered in the plugin.json'); }); it('should return TRUE and log a warning if the title does not match', () => { + const log = createLogMock(); config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); - const returnValue = isExposedComponentMetaInfoMissing(pluginId, { - ...exposedComponentConfig, - title: 'UPDATED', - }); + const returnValue = isExposedComponentMetaInfoMissing( + pluginId, + { + ...exposedComponentConfig, + title: 'UPDATED', + }, + log + ); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"title" doesn\'t match'); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"title" doesn\'t match'); }); it('should return TRUE and log a warning if the "description" does not match', () => { + const log = createLogMock(); config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); - const returnValue = isExposedComponentMetaInfoMissing(pluginId, { - ...exposedComponentConfig, - description: 'UPDATED', - }); + const returnValue = isExposedComponentMetaInfoMissing( + pluginId, + { + ...exposedComponentConfig, + description: 'UPDATED', + }, + log + ); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch('"description" doesn\'t match'); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"description" doesn\'t match'); }); }); describe('isExposedComponentDependencyMissing()', () => { - let consoleWarnSpy: jest.SpyInstance; let pluginContext: PluginContextType; const pluginId = 'myorg-extensions-app'; const exposedComponentId = `${pluginId}/component/v1`; beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); pluginContext = { meta: { id: pluginId, @@ -806,35 +839,37 @@ describe('Plugin Extensions / Utils', () => { }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { + const log = createLogMock(); pluginContext.meta.dependencies?.extensions.exposedComponents.push(exposedComponentId); - const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext); + const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext, log); expect(returnValue).toBe(false); - expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + expect(log.warning).toHaveBeenCalledTimes(0); }); it('should return TRUE and log a warning if the dependencies are missing', () => { + const log = createLogMock(); delete pluginContext.meta.dependencies; - const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext); + const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext, log); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch(`Using exposed component "${exposedComponentId}"`); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch(`Using exposed component "${exposedComponentId}"`); }); it('should return TRUE and log a warning if the exposed component id is not specified in the list of dependencies', () => { - const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext); + const log = createLogMock(); + const returnValue = isExposedComponentDependencyMissing(exposedComponentId, pluginContext, log); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch(`Using exposed component "${exposedComponentId}"`); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch(`Using exposed component "${exposedComponentId}"`); }); }); describe('isExtensionPointMetaInfoMissing()', () => { - let consoleWarnSpy: jest.SpyInstance; let pluginContext: PluginContextType; const pluginId = 'myorg-extensions-app'; const extensionPointId = `${pluginId}/extension-point/v1`; @@ -845,7 +880,6 @@ describe('Plugin Extensions / Utils', () => { }; beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); pluginContext = { meta: { id: pluginId, @@ -885,20 +919,22 @@ describe('Plugin Extensions / Utils', () => { }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { + const log = createLogMock(); pluginContext.meta.extensions?.extensionPoints.push(extensionPointConfig); - const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext); + const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, log); expect(returnValue).toBe(false); - expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + expect(log.warning).toHaveBeenCalledTimes(0); }); it('should return TRUE and log a warning if the extension point id is not recorded in the plugin.json', () => { - const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext); + const log = createLogMock(); + const returnValue = isExtensionPointMetaInfoMissing(extensionPointId, pluginContext, log); expect(returnValue).toBe(true); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy.mock.calls[0][0]).toMatch(`Extension point "${extensionPointId}"`); + expect(log.warning).toHaveBeenCalledTimes(1); + expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch(`Extension point "${extensionPointId}"`); }); }); }); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 0938c1df666..5f92e51b7f6 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -28,13 +28,10 @@ import { sidecarService } from 'app/core/services/SidecarService'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; import { ShowModalReactEvent } from 'app/types/events'; +import { ExtensionsLog, log } from './logs/log'; import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry'; import { assertIsNotPromise, assertLinkPathIsValid, assertStringProps, isPromise } from './validators'; -export function logWarning(message: string) { - console.warn(`[Plugin Extensions] ${message}`); -} - export function isPluginExtensionLinkConfig( extension: PluginExtensionConfig | undefined ): extension is PluginExtensionLinkConfig { @@ -59,7 +56,11 @@ export function createOpenModalFunction(pluginId: string): PluginExtensionEventH appEvents.publish( new ShowModalReactEvent({ - component: wrapWithPluginContext(pluginId, getModalWrapper({ title, body, width, height })), + component: wrapWithPluginContext( + pluginId, + getModalWrapper({ title, body, width, height }), + log + ), }) ); }; @@ -69,7 +70,7 @@ type ModalWrapperProps = { onDismiss: () => void; }; -export const wrapWithPluginContext = (pluginId: string, Component: React.ComponentType) => { +export const wrapWithPluginContext = (pluginId: string, Component: React.ComponentType, log: ExtensionsLog) => { const WrappedExtensionComponent = (props: T & React.JSX.IntrinsicAttributes) => { const { error, @@ -82,12 +83,15 @@ export const wrapWithPluginContext = (pluginId: string, Component: React.Com } if (error) { - logWarning(`Could not fetch plugin meta information for "${pluginId}", aborting. (${error.message})`); + log.error(`Could not fetch plugin meta information for "${pluginId}", aborting. (${error.message})`, { + stack: error.stack ?? '', + message: error.message, + }); return null; } if (!pluginMeta) { - logWarning(`Fetched plugin meta information is empty for "${pluginId}", aborting.`); + log.error(`Fetched plugin meta information is empty for "${pluginId}", aborting.`); return null; } @@ -298,7 +302,12 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel return subMenu; } -export function getLinkExtensionOverrides(pluginId: string, config: AddedLinkRegistryItem, context?: object) { +export function getLinkExtensionOverrides( + pluginId: string, + config: AddedLinkRegistryItem, + log: ExtensionsLog, + context?: object +) { try { const overrides = config.configure?.(context, { isAppOpened: () => isAppOpened(pluginId) }); @@ -325,7 +334,7 @@ export function getLinkExtensionOverrides(pluginId: string, config: AddedLinkReg assertStringProps({ title, description }, ['title', 'description']); if (Object.keys(rest).length > 0) { - logWarning( + log.warning( `Extension "${config.title}", is trying to override restricted properties: ${Object.keys(rest).join( ', ' )} which will be ignored.` @@ -341,7 +350,10 @@ export function getLinkExtensionOverrides(pluginId: string, config: AddedLinkReg }; } catch (error) { if (error instanceof Error) { - logWarning(error.message); + log.error(`Failed to configure link with title "${config.title}"`, { + stack: error.stack ?? '', + message: error.message, + }); } // If there is an error, we hide the extension @@ -354,6 +366,7 @@ export function getLinkExtensionOnClick( pluginId: string, extensionPointId: string, config: AddedLinkRegistryItem, + log: ExtensionsLog, context?: object ): ((event?: React.MouseEvent) => void) | undefined { const { onClick } = config; @@ -379,18 +392,25 @@ export function getLinkExtensionOnClick( closeAppInSideview: () => closeAppInSideview(pluginId), }; + log.debug(`onClick '${config.title}' at '${extensionPointId}'`); const result = onClick(event, helpers); if (isPromise(result)) { - result.catch((e) => { - if (e instanceof Error) { - logWarning(e.message); + result.catch((error) => { + if (error instanceof Error) { + log.error(error.message, { + message: error.message, + stack: error.stack ?? '', + }); } }); } } catch (error) { if (error instanceof Error) { - logWarning(error.message); + log.error(error.message, { + message: error.message, + stack: error.stack ?? '', + }); } } }; @@ -417,12 +437,16 @@ export const isAppOpened = (pluginId: string) => sidecarService.isAppOpened(plug export const isGrafanaDevMode = () => config.buildInfo.env === 'development'; // Checks if the meta information is missing from the plugin's plugin.json file -export const isExtensionPointMetaInfoMissing = (extensionPointId: string, pluginContext: PluginContextType) => { +export const isExtensionPointMetaInfoMissing = ( + extensionPointId: string, + pluginContext: PluginContextType, + log: ExtensionsLog +) => { const pluginId = pluginContext.meta?.id; const extensionPoints = pluginContext.meta?.extensions?.extensionPoints; if (!extensionPoints || !extensionPoints.some((ep) => ep.id === extensionPointId)) { - logWarning( + log.warning( `Extension point "${extensionPointId}" - it's not recorded in the "plugin.json" for "${pluginId}". Please add it under "extensions.extensionPoints[]".` ); return true; @@ -432,12 +456,16 @@ export const isExtensionPointMetaInfoMissing = (extensionPointId: string, plugin }; // Checks if an exposed component that the plugin is depending on is missing from the `dependencies` in the plugin.json file -export const isExposedComponentDependencyMissing = (id: string, pluginContext: PluginContextType) => { +export const isExposedComponentDependencyMissing = ( + id: string, + pluginContext: PluginContextType, + log: ExtensionsLog +) => { const pluginId = pluginContext.meta?.id; const exposedComponentsDependencies = pluginContext.meta?.dependencies?.extensions?.exposedComponents; if (!exposedComponentsDependencies || !exposedComponentsDependencies.includes(id)) { - logWarning( + log.warning( `Using exposed component "${id}" - it's not recorded in the "plugin.json" for "${pluginId}". Please add it under "dependencies.extensions.exposedComponents[]".` ); return true; @@ -446,31 +474,35 @@ export const isExposedComponentDependencyMissing = (id: string, pluginContext: P return false; }; -export const isAddedLinkMetaInfoMissing = (pluginId: string, metaInfo: PluginExtensionAddedLinkConfig) => { +export const isAddedLinkMetaInfoMissing = ( + pluginId: string, + metaInfo: PluginExtensionAddedLinkConfig, + log: ExtensionsLog +) => { const app = config.apps[pluginId]; const logPrefix = `Added-link "${metaInfo.title}" from "${pluginId}" -`; const pluginJsonMetaInfo = app ? app.extensions.addedLinks.find(({ title }) => title === metaInfo.title) : null; if (!app) { - logWarning(`${logPrefix} couldn't find app plugin "${pluginId}"`); + log.warning(`${logPrefix} couldn't find app plugin "${pluginId}"`); return true; } if (!pluginJsonMetaInfo) { - logWarning(`${logPrefix} not registered in the plugin.json under "extensions.addedLinks[]".`); + log.warning(`${logPrefix} not registered in the plugin.json under "extensions.addedLinks[]".`); return true; } const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { - logWarning(`${logPrefix} the "targets" don't match with ones in the plugin.json under "extensions.addedLinks[]".`); + log.warning(`${logPrefix} the "targets" don't match with ones in the plugin.json under "extensions.addedLinks[]".`); return true; } if (pluginJsonMetaInfo.description !== metaInfo.description) { - logWarning( + log.warning( `${logPrefix} the "description" doesn't match with one in the plugin.json under "extensions.addedLinks[]".` ); @@ -480,25 +512,29 @@ export const isAddedLinkMetaInfoMissing = (pluginId: string, metaInfo: PluginExt return false; }; -export const isAddedComponentMetaInfoMissing = (pluginId: string, metaInfo: PluginExtensionAddedComponentConfig) => { +export const isAddedComponentMetaInfoMissing = ( + pluginId: string, + metaInfo: PluginExtensionAddedComponentConfig, + log: ExtensionsLog +) => { const app = config.apps[pluginId]; const logPrefix = `Added component "${metaInfo.title}" -`; const pluginJsonMetaInfo = app ? app.extensions.addedComponents.find(({ title }) => title === metaInfo.title) : null; if (!app) { - logWarning(`${logPrefix} couldn't find app plugin "${pluginId}"`); + log.warning(`${logPrefix} couldn't find app plugin "${pluginId}"`); return true; } if (!pluginJsonMetaInfo) { - logWarning(`${logPrefix} not registered in the plugin.json under "extensions.addedComponents[]".`); + log.warning(`${logPrefix} not registered in the plugin.json under "extensions.addedComponents[]".`); return true; } const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { - logWarning( + log.warning( `${logPrefix} the "targets" don't match with ones in the plugin.json under "extensions.addedComponents[]".` ); @@ -506,7 +542,7 @@ export const isAddedComponentMetaInfoMissing = (pluginId: string, metaInfo: Plug } if (pluginJsonMetaInfo.description !== metaInfo.description) { - logWarning( + log.warning( `${logPrefix} the "description" doesn't match with one in the plugin.json under "extensions.addedComponents[]".` ); @@ -518,25 +554,26 @@ export const isAddedComponentMetaInfoMissing = (pluginId: string, metaInfo: Plug export const isExposedComponentMetaInfoMissing = ( pluginId: string, - metaInfo: PluginExtensionExposedComponentConfig + metaInfo: PluginExtensionExposedComponentConfig, + log: ExtensionsLog ) => { const app = config.apps[pluginId]; const logPrefix = `Exposed component "${metaInfo.id}" -`; const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.find(({ id }) => id === metaInfo.id) : null; if (!app) { - logWarning(`${logPrefix} couldn't find app plugin: "${pluginId}"`); + log.warning(`${logPrefix} couldn't find app plugin: "${pluginId}"`); return true; } if (!pluginJsonMetaInfo) { - logWarning(`${logPrefix} not registered in the plugin.json under "extensions.exposedComponents[]".`); + log.warning(`${logPrefix} not registered in the plugin.json under "extensions.exposedComponents[]".`); return true; } if (pluginJsonMetaInfo.title !== metaInfo.title) { - logWarning( + log.warning( `${logPrefix} the "title" doesn't match with one in the plugin.json under "extensions.exposedComponents[]".` ); @@ -544,7 +581,7 @@ export const isExposedComponentMetaInfoMissing = ( } if (pluginJsonMetaInfo.description !== metaInfo.description) { - logWarning( + log.warning( `${logPrefix} the "description" doesn't match with one in the plugin.json under "extensions.exposedComponents[]".` ); diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 3fa1f3bf98d..6d7ace4e8ca 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -23,6 +23,7 @@ import { SystemJSWithLoaderHooks } from './loader/types'; import { buildImportMap, resolveModulePath } from './loader/utils'; import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader'; import { isFrontendSandboxSupported } from './sandbox/utils'; +import { pluginsLogger } from './utils'; const imports = buildImportMap(sharedDependenciesMap); @@ -118,7 +119,19 @@ export async function importPluginModule({ return importPluginModuleInSandbox({ pluginId }); } - return SystemJS.import(modulePath); + return SystemJS.import(modulePath).catch((e) => { + let error = new Error('Could not load plugin: ' + e); + console.error(error); + pluginsLogger.logError(error, { + path, + pluginId, + pluginVersion: version ?? '', + expectedHash: moduleHash ?? '', + loadingStrategy: loadingStrategy.toString(), + sriChecksEnabled: (config.featureToggles.pluginsSriChecks ?? false).toString(), + }); + throw error; + }); } export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise { diff --git a/public/app/features/query-library/api/factory.ts b/public/app/features/query-library/api/factory.ts index 5ee9b021c59..d456fa4e453 100644 --- a/public/app/features/query-library/api/factory.ts +++ b/public/app/features/query-library/api/factory.ts @@ -7,7 +7,7 @@ import { baseQuery } from './query'; // Currently, we are loading all query templates // Organizations can have maximum of 1000 query templates -const GET_LIMIT = 1000; +export const QUERY_LIBRARY_GET_LIMIT = 1000; export const queryLibraryApi = createApi({ baseQuery, @@ -15,7 +15,7 @@ export const queryLibraryApi = createApi({ endpoints: (builder) => ({ allQueryTemplates: builder.query({ query: () => ({ - url: `?limit=${GET_LIMIT}`, + url: `?limit=${QUERY_LIBRARY_GET_LIMIT}`, }), transformResponse: convertDataQueryResponseToQueryTemplates, providesTags: ['QueryTemplatesList'], diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index 87bb9bf93f5..8853a9945a2 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -1,4 +1,4 @@ -import { cloneDeep, merge } from 'lodash'; +import { cloneDeep, merge, isEqual } from 'lodash'; import { Observable, of, ReplaySubject, Unsubscribable } from 'rxjs'; import { map, mergeMap, catchError } from 'rxjs/operators'; @@ -371,6 +371,7 @@ export class PanelQueryRunner { let sameSeries = compareArrayValues(last.series ?? [], next.series ?? [], (a, b) => a === b); let sameAnnotations = compareArrayValues(last.annotations ?? [], next.annotations ?? [], (a, b) => a === b); let sameState = last.state === next.state; + let sameErrors = compareArrayValues(last.errors ?? [], next.errors ?? [], (a, b) => isEqual(a, b)); if (sameSeries) { next.series = last.series; @@ -380,7 +381,7 @@ export class PanelQueryRunner { next.annotations = last.annotations; } - if (sameSeries && sameAnnotations && sameState) { + if (sameSeries && sameAnnotations && sameState && sameErrors) { return; } } diff --git a/public/app/features/query/state/runRequest.test.ts b/public/app/features/query/state/runRequest.test.ts index bf789c25f62..753a219e432 100644 --- a/public/app/features/query/state/runRequest.test.ts +++ b/public/app/features/query/state/runRequest.test.ts @@ -20,7 +20,7 @@ import { Echo } from '../../../core/services/echo/Echo'; import { createDashboardModelFixture } from '../../dashboard/state/__fixtures__/dashboardFixtures'; import { getMockDataSource, TestQuery } from './__mocks__/mockDataSource'; -import { callQueryMethod, runRequest } from './runRequest'; +import { callQueryMethodWithMigration, runRequest } from './runRequest'; jest.mock('app/core/services/backend_srv'); @@ -42,6 +42,14 @@ jest.mock('app/features/expressions/ExpressionDatasource', () => ({ }, })); +let isMigrationHandlerMock = jest.fn().mockReturnValue(false); +let migrateRequestMock = jest.fn(); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + isMigrationHandler: () => isMigrationHandlerMock(), + migrateRequest: () => migrateRequestMock(), +})); + class ScenarioCtx { ds!: DataSourceApi; request!: DataQueryRequest; @@ -405,7 +413,7 @@ describe('runRequest', () => { }); }); -describe('callQueryMethod', () => { +describe('callQueryMethodWithMigration', () => { let request: DataQueryRequest; let filterQuerySpy: jest.SpyInstance; let querySpy: jest.SpyInstance; @@ -417,11 +425,13 @@ describe('callQueryMethod', () => { filterQuery, getDefaultQuery, queryFunction, + migrateRequest, }: { targets: TestQuery[]; getDefaultQuery?: (app: CoreApp) => Partial; filterQuery?: typeof ds.filterQuery; queryFunction?: typeof ds.query; + migrateRequest?: jest.Mock; }) => { request = { range: { @@ -448,8 +458,12 @@ describe('callQueryMethod', () => { ds.getDefaultQuery = getDefaultQuery; defaultQuerySpy = jest.spyOn(ds, 'getDefaultQuery'); } + if (migrateRequest) { + isMigrationHandlerMock = jest.fn().mockReturnValue(true); + migrateRequestMock = migrateRequest; + } querySpy = jest.spyOn(ds, 'query'); - callQueryMethod(ds, request, queryFunction); + return callQueryMethodWithMigration(ds, request, queryFunction); }; beforeEach(() => { @@ -590,6 +604,48 @@ describe('callQueryMethod', () => { }) ); }); + + it('Should migrate a request if defined', (done) => { + const migrateRequest = jest.fn(); + const res = setup({ + targets: [ + { + refId: 'A', + q: 'SUM(foo)', + }, + ], + migrateRequest: migrateRequest.mockResolvedValue({ + range: { + from: dateTime(), + to: dateTime(), + raw: { from: '1h', to: 'now' }, + }, + targets: [ + { + refId: 'A', + qMigrated: 'SUM(foo)', + }, + ], + requestId: '', + interval: '', + intervalMs: 0, + scopedVars: {}, + timezone: '', + app: '', + startTime: 0, + }), + }); + expect(migrateRequest).toHaveBeenCalledTimes(1); + res.subscribe((res) => { + expect(res).toBeDefined(); + expect(querySpy).toHaveBeenCalledWith( + expect.objectContaining({ + targets: [{ qMigrated: 'SUM(foo)', refId: 'A' }], + }) + ); + done(); + }); + }); }); const expectThatRangeHasNotMutated = (ctx: ScenarioCtx) => { diff --git a/public/app/features/query/state/runRequest.ts b/public/app/features/query/state/runRequest.ts index 7a4e6e2fdf7..7c366852c3d 100644 --- a/public/app/features/query/state/runRequest.ts +++ b/public/app/features/query/state/runRequest.ts @@ -1,7 +1,7 @@ // Libraries import { isString, map as isArray } from 'lodash'; import { from, merge, Observable, of, timer } from 'rxjs'; -import { catchError, map, mapTo, share, takeUntil, tap } from 'rxjs/operators'; +import { catchError, map, mapTo, mergeMap, share, takeUntil, tap } from 'rxjs/operators'; // Utils & Services // Types @@ -18,7 +18,7 @@ import { PanelData, TimeRange, } from '@grafana/data'; -import { config, toDataQueryError } from '@grafana/runtime'; +import { config, isMigrationHandler, migrateRequest, toDataQueryError } from '@grafana/runtime'; import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { backendSrv } from 'app/core/services/backend_srv'; import { queryIsEmpty } from 'app/core/utils/query'; @@ -143,7 +143,7 @@ export function runRequest( return of(state.panelData); } - const dataObservable = callQueryMethod(datasource, request, queryFunction).pipe( + const dataObservable = callQueryMethodWithMigration(datasource, request, queryFunction).pipe( // Transform response packets into PanelData with merged results map((packet: DataQueryResponse) => { if (!isArray(packet.data)) { @@ -186,6 +186,20 @@ export function runRequest( return merge(timer(200).pipe(mapTo(state.panelData), takeUntil(dataObservable)), dataObservable); } +export function callQueryMethodWithMigration( + datasource: DataSourceApi, + request: DataQueryRequest, + queryFunction?: typeof datasource.query +) { + if (isMigrationHandler(datasource)) { + const migratedRequestPromise = migrateRequest(datasource, request); + return from(migratedRequestPromise).pipe( + mergeMap((migratedRequest) => callQueryMethod(datasource, migratedRequest, queryFunction)) + ); + } + return callQueryMethod(datasource, request, queryFunction); +} + export function callQueryMethod( datasource: DataSourceApi, request: DataQueryRequest, diff --git a/public/app/features/sandbox/TestStuffPage.tsx b/public/app/features/sandbox/TestStuffPage.tsx index 7faa71465b7..36368f5b508 100644 --- a/public/app/features/sandbox/TestStuffPage.tsx +++ b/public/app/features/sandbox/TestStuffPage.tsx @@ -19,7 +19,6 @@ export const TestStuffPage = () => { - diff --git a/public/app/features/scopes/internal/ScopesDashboardsScene.tsx b/public/app/features/scopes/internal/ScopesDashboardsScene.tsx index e838286df3f..b5450af8630 100644 --- a/public/app/features/scopes/internal/ScopesDashboardsScene.tsx +++ b/public/app/features/scopes/internal/ScopesDashboardsScene.tsx @@ -1,5 +1,6 @@ import { css, cx } from '@emotion/css'; import { isEqual } from 'lodash'; +import { finalize, from, Subscription } from 'rxjs'; import { GrafanaTheme2, ScopeDashboardBinding } from '@grafana/data'; import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } from '@grafana/scenes'; @@ -10,7 +11,6 @@ import { ScopesDashboardsTree } from './ScopesDashboardsTree'; import { ScopesDashboardsTreeSearch } from './ScopesDashboardsTreeSearch'; import { ScopesSelectorScene } from './ScopesSelectorScene'; import { fetchDashboards } from './api'; -import { DASHBOARDS_OPENED_KEY } from './const'; import { SuggestedDashboardsFoldersMap } from './types'; import { filterFolders, getScopeNamesFromSelectedScopes, groupDashboards } from './utils'; @@ -26,6 +26,7 @@ export interface ScopesDashboardsSceneState extends SceneObjectState { isLoading: boolean; isPanelOpened: boolean; isEnabled: boolean; + isReadOnly: boolean; scopesSelected: boolean; searchQuery: string; } @@ -36,8 +37,9 @@ export const getInitialDashboardsState: () => Omit Omit { static Component = ScopesDashboardsSceneRenderer; + private dashboardsFetchingSub: Subscription | undefined; + constructor() { super({ selector: null, @@ -52,35 +56,44 @@ export class ScopesDashboardsScene extends SceneObjectBase { - if (this.state.isEnabled && this.state.isPanelOpened) { - this.fetchDashboards(); - } - const resolvedSelector = this.state.selector?.resolve(); + if (resolvedSelector?.state.scopes.length ?? 0 > 0) { + this.fetchDashboards(); + this.openPanel(); + } + if (resolvedSelector) { this._subs.add( resolvedSelector.subscribeToState((newState, prevState) => { - if ( - this.state.isEnabled && - this.state.isPanelOpened && - !newState.isLoadingScopes && - (prevState.isLoadingScopes || newState.scopes !== prevState.scopes) - ) { + const newScopeNames = getScopeNamesFromSelectedScopes(newState.scopes ?? []); + const oldScopeNames = getScopeNamesFromSelectedScopes(prevState.scopes ?? []); + + if (!isEqual(newScopeNames, oldScopeNames)) { this.fetchDashboards(); + + if (newState.scopes.length > 0) { + this.openPanel(); + } else { + this.closePanel(); + } } }) ); } + + return () => { + this.dashboardsFetchingSub?.unsubscribe(); + }; }); } public async fetchDashboards() { const scopeNames = getScopeNamesFromSelectedScopes(this.state.selector?.resolve().state.scopes ?? []); - if (isEqual(scopeNames, this.state.forScopeNames)) { - return; - } + this.dashboardsFetchingSub?.unsubscribe(); + + this.setState({ forScopeNames: scopeNames }); if (scopeNames.length === 0) { return this.setState({ @@ -95,18 +108,26 @@ export class ScopesDashboardsScene extends SceneObjectBase { + this.setState({ isLoading: false }); + }) + ) + .subscribe((dashboards) => { + const folders = groupDashboards(dashboards); + const filteredFolders = filterFolders(folders, this.state.searchQuery); - this.setState({ - dashboards, - folders, - filteredFolders, - forScopeNames: scopeNames, - isLoading: false, - scopesSelected: scopeNames.length > 0, - }); + this.setState({ + dashboards, + folders, + filteredFolders, + isLoading: false, + scopesSelected: scopeNames.length > 0, + }); + + this.dashboardsFetchingSub?.unsubscribe(); + }); } public changeSearchQuery(searchQuery: string) { @@ -148,14 +169,19 @@ export class ScopesDashboardsScene extends SceneObjectBase) { - const { dashboards, filteredFolders, isLoading, isPanelOpened, isEnabled, searchQuery, scopesSelected } = + const { dashboards, filteredFolders, isLoading, isPanelOpened, isEnabled, isReadOnly, searchQuery, scopesSelected } = model.useState(); const styles = useStyles2(getStyles); - if (!isEnabled || !isPanelOpened) { + if (!isEnabled || !isPanelOpened || isReadOnly) { return null; } diff --git a/public/app/features/scopes/internal/const.ts b/public/app/features/scopes/internal/const.ts deleted file mode 100644 index 643a106960d..00000000000 --- a/public/app/features/scopes/internal/const.ts +++ /dev/null @@ -1 +0,0 @@ -export const DASHBOARDS_OPENED_KEY = 'grafana.scopes.dashboards.opened'; diff --git a/public/app/features/scopes/tests/dashboardsList.test.ts b/public/app/features/scopes/tests/dashboardsList.test.ts index e5215a89f86..3b5f2fe1885 100644 --- a/public/app/features/scopes/tests/dashboardsList.test.ts +++ b/public/app/features/scopes/tests/dashboardsList.test.ts @@ -12,7 +12,9 @@ import { expectDashboardInDocument, expectDashboardLength, expectDashboardNotInDocument, + expectDashboardsClosed, expectDashboardSearchValue, + expectDashboardsOpen, expectDashboardsSearch, expectNoDashboardsForFilter, expectNoDashboardsForScope, @@ -45,9 +47,20 @@ describe('Dashboards list', () => { await resetScenes(); }); - it('Does not fetch dashboards list when the list is not expanded', async () => { + it('Opens container and fetches dashboards list when a scope is selected', async () => { + expectDashboardsClosed(); await updateScopes(['mimir']); - expect(fetchDashboardsSpy).not.toHaveBeenCalled(); + expectDashboardsOpen(); + expect(fetchDashboardsSpy).toHaveBeenCalled(); + }); + + it('Closes container when no scopes are selected', async () => { + await updateScopes(['mimir']); + expectDashboardsOpen(); + await updateScopes(['mimir', 'loki']); + expectDashboardsOpen(); + await updateScopes([]); + expectDashboardsClosed(); }); it('Fetches dashboards list when the list is expanded', async () => { diff --git a/public/app/features/scopes/tests/utils/assertions.ts b/public/app/features/scopes/tests/utils/assertions.ts index 785f72e005f..cfd54c4ad78 100644 --- a/public/app/features/scopes/tests/utils/assertions.ts +++ b/public/app/features/scopes/tests/utils/assertions.ts @@ -1,6 +1,7 @@ import { getMock, locationReloadSpy } from './mocks'; import { getDashboard, + getDashboardsContainer, getDashboardsExpand, getDashboardsSearch, getNotFoundForFilter, @@ -62,6 +63,7 @@ export const expectResultCloudOpsNotSelected = () => expectRadioNotChecked(getRe export const expectDashboardsDisabled = () => expectDisabled(getDashboardsExpand); export const expectDashboardsClosed = () => expectNotInDocument(queryDashboardsContainer); +export const expectDashboardsOpen = () => expectInDocument(getDashboardsContainer); export const expectNoDashboardsSearch = () => expectNotInDocument(queryDashboardsSearch); export const expectDashboardsSearch = () => expectInDocument(getDashboardsSearch); export const expectNoDashboardsNoScopes = () => expectInDocument(getNotFoundNoScopes); diff --git a/public/app/features/scopes/tests/utils/render.tsx b/public/app/features/scopes/tests/utils/render.tsx index 538ed293770..71d60ff6492 100644 --- a/public/app/features/scopes/tests/utils/render.tsx +++ b/public/app/features/scopes/tests/utils/render.tsx @@ -12,7 +12,6 @@ import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types'; import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from '../../instance'; import { getInitialDashboardsState } from '../../internal/ScopesDashboardsScene'; import { initialSelectorState } from '../../internal/ScopesSelectorScene'; -import { DASHBOARDS_OPENED_KEY } from '../../internal/const'; import { clearMocks } from './actions'; @@ -160,7 +159,6 @@ export async function resetScenes() { await jest.runOnlyPendingTimersAsync(); jest.useRealTimers(); scopesSelectorScene?.setState(initialSelectorState); - localStorage.removeItem(DASHBOARDS_OPENED_KEY); scopesDashboardsScene?.setState(getInitialDashboardsState()); cleanup(); } diff --git a/public/app/features/scopes/tests/utils/selectors.ts b/public/app/features/scopes/tests/utils/selectors.ts index 44f24b5fbac..b3174c0c6e3 100644 --- a/public/app/features/scopes/tests/utils/selectors.ts +++ b/public/app/features/scopes/tests/utils/selectors.ts @@ -38,6 +38,7 @@ export const getSelectorApply = () => screen.getByTestId(selectors.selector.appl export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel); export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand); +export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container); export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container); export const queryDashboardsSearch = () => screen.queryByTestId(selectors.dashboards.search); export const getDashboardsSearch = () => screen.getByTestId(selectors.dashboards.search); diff --git a/public/app/features/scopes/utils.ts b/public/app/features/scopes/utils.ts index 85d01626fa8..29ddd616382 100644 --- a/public/app/features/scopes/utils.ts +++ b/public/app/features/scopes/utils.ts @@ -25,12 +25,12 @@ export function disableScopes() { export function exitScopesReadOnly() { scopesSelectorScene?.exitReadOnly(); - scopesDashboardsScene?.enable(); + scopesDashboardsScene?.exitReadOnly(); } export function enterScopesReadOnly() { scopesSelectorScene?.enterReadOnly(); - scopesDashboardsScene?.disable(); + scopesDashboardsScene?.enterReadOnly(); } export function getClosestScopesFacade(scene: SceneObject): ScopesFacade | null { diff --git a/public/app/features/search/service/searcher.ts b/public/app/features/search/service/searcher.ts index b7e8b885424..354411534c9 100644 --- a/public/app/features/search/service/searcher.ts +++ b/public/app/features/search/service/searcher.ts @@ -4,7 +4,6 @@ import { BlugeSearcher } from './bluge'; import { FrontendSearcher } from './frontend'; import { SQLSearcher } from './sql'; import { GrafanaSearcher } from './types'; -import { UnifiedSearcher } from './unified'; let searcher: GrafanaSearcher | undefined = undefined; @@ -14,11 +13,6 @@ export function getGrafanaSearcher(): GrafanaSearcher { const useBluge = config.featureToggles.panelTitleSearch; searcher = useBluge ? new BlugeSearcher(sqlSearcher) : sqlSearcher; - const useUnified = config.featureToggles.unifiedStorageSearch; - if (useUnified) { - searcher = new UnifiedSearcher(sqlSearcher); - } - if (useBluge && location.search.includes('do-frontend-query')) { searcher = new FrontendSearcher(searcher); } diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts deleted file mode 100644 index 882b3a0a5bc..00000000000 --- a/public/app/features/search/service/unified.ts +++ /dev/null @@ -1,265 +0,0 @@ -// TODO: fix - copied from bluge.ts -import { - DataFrame, - DataFrameJSON, - DataFrameView, - getDisplayProcessor, - SelectableValue, - toDataFrame, -} from '@grafana/data'; -import { config, getBackendSrv } from '@grafana/runtime'; -import { TermCount } from 'app/core/components/TagFilter/TagFilter'; - -import { replaceCurrentFolderQuery } from './utils'; - -import { DashboardQueryResult, GrafanaSearcher, QueryResponse, SearchQuery } from '.'; - -// The backend returns an empty frame with a special name to indicate that the indexing engine is being rebuilt, -// and that it can not serve any search requests. We are temporarily using the old SQL Search API as a fallback when that happens. -const loadingFrameName = 'Loading'; - -const searchURI = 'api/unified-search'; - -type SearchAPIResponse = { - frames: DataFrameJSON[]; -}; - -const folderViewSort = 'name_sort'; - -export class UnifiedSearcher implements GrafanaSearcher { - constructor(private fallbackSearcher: GrafanaSearcher) {} - - async search(query: SearchQuery): Promise { - if (query.facet?.length) { - throw new Error('facets not supported!'); - } - return this.doSearchQuery(query); - } - - // TODO: fix - copied from bluge.ts - async starred(query: SearchQuery): Promise { - if (query.facet?.length) { - throw new Error('facets not supported!'); - } - // get the starred dashboards - const starsUIDS = await getBackendSrv().get('api/user/stars'); - if (starsUIDS?.length) { - return this.doSearchQuery({ - uid: starsUIDS, - query: query.query ?? '*', - }); - } - // Nothing is starred - return { - view: new DataFrameView({ length: 0, fields: [] }), - totalRows: 0, - loadMoreItems: async (startIndex: number, stopIndex: number): Promise => { - return; - }, - isItemLoaded: (index: number): boolean => { - return true; - }, - }; - } - - // TODO: fix - copied from bluge.ts - async tags(query: SearchQuery): Promise { - const req = { - ...query, - query: query.query ?? '*', - sort: undefined, // no need to sort the initial query results (not used) - facet: [{ field: 'tag' }], - limit: 1, // 0 would be better, but is ignored by the backend - }; - - const resp = await getBackendSrv().post(searchURI, req); - const frames = resp.frames.map((f) => toDataFrame(f)); - - if (frames[0]?.name === loadingFrameName) { - return this.fallbackSearcher.tags(query); - } - - for (const frame of frames) { - if (frame.fields[0].name === 'tag') { - return getTermCountsFrom(frame); - } - } - - return []; - } - - // TODO: fix - copied from bluge.ts - getSortOptions(): Promise { - const opts: SelectableValue[] = [ - { value: folderViewSort, label: 'Alphabetically (A-Z)' }, - { value: '-name_sort', label: 'Alphabetically (Z-A)' }, - ]; - - if (config.licenseInfo.enabledFeatures.analytics) { - for (const sf of sortFields) { - opts.push({ value: `-${sf.name}`, label: `${sf.display} (most)` }); - opts.push({ value: `${sf.name}`, label: `${sf.display} (least)` }); - } - for (const sf of sortTimeFields) { - opts.push({ value: `-${sf.name}`, label: `${sf.display} (recent)` }); - opts.push({ value: `${sf.name}`, label: `${sf.display} (oldest)` }); - } - } - - return Promise.resolve(opts); - } - - // TODO: update - copied from bluge.ts - async doSearchQuery(query: SearchQuery): Promise { - query = await replaceCurrentFolderQuery(query); - const req = { - ...query, - query: query.query ?? '*', - limit: query.limit ?? firstPageSize, - }; - - const rsp = await getBackendSrv().post(searchURI, req); - const frames = rsp.frames.map((f) => toDataFrame(f)); - - const first = frames.length ? toDataFrame(frames[0]) : { fields: [], length: 0 }; - - if (first.name === loadingFrameName) { - return this.fallbackSearcher.search(query); - } - - for (const field of first.fields) { - field.display = getDisplayProcessor({ field, theme: config.theme2 }); - } - - // Make sure the object exists - if (!first.meta?.custom) { - first.meta = { - ...first.meta, - custom: { - count: first.length, - max_score: 1, - }, - }; - } - - const meta = first.meta.custom || {}; - if (!meta.locationInfo) { - meta.locationInfo = {}; // always set it so we can append - } - - // Set the field name to a better display name - if (meta.sortBy?.length) { - const field = first.fields.find((f) => f.name === meta.sortBy); - if (field) { - const name = getSortFieldDisplayName(field.name); - meta.sortBy = name; - field.name = name; // make it look nicer - } - } - - let loadMax = 0; - let pending: Promise | undefined = undefined; - const getNextPage = async () => { - while (loadMax > view.dataFrame.length) { - const from = view.dataFrame.length; - if (from >= meta.count) { - return; - } - const resp = await getBackendSrv().post(searchURI, { - ...(req ?? {}), - from, - limit: nextPageSizes, - }); - const frame = toDataFrame(resp.frames[0]); - - if (!frame) { - console.log('no results', frame); - return; - } - if (frame.fields.length !== view.dataFrame.fields.length) { - console.log('invalid shape', frame, view.dataFrame); - return; - } - - // Append the raw values to the same array buffer - const length = frame.length + view.dataFrame.length; - for (let i = 0; i < frame.fields.length; i++) { - const values = view.dataFrame.fields[i].values; - values.push(...frame.fields[i].values); - } - view.dataFrame.length = length; - - // Add all the location lookup info - const submeta = frame.meta?.custom; - if (submeta?.locationInfo && meta) { - for (const [key, value] of Object.entries(submeta.locationInfo)) { - meta.locationInfo[key] = value; - } - } - } - pending = undefined; - }; - - const view = new DataFrameView(first); - return { - totalRows: meta.count ?? first.length, - view, - loadMoreItems: async (startIndex: number, stopIndex: number): Promise => { - loadMax = Math.max(loadMax, stopIndex); - if (!pending) { - pending = getNextPage(); - } - return pending; - }, - isItemLoaded: (index: number): boolean => { - return index < view.dataFrame.length; - }, - }; - } - - getFolderViewSort(): string { - return 'name_sort'; - } -} - -const firstPageSize = 50; -const nextPageSizes = 100; - -function getTermCountsFrom(frame: DataFrame): TermCount[] { - const keys = frame.fields[0].values; - const vals = frame.fields[1].values; - const counts: TermCount[] = []; - for (let i = 0; i < frame.length; i++) { - counts.push({ term: keys[i], count: vals[i] }); - } - return counts; -} - -// Enterprise only sort field values for dashboards -const sortFields = [ - { name: 'views_total', display: 'Views total' }, - { name: 'views_last_30_days', display: 'Views 30 days' }, - { name: 'errors_total', display: 'Errors total' }, - { name: 'errors_last_30_days', display: 'Errors 30 days' }, -]; - -// Enterprise only time sort field values for dashboards -const sortTimeFields = [ - { name: 'created_at', display: 'Created time' }, - { name: 'updated_at', display: 'Updated time' }, -]; - -/** Given the internal field name, this gives a reasonable display name for the table colum header */ -function getSortFieldDisplayName(name: string) { - for (const sf of sortFields) { - if (sf.name === name) { - return sf.display; - } - } - for (const sf of sortTimeFields) { - if (sf.name === name) { - return sf.display; - } - } - return name; -} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts index 9f40180d68d..36578e56ffb 100644 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts +++ b/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts @@ -1,10 +1,14 @@ import { PanelBuilders } from '@grafana/scenes'; +import { SortOrder } from '@grafana/schema'; +import { TooltipDisplayMode } from '@grafana/ui'; import { CommonVizParams } from './types'; export function percentilesGraphBuilder({ title, unit }: CommonVizParams) { - return PanelBuilders.timeseries() // + return PanelBuilders.timeseries() .setTitle(title) .setUnit(unit) - .setCustomFieldConfig('fillOpacity', 9); + .setCustomFieldConfig('fillOpacity', 9) + .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) + .setOption('legend', { showLegend: false }); } diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts index 5eab7d70bfd..d04766f23e4 100644 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts +++ b/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts @@ -1,4 +1,6 @@ import { PanelBuilders } from '@grafana/scenes'; +import { SortOrder } from '@grafana/schema'; +import { TooltipDisplayMode } from '@grafana/ui'; import { CommonVizParams } from './types'; @@ -7,5 +9,6 @@ export function simpleGraphBuilder({ title, unit }: CommonVizParams) { .setTitle(title) .setUnit(unit) .setOption('legend', { showLegend: false }) + .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) .setCustomFieldConfig('fillOpacity', 9); } diff --git a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx index 26b04788dda..b908c25f801 100644 --- a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx +++ b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx @@ -21,7 +21,7 @@ import { VariableDependencyConfig, VizPanel, } from '@grafana/scenes'; -import { DataQuery } from '@grafana/schema'; +import { DataQuery, SortOrder, TooltipDisplayMode } from '@grafana/schema'; import { Button, Field, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; @@ -39,7 +39,6 @@ import { AddToFiltersGraphAction } from './AddToFiltersGraphAction'; import { BreakdownSearchReset, BreakdownSearchScene } from './BreakdownSearchScene'; import { ByFrameRepeater } from './ByFrameRepeater'; import { LayoutSwitcher } from './LayoutSwitcher'; -import { breakdownPanelOptions } from './panelConfigs'; import { BreakdownLayoutChangeCallback, BreakdownLayoutType } from './types'; import { getLabelOptions } from './utils'; import { BreakdownAxisChangeEvent, yAxisSyncBehavior } from './yAxisSyncBehavior'; @@ -300,6 +299,8 @@ export function buildAllLayout( const unit = queryDef.unit; const vizPanel = PanelBuilders.timeseries() + .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) + .setOption('legend', { showLegend: false }) .setTitle(option.label!) .setData( new SceneQueryRunner({ @@ -319,10 +320,6 @@ export function buildAllLayout( .setBehaviors([fixLegendForUnspecifiedLabelValueBehavior]) .build(); - vizPanel.addActivationHandler(() => { - vizPanel.onOptionsChange(breakdownPanelOptions); - }); - children.push( new SceneCSSGridItem({ $behaviors: [yAxisSyncBehavior], @@ -382,10 +379,6 @@ function buildNormalLayout( isHidden, }); - vizPanel.addActivationHandler(() => { - vizPanel.onOptionsChange(breakdownPanelOptions); - }); - return item; } @@ -409,7 +402,11 @@ function buildNormalLayout( children: [ new SceneFlexItem({ minHeight: 300, - body: PanelBuilders.timeseries().setTitle('$metric').build(), + body: PanelBuilders.timeseries() + .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) + .setOption('legend', { showLegend: false }) + .setTitle('$metric') + .build(), }), ], }), diff --git a/public/app/features/trails/Breakdown/panelConfigs.ts b/public/app/features/trails/Breakdown/panelConfigs.ts deleted file mode 100644 index 23d796fdf8e..00000000000 --- a/public/app/features/trails/Breakdown/panelConfigs.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PanelOptionsBuilders } from '@grafana/scenes'; -import { SortOrder } from '@grafana/schema/dist/esm/index'; -import { TooltipDisplayMode } from '@grafana/ui'; - -export const breakdownPanelOptions = PanelOptionsBuilders.timeseries() - .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) - .setOption('legend', { showLegend: false }) - .build(); diff --git a/public/app/features/trails/DataTrailsApp.tsx b/public/app/features/trails/DataTrailsApp.tsx index a3b7c8ec46a..a3057fa4b66 100644 --- a/public/app/features/trails/DataTrailsApp.tsx +++ b/public/app/features/trails/DataTrailsApp.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom-v5-compat'; import { PageLayoutType } from '@grafana/data'; import { locationService } from '@grafana/runtime'; @@ -32,11 +32,11 @@ export class DataTrailsApp extends SceneObjectBase { const { trail, home } = model.useState(); return ( - + + {/* The routes are relative to the HOME_ROUTE */} ( + path={'/'} + element={ { > - )} + } /> - } /> - + } /> + ); }; } diff --git a/public/app/features/trails/Integrations/dashboardIntegration.ts b/public/app/features/trails/Integrations/dashboardIntegration.ts index ea2a34ea91f..4ff039f377b 100644 --- a/public/app/features/trails/Integrations/dashboardIntegration.ts +++ b/public/app/features/trails/Integrations/dashboardIntegration.ts @@ -1,4 +1,4 @@ -import { PanelMenuItem } from '@grafana/data'; +import { DataSourceApi, PanelMenuItem } from '@grafana/data'; import { PromQuery } from '@grafana/prometheus'; import { getDataSourceSrv } from '@grafana/runtime'; import { SceneTimeRangeState, VizPanel } from '@grafana/scenes'; @@ -34,7 +34,13 @@ export async function addDataTrailPanelAction(dashboard: DashboardScene, panel: return; } - const dataSourceApi = await getDataSourceSrv().get(datasource); + let dataSourceApi: DataSourceApi | undefined; + + try { + dataSourceApi = await getDataSourceSrv().get(datasource); + } catch (e) { + return; + } if (dataSourceApi.interpolateVariablesInQueries == null) { return; diff --git a/public/app/features/trails/MetricGraphScene.tsx b/public/app/features/trails/MetricGraphScene.tsx index 5e439480f71..69003e9edc5 100644 --- a/public/app/features/trails/MetricGraphScene.tsx +++ b/public/app/features/trails/MetricGraphScene.tsx @@ -62,6 +62,8 @@ function getStyles(theme: GrafanaTheme2, chromeHeaderHeight: number) { flexDirection: 'row', background: theme.isLight ? theme.colors.background.primary : theme.colors.background.canvas, position: 'sticky', + paddingTop: theme.spacing(1), + marginTop: `-${theme.spacing(1)}`, top: `${chromeHeaderHeight + 70}px`, zIndex: 10, }), diff --git a/public/app/features/variables/editor/VariableEditorContainer.tsx b/public/app/features/variables/editor/VariableEditorContainer.tsx index d54786ed62a..f810d4bfc88 100644 --- a/public/app/features/variables/editor/VariableEditorContainer.tsx +++ b/public/app/features/variables/editor/VariableEditorContainer.tsx @@ -105,14 +105,14 @@ class VariableEditorContainerUnconnected extends PureComponent { }; render() { - const { editIndex, variables, sectionNav, toolbar } = this.props; + const { editIndex, variables, sectionNav } = this.props; const variableToEdit = editIndex != null ? variables[editIndex] : undefined; const node = sectionNav.node; const parentItem = node.parentItem; const subPageNav = variableToEdit ? { text: variableToEdit.name, parentItem } : parentItem; return ( - + {!variableToEdit && ( { }; return ( -
+
    {value.map((tz, idx) => ( -
    - - setTimezone(idx, v)} - includeInternal={true} - value={tz ?? InternalTimeZones.default} - /> - +
  • + setTimezone(idx, v)} + includeInternal={true} + value={tz ?? InternalTimeZones.default} + /> {idx === value.length - 1 ? ( ) : ( removeTimezone(idx)} tooltip="Remove timezone" /> )} -
  • + ))} -
+ ); }; const getStyles = (theme: GrafanaTheme2) => ({ - wrapper: css` - width: 100%; - display: flex; - flex-direction: rows; - align-items: center; - `, - first: css` - margin-right: 8px; - flex-grow: 2; - `, + list: css({ + listStyle: 'none', + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(0.5), + }), + listItem: css({ + display: 'flex', + gap: theme.spacing(1), + }), }); diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 405c76ee35f..3355616189d 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -23,6 +23,7 @@ import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamic import { RouteDescriptor } from '../core/navigation/types'; import { getPublicDashboardRoutes } from '../features/dashboard/routes'; +const isDevEnv = config.buildInfo.env === 'development'; export const extraRoutes: RouteDescriptor[] = []; export function getAppRoutes(): RouteDescriptor[] { @@ -198,6 +199,15 @@ export function getAppRoutes(): RouteDescriptor[] { path: '/admin/plugins', component: () => , }, + { + path: '/admin/extensions', + navId: 'extensions', + component: isDevEnv + ? SafeDynamicImport( + () => import(/* webpackChunkName: "PluginExtensionsLog" */ 'app/features/plugins/extensions/logs/LogViewer') + ) + : () => , + }, { path: '/admin/access', component: () => , @@ -434,9 +444,9 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "SnapshotListPage" */ 'app/features/manage-dashboards/SnapshotListPage') ), }, - config.featureToggles.dashboardRestoreUI && { + config.featureToggles.dashboardRestore && { path: '/dashboard/recently-deleted', - roles: () => ['Admin'], + roles: () => ['Admin', 'ServerAdmin'], component: SafeDynamicImport( () => import(/* webpackChunkName: "RecentlyDeletedPage" */ 'app/features/browse-dashboards/RecentlyDeletedPage') ), diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 731ad1e9d49..c6713fa0865 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -139,3 +139,5 @@ export interface DashboardState { initialDatasource?: DataSourceRef['uid']; initError: DashboardInitError | null; } + +export const DASHBOARD_FROM_LS_KEY = 'DASHBOARD_FROM_LS_KEY'; diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index 0c379f929b1..bf149896c12 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -201,6 +201,8 @@ export interface AlertDataQuery extends DataQuery { maxDataPoints?: number; intervalMs?: number; expression?: string; + instant?: boolean; + range?: boolean; } export interface AlertQuery { diff --git a/public/img/icons/unicons/api-endpoint.svg b/public/img/icons/unicons/api-endpoint.svg new file mode 100644 index 00000000000..1d300cffc63 --- /dev/null +++ b/public/img/icons/unicons/api-endpoint.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/img/icons/unicons/browser-alt.svg b/public/img/icons/unicons/browser-alt.svg new file mode 100644 index 00000000000..253275890d3 --- /dev/null +++ b/public/img/icons/unicons/browser-alt.svg @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/public/img/icons/unicons/k6-rounded.svg b/public/img/icons/unicons/k6-rounded.svg new file mode 100644 index 00000000000..101c269716e --- /dev/null +++ b/public/img/icons/unicons/k6-rounded.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/public/img/icons/unicons/multi-step.svg b/public/img/icons/unicons/multi-step.svg new file mode 100644 index 00000000000..eea231cd406 --- /dev/null +++ b/public/img/icons/unicons/multi-step.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index ad79affc6b5..ac7243cb160 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1096,7 +1096,6 @@ "help-wizard": { "download-snapshot": "", "github-comment": "", - "preview-snapshot": "", "support-bundle": "", "troubleshooting-help": "" }, @@ -1225,6 +1224,7 @@ "documentation": "", "host": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1234,11 +1234,13 @@ }, "search_filter": { "description": "", + "error": "", "label": "", "placeholder": "" }, "search-base-dns": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1440,6 +1442,7 @@ "snapshot-error-status-body": "", "snapshot-error-status-title": "", "success-message": "", + "success-message-generic": "", "success-title": "", "upload-snapshot-error-title": "" }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 0823f93c9b3..376282bc21d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -831,8 +831,8 @@ "private": "Private", "public": "Public", "query-deleted": "Query deleted", - "query-template-add-error": "Error attempting to add this query to the library", - "query-template-added": "Query template successfully added to the library", + "query-template-add-error": "Error attempting to save this query to the library", + "query-template-added": "Query successfully saved to the library", "query-template-edit-error": "Error attempting to edit this query", "query-template-edited": "Query template successfully edited", "save": "Save" @@ -1096,7 +1096,6 @@ "help-wizard": { "download-snapshot": "Download snapshot", "github-comment": "Copy Github comment", - "preview-snapshot": "Preview snapshot", "support-bundle": "You can also retrieve a support bundle containing information concerning your Grafana instance and configured datasources in the <1>support bundles section.", "troubleshooting-help": "To request troubleshooting help, send a snapshot of this panel to Grafana Labs Technical Support. The snapshot contains query response data and panel settings." }, @@ -1225,7 +1224,8 @@ "documentation": "documentation", "host": { "description": "Hostname or IP address of the LDAP server you wish to connect to.", - "label": "Server host *", + "error": "Server host is a required field", + "label": "Server host", "placeholder": "example: 127.0.0.1" }, "login-form-alert": { @@ -1234,12 +1234,14 @@ }, "search_filter": { "description": "LDAP search filter used to locate specific entries within the directory.", - "label": "Search filter *", + "error": "Search filter is a required field", + "label": "Search filter", "placeholder": "example: cn=%s" }, "search-base-dns": { "description": "An array of base dns to search through.", - "label": "Search base DNS *", + "error": "Search base DNS is a required field", + "label": "Search base DNS", "placeholder": "example: dc=grafana,dc=org" }, "subtitle": "The LDAP integration in Grafana allows your Grafana users to log in with their LDAP credentials. Find out more in our <2><0>documentation.", @@ -1440,6 +1442,7 @@ "snapshot-error-status-body": "There was an error creating the snapshot or starting the migration process. See the Grafana server logs for more details", "snapshot-error-status-title": "Error migrating resources", "success-message": "Successfully migrated {{successCount}} {{types, list}} to your Grafana Cloud instance.", + "success-message-generic": "Successfully migrated {{successCount}} resources to your Grafana Cloud instance.", "success-title": "Migration completed!", "upload-snapshot-error-title": "Error uploading snapshot" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 9ab396e02e5..fde74558ed1 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1096,7 +1096,6 @@ "help-wizard": { "download-snapshot": "", "github-comment": "", - "preview-snapshot": "", "support-bundle": "", "troubleshooting-help": "" }, @@ -1225,6 +1224,7 @@ "documentation": "", "host": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1234,11 +1234,13 @@ }, "search_filter": { "description": "", + "error": "", "label": "", "placeholder": "" }, "search-base-dns": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1440,6 +1442,7 @@ "snapshot-error-status-body": "", "snapshot-error-status-title": "", "success-message": "", + "success-message-generic": "", "success-title": "", "upload-snapshot-error-title": "" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index da3b6c03325..744b041a763 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1096,7 +1096,6 @@ "help-wizard": { "download-snapshot": "", "github-comment": "", - "preview-snapshot": "", "support-bundle": "", "troubleshooting-help": "" }, @@ -1225,6 +1224,7 @@ "documentation": "", "host": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1234,11 +1234,13 @@ }, "search_filter": { "description": "", + "error": "", "label": "", "placeholder": "" }, "search-base-dns": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1440,6 +1442,7 @@ "snapshot-error-status-body": "", "snapshot-error-status-title": "", "success-message": "", + "success-message-generic": "", "success-title": "", "upload-snapshot-error-title": "" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index a2a87a13fd4..5a68f130569 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -831,8 +831,8 @@ "private": "Přįväŧę", "public": "Pūþľįč", "query-deleted": "Qūęřy đęľęŧęđ", - "query-template-add-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő äđđ ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy", - "query-template-added": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy äđđęđ ŧő ŧĥę ľįþřäřy", + "query-template-add-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő şävę ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy", + "query-template-added": "Qūęřy şūččęşşƒūľľy şävęđ ŧő ŧĥę ľįþřäřy", "query-template-edit-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ęđįŧ ŧĥįş qūęřy", "query-template-edited": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy ęđįŧęđ", "save": "Ŝävę" @@ -1096,7 +1096,6 @@ "help-wizard": { "download-snapshot": "Đőŵʼnľőäđ şʼnäpşĥőŧ", "github-comment": "Cőpy Ğįŧĥūþ čőmmęʼnŧ", - "preview-snapshot": "Přęvįęŵ şʼnäpşĥőŧ", "support-bundle": "Ÿőū čäʼn äľşő řęŧřįęvę ä şūppőřŧ þūʼnđľę čőʼnŧäįʼnįʼnģ įʼnƒőřmäŧįőʼn čőʼnčęřʼnįʼnģ yőūř Ğřäƒäʼnä įʼnşŧäʼnčę äʼnđ čőʼnƒįģūřęđ đäŧäşőūřčęş įʼn ŧĥę <1>şūppőřŧ þūʼnđľęş şęčŧįőʼn.", "troubleshooting-help": "Ŧő řęqūęşŧ ŧřőūþľęşĥőőŧįʼnģ ĥęľp, şęʼnđ ä şʼnäpşĥőŧ őƒ ŧĥįş päʼnęľ ŧő Ğřäƒäʼnä Ŀäþş Ŧęčĥʼnįčäľ Ŝūppőřŧ. Ŧĥę şʼnäpşĥőŧ čőʼnŧäįʼnş qūęřy řęşpőʼnşę đäŧä äʼnđ päʼnęľ şęŧŧįʼnģş." }, @@ -1225,7 +1224,8 @@ "documentation": "đőčūmęʼnŧäŧįőʼn", "host": { "description": "Ħőşŧʼnämę őř ĨP äđđřęşş őƒ ŧĥę ĿĐÅP şęřvęř yőū ŵįşĥ ŧő čőʼnʼnęčŧ ŧő.", - "label": "Ŝęřvęř ĥőşŧ *", + "error": "Ŝęřvęř ĥőşŧ įş ä řęqūįřęđ ƒįęľđ", + "label": "Ŝęřvęř ĥőşŧ", "placeholder": "ęχämpľę: 127.0.0.1" }, "login-form-alert": { @@ -1234,12 +1234,14 @@ }, "search_filter": { "description": "ĿĐÅP şęäřčĥ ƒįľŧęř ūşęđ ŧő ľőčäŧę şpęčįƒįč ęʼnŧřįęş ŵįŧĥįʼn ŧĥę đįřęčŧőřy.", - "label": "Ŝęäřčĥ ƒįľŧęř *", + "error": "Ŝęäřčĥ ƒįľŧęř įş ä řęqūįřęđ ƒįęľđ", + "label": "Ŝęäřčĥ ƒįľŧęř", "placeholder": "ęχämpľę: čʼn=%ş" }, "search-base-dns": { "description": "Åʼn äřřäy őƒ þäşę đʼnş ŧő şęäřčĥ ŧĥřőūģĥ.", - "label": "Ŝęäřčĥ þäşę ĐŃŜ *", + "error": "Ŝęäřčĥ þäşę ĐŃŜ įş ä řęqūįřęđ ƒįęľđ", + "label": "Ŝęäřčĥ þäşę ĐŃŜ", "placeholder": "ęχämpľę: đč=ģřäƒäʼnä,đč=őřģ" }, "subtitle": "Ŧĥę ĿĐÅP įʼnŧęģřäŧįőʼn įʼn Ğřäƒäʼnä äľľőŵş yőūř Ğřäƒäʼnä ūşęřş ŧő ľőģ įʼn ŵįŧĥ ŧĥęįř ĿĐÅP čřęđęʼnŧįäľş. Fįʼnđ őūŧ mőřę įʼn őūř <2><0>đőčūmęʼnŧäŧįőʼn.", @@ -1440,6 +1442,7 @@ "snapshot-error-status-body": "Ŧĥęřę ŵäş äʼn ęřřőř čřęäŧįʼnģ ŧĥę şʼnäpşĥőŧ őř şŧäřŧįʼnģ ŧĥę mįģřäŧįőʼn přőčęşş. Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş", "snapshot-error-status-title": "Ēřřőř mįģřäŧįʼnģ řęşőūřčęş", "success-message": "Ŝūččęşşƒūľľy mįģřäŧęđ {{successCount}} {{types, list}} ŧő yőūř Ğřäƒäʼnä Cľőūđ įʼnşŧäʼnčę.", + "success-message-generic": "Ŝūččęşşƒūľľy mįģřäŧęđ {{successCount}} řęşőūřčęş ŧő yőūř Ğřäƒäʼnä Cľőūđ įʼnşŧäʼnčę.", "success-title": "Mįģřäŧįőʼn čőmpľęŧęđ!", "upload-snapshot-error-title": "Ēřřőř ūpľőäđįʼnģ şʼnäpşĥőŧ" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index edb0a1a470e..e4fcf72a3b7 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -1096,7 +1096,6 @@ "help-wizard": { "download-snapshot": "", "github-comment": "", - "preview-snapshot": "", "support-bundle": "", "troubleshooting-help": "" }, @@ -1225,6 +1224,7 @@ "documentation": "", "host": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1234,11 +1234,13 @@ }, "search_filter": { "description": "", + "error": "", "label": "", "placeholder": "" }, "search-base-dns": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1440,6 +1442,7 @@ "snapshot-error-status-body": "", "snapshot-error-status-title": "", "success-message": "", + "success-message-generic": "", "success-title": "", "upload-snapshot-error-title": "" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 20bc710c828..37c08f6b213 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1087,7 +1087,6 @@ "help-wizard": { "download-snapshot": "", "github-comment": "", - "preview-snapshot": "", "support-bundle": "", "troubleshooting-help": "" }, @@ -1216,6 +1215,7 @@ "documentation": "", "host": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1225,11 +1225,13 @@ }, "search_filter": { "description": "", + "error": "", "label": "", "placeholder": "" }, "search-base-dns": { "description": "", + "error": "", "label": "", "placeholder": "" }, @@ -1430,6 +1432,7 @@ "snapshot-error-status-body": "", "snapshot-error-status-title": "", "success-message": "", + "success-message-generic": "", "success-title": "", "upload-snapshot-error-title": "" }, diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx index b40cdb66d64..465b2ef0dc3 100644 --- a/public/test/helpers/alertingRuleEditor.tsx +++ b/public/test/helpers/alertingRuleEditor.tsx @@ -1,8 +1,9 @@ -import { Route } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom-v5-compat'; import { render } from 'test/test-utils'; import { byRole, byTestId, byText } from 'testing-library-selector'; import { selectors } from '@grafana/e2e-selectors'; +import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; import RuleEditor from 'app/features/alerting/unified/RuleEditor'; export const ui = { @@ -35,11 +36,20 @@ export const ui = { }; export function renderRuleEditor(identifier?: string, recording = false) { - return render(, { - historyOptions: { - initialEntries: [ - identifier ? `/alerting/${identifier}/edit` : `/alerting/new/${recording ? 'recording' : 'alerting'}`, - ], - }, - }); + return render( + <> + + + } /> + } /> + + , + { + historyOptions: { + initialEntries: [ + identifier ? `/alerting/${identifier}/edit` : `/alerting/new/${recording ? 'recording' : 'alerting'}`, + ], + }, + } + ); } diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index f47f43ecb60..17579226432 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -117,3 +117,12 @@ global.ResizeObserver = class ResizeObserver { disconnect() {} unobserve() {} }; + +global.BroadcastChannel = class BroadcastChannel { + onmessage() {} + onmessageerror() {} + postMessage(data: unknown) {} + close() {} + addEventListener() {} + removeEventListener() {} +}; diff --git a/scripts/check-breaking-changes.sh b/scripts/check-breaking-changes.sh index 07ff141e6ba..d37a6c23669 100755 --- a/scripts/check-breaking-changes.sh +++ b/scripts/check-breaking-changes.sh @@ -55,7 +55,7 @@ while IFS=" " read -r -a package; do # (non-zero if any of the packages failed the checks) if [ "$STATUS" -gt 0 ]; then EXIT_CODE=1 - GITHUB_MESSAGE="${GITHUB_MESSAGE}**\\\`${PACKAGE_PATH}\\\`** has possible breaking changes ([more info](${GITHUB_JOB_LINK}#step:${GITHUB_STEP_NUMBER}:1))
" + GITHUB_MESSAGE="${GITHUB_MESSAGE}**\\\`${PACKAGE_PATH}\\\`** has possible breaking changes
" GITHUB_LEVITATE_MARKDOWN+="

${PACKAGE_PATH}

${CURRENT_REPORT}
" fi diff --git a/yarn.lock b/yarn.lock index 915142467a8..9f3bd02e63d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,10 +12,10 @@ __metadata: languageName: node linkType: hard -"@adobe/css-tools@npm:^4.3.2": - version: 4.3.2 - resolution: "@adobe/css-tools@npm:4.3.2" - checksum: 10/973dcb7ba5141f57ec726ddec2e94e8947361bb0c5f0e8ebd1e8aa3a84b28e66db4ad843908825f99730d59784ff3c43868b014a7268676a65950cdb850c42cc +"@adobe/css-tools@npm:^4.4.0": + version: 4.4.0 + resolution: "@adobe/css-tools@npm:4.4.0" + checksum: 10/9c6315fe9efa5075d6ddb6ded7a1424bc9c41a01f2314b6bdcc368723985fe161008d03ddcc2b27b2da50cb9c14190fbce965d15cefe5f9a31bdd43f35b52115 languageName: node linkType: hard @@ -92,16 +92,16 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/compat-data@npm:7.25.7" - checksum: 10/8fdc451e0ed9e22d1324d504b84d4452ba6f4a806b0f5c364996ee4c2a77293f79ecf4da03033acb625c90bac115c61617eb6c894c2b88486724bcbe3af1a6eb +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.25.7, @babel/compat-data@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/compat-data@npm:7.25.8" + checksum: 10/269fcb0d89e02e36c8a11e0c1b960a6b4204e88f59f20c374d28f8e318f4cd5ded42dfedc4b54162065e6a10f71c0de651f5ed3f9b45d3a4b52240196df85726 languageName: node linkType: hard -"@babel/core@npm:7.25.7, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.9, @babel/core@npm:^7.23.0, @babel/core@npm:^7.24.4": - version: 7.25.7 - resolution: "@babel/core@npm:7.25.7" +"@babel/core@npm:7.25.8, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.9, @babel/core@npm:^7.23.0, @babel/core@npm:^7.24.4": + version: 7.25.8 + resolution: "@babel/core@npm:7.25.8" dependencies: "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.25.7" @@ -109,16 +109,16 @@ __metadata: "@babel/helper-compilation-targets": "npm:^7.25.7" "@babel/helper-module-transforms": "npm:^7.25.7" "@babel/helpers": "npm:^7.25.7" - "@babel/parser": "npm:^7.25.7" + "@babel/parser": "npm:^7.25.8" "@babel/template": "npm:^7.25.7" "@babel/traverse": "npm:^7.25.7" - "@babel/types": "npm:^7.25.7" + "@babel/types": "npm:^7.25.8" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/f5fb7fb1e3ce357485cb33fe7984051a2d416472370b33144ae809df86a4663192b58cf0d828d40674d30f485790f3dd5aaf72eb659487673a4dc4be47cb3575 + checksum: 10/31eb1a8ca1a3cc0026060720eb290e68205d95c5c00fbd831e69ddc0810f5920b8eb2749db1889ac0a0312b6eddbf321d18a996a88858f3b75c9582bef9ec1e4 languageName: node linkType: hard @@ -371,14 +371,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/parser@npm:7.25.7" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.7, @babel/parser@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/parser@npm:7.25.8" dependencies: - "@babel/types": "npm:^7.25.7" + "@babel/types": "npm:^7.25.8" bin: parser: ./bin/babel-parser.js - checksum: 10/98eaa81bd378734a5f2790f02c7c076ecaba0839217445b4b84f45a7b391d640c34034253231a5bb2b2daf8204796f03584c3f94c10d46b004369bbb426a418f + checksum: 10/0396eb71e379903cedb43862f84ebb1bec809c41e82b4894d2e6e83b8e8bc636ba6eff45382e615baefdb2399ede76ca82247ecc3a9877ac16eb3140074a3276 languageName: node linkType: hard @@ -484,7 +484,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-class-properties@npm:^7.0.0, @babel/plugin-syntax-class-properties@npm:^7.12.13, @babel/plugin-syntax-class-properties@npm:^7.8.3": +"@babel/plugin-syntax-class-properties@npm:^7.0.0, @babel/plugin-syntax-class-properties@npm:^7.8.3": version: 7.12.13 resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" dependencies: @@ -495,17 +495,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-class-static-block@npm:^7.14.5": - version: 7.14.5 - resolution: "@babel/plugin-syntax-class-static-block@npm:7.14.5" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.14.5" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/3e80814b5b6d4fe17826093918680a351c2d34398a914ce6e55d8083d72a9bdde4fbaf6a2dcea0e23a03de26dc2917ae3efd603d27099e2b98380345703bf948 - languageName: node - linkType: hard - "@babel/plugin-syntax-dynamic-import@npm:^7.8.3": version: 7.8.3 resolution: "@babel/plugin-syntax-dynamic-import@npm:7.8.3" @@ -561,7 +550,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-meta@npm:^7.10.4, @babel/plugin-syntax-import-meta@npm:^7.8.3": +"@babel/plugin-syntax-import-meta@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" dependencies: @@ -594,7 +583,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4, @babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" dependencies: @@ -616,7 +605,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-numeric-separator@npm:^7.10.4, @babel/plugin-syntax-numeric-separator@npm:^7.8.3": +"@babel/plugin-syntax-numeric-separator@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" dependencies: @@ -660,18 +649,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-private-property-in-object@npm:^7.14.5": - version: 7.14.5 - resolution: "@babel/plugin-syntax-private-property-in-object@npm:7.14.5" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.14.5" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/b317174783e6e96029b743ccff2a67d63d38756876e7e5d0ba53a322e38d9ca452c13354a57de1ad476b4c066dbae699e0ca157441da611117a47af88985ecda - languageName: node - linkType: hard - -"@babel/plugin-syntax-top-level-await@npm:^7.14.5, @babel/plugin-syntax-top-level-await@npm:^7.8.3": +"@babel/plugin-syntax-top-level-await@npm:^7.8.3": version: 7.14.5 resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" dependencies: @@ -716,17 +694,16 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.25.7" +"@babel/plugin-transform-async-generator-functions@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.25.8" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.7" "@babel/helper-remap-async-to-generator": "npm:^7.25.7" - "@babel/plugin-syntax-async-generators": "npm:^7.8.4" "@babel/traverse": "npm:^7.25.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/f5e14796bcb04db7f045833d695e49bb1178162c74754f67f25fd6934ebbd0e57a59784d76a23b3f472bbd3e5a0c33d433ab60e7c6a5c7ca240b54d8ca231baa + checksum: 10/ab3f74664fc03af357e8450711de60ec77149be668059dbc0c0d616d85253117aec0e5ffb2eccda3449d0099d5fba5ef32f0e6e12a52af5f72fbca437372ece5 languageName: node linkType: hard @@ -777,16 +754,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-class-static-block@npm:7.25.7" +"@babel/plugin-transform-class-static-block@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-class-static-block@npm:7.25.8" dependencies: "@babel/helper-create-class-features-plugin": "npm:^7.25.7" "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" peerDependencies: "@babel/core": ^7.12.0 - checksum: 10/d6fa7132071860d4b6e58baa43c8efdd241d5b4ff3d31b0b6593390c7b39bf17ab549e427c08db550c84b0fb02eaad41fc96c2d299fbee4c0a6030315b0a5296 + checksum: 10/160d5f9d1dbe4dc12c2998227b51b1ccfe9f4d11b1031d0698f34403961d5b9bb995cc86acf1855102b9be365370c97d8cea243802b73c7ba7b2b18b2ac3aae9 languageName: node linkType: hard @@ -864,15 +840,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-dynamic-import@npm:7.25.7" +"@babel/plugin-transform-dynamic-import@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.25.8" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a153f2a8b10a733f884a0ba0e680387d0ba643d40774885c26cceef90cc61b8e2f946a64e1764605189498a9a04e1d4da4e886c3e26a850e7c4e5ea5fb4b3f50 + checksum: 10/cf2c105143461876f418d21893ac8f7f2b0a3c3cefb4374c3cd6338a19d3a0deed3565049f7436b94452c6471622958ef9248c7bdfeb34d2917710ac74431203 languageName: node linkType: hard @@ -888,15 +863,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.25.7" +"@babel/plugin-transform-export-namespace-from@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.25.8" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/e3ec589906380d1daa3349c358f8a56c9adc1c5003b4069321172e1e8160f2a497e9ae112ad1ac68e26dce33eb19019932cf938ad411493441ad202db0e72c2b + checksum: 10/439aac4ca1c7dbb63f021142e7abcd746049bf0d44cc5d2eb469ae3b75d90e076a43ff77190b74d8139402b53eea625b08c68651d3ce1d0a0915f5643450b3de languageName: node linkType: hard @@ -937,15 +911,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-json-strings@npm:7.25.7" +"@babel/plugin-transform-json-strings@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-json-strings@npm:7.25.8" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-json-strings": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/99e7b19d5b8f0df78eb2eaef2c1c8734e634b08910b47d5c2f61e43e75bd94a9f8e51c8baf2d2fa181bf0a7424c08b0aee29a95ae5f9e59f50e51e188dc943a1 + checksum: 10/adbc6a5a77b96db0f7e168c5fd2e56941df649808ce960f12447c1ba5d3893e9d458e7e14e3a5bd725ac5f3432ac1b3cf62b7413bbf7168a7c656dce51db711a languageName: node linkType: hard @@ -960,15 +933,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.25.7" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.25.8" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/28d00bae5637564243585e4fa044b9bc1da4b5c2ac2859e848e2db5f1c11c24dbc1ecc690755a00f593815a40d9cd99df2b31110ab68626840b22598ecf08d93 + checksum: 10/7af0e4ad63c1a59f24894b64330040966204963b75287752a2d56703c7924d3a883a3c2497e1f03c4b1792f8664e0650cf6687010dc5483444c077de1daae9f5 languageName: node linkType: hard @@ -1057,41 +1029,38 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.25.7" +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.25.8" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/af4d2d17eb0bdfa4a0414eb02a402f65226363ea3cc6d781028fd0051893918eeb3cf13aaceef8ef58ef35b36362dbcc25cd018e0c24b5b441226e086bf3b58f + checksum: 10/d742fedc1abf404d7f40065cdff9afc521236607f0d06c48d1e471f43d3a7471010d1651ba4758d80c73347a39dc278d86c43a9c814382ded4e9c7c519ace021 languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.25.7" +"@babel/plugin-transform-numeric-separator@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.25.8" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/963ff47d27b7e61e0b346a29e2ff86ccf4fb779f7069b54c09e93376e6ed769523d81b9ef4ef60ae2d8fe35f3411a25d4b2e8d0a23850afa821f47f33c7b510c + checksum: 10/e27779a309dbc5fdba71d7eae0eac5506547632b0cbf8f0add8215797bbda4f4e61595750236fee3292600cc2d13892f133beccc52b2998534e0b10c668db857 languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.25.7" +"@babel/plugin-transform-object-rest-spread@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.25.8" dependencies: "@babel/helper-compilation-targets": "npm:^7.25.7" "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" "@babel/plugin-transform-parameters": "npm:^7.25.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/b831c56921cb4818956941fd6eb54ba75fa51e5e5c1dc00466d70c79fea2a73d166a6ff02e8a27d63e40238de49f966af2bd243a8b6d281f784672f1a285d47c + checksum: 10/38f0fab8321a0b1e44784b7371f8bd5601eb885a7e9d88d7904dedda33a72f500d84792758c47e1541336c1b7592b6d956a85c2fd8e2e294f34c0303cc73442c languageName: node linkType: hard @@ -1107,28 +1076,26 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.25.7" +"@babel/plugin-transform-optional-catch-binding@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.25.8" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/b195680e047bd2e0e36debfb75e5649f0fd8c88185e33c627c5418c68f9990cb4d96faccfbf47a2fdf9ff848a9e013ca077647616aa9dc1f78fa250ef1521437 + checksum: 10/9ecf32accc5b12b83ce2f6537c9eac87f2b0f89abfe91a8a8c87ea5ece05820988415271d0fdaf7f565e2c0c837afb24fc644779029b98b1401782d9c0d73642 languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.23.0, @babel/plugin-transform-optional-chaining@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.25.7" +"@babel/plugin-transform-optional-chaining@npm:^7.23.0, @babel/plugin-transform-optional-chaining@npm:^7.25.7, @babel/plugin-transform-optional-chaining@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.25.8" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.7" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.7" - "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/9c0a742eb3111f250b95065cd7d27b31c724d3f3c29b1de3c6655632172f69d5aa658d3ebff76d3b530b6792b6f4cf4845f4132ac8a1fea6e57ff19e3ec6f531 + checksum: 10/ffb5d81e6dbb28907d5346c8e12a1ed1ea0e30170fbe609d48d0466cdbc9d11b5774c8781682693f7cf7bd39da6111980e54813af96c6b3086dc769369c67d28 languageName: node linkType: hard @@ -1155,17 +1122,16 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.25.7": - version: 7.25.7 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.25.7" +"@babel/plugin-transform-private-property-in-object@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.25.8" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.25.7" "@babel/helper-create-class-features-plugin": "npm:^7.25.7" "@babel/helper-plugin-utils": "npm:^7.25.7" - "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/c23786b9c123cceb0f69ab074377236825d76db090413db0b92d1a412b0e488e7ebde3e6155883ddbce6616114c54693485274a5d2b8a9a280b40781f80bd343 + checksum: 10/c612023879930c951e3a993104bbc3b78169aef6c38233758ee3358a7ab76954b41880bca67635df218dc6893aabad138f3783d508dc715419e62c8d1fad9088 languageName: node linkType: hard @@ -1379,11 +1345,11 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.25.7, @babel/preset-env@npm:^7.24.4": - version: 7.25.7 - resolution: "@babel/preset-env@npm:7.25.7" +"@babel/preset-env@npm:7.25.8, @babel/preset-env@npm:^7.24.4": + version: 7.25.8 + resolution: "@babel/preset-env@npm:7.25.8" dependencies: - "@babel/compat-data": "npm:^7.25.7" + "@babel/compat-data": "npm:^7.25.8" "@babel/helper-compilation-targets": "npm:^7.25.7" "@babel/helper-plugin-utils": "npm:^7.25.7" "@babel/helper-validator-option": "npm:^7.25.7" @@ -1393,45 +1359,30 @@ __metadata: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.25.7" "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.25.7" "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-async-generators": "npm:^7.8.4" - "@babel/plugin-syntax-class-properties": "npm:^7.12.13" - "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" - "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" - "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" "@babel/plugin-syntax-import-assertions": "npm:^7.25.7" "@babel/plugin-syntax-import-attributes": "npm:^7.25.7" - "@babel/plugin-syntax-import-meta": "npm:^7.10.4" - "@babel/plugin-syntax-json-strings": "npm:^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" - "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" - "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" - "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" - "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" - "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" - "@babel/plugin-syntax-top-level-await": "npm:^7.14.5" "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" "@babel/plugin-transform-arrow-functions": "npm:^7.25.7" - "@babel/plugin-transform-async-generator-functions": "npm:^7.25.7" + "@babel/plugin-transform-async-generator-functions": "npm:^7.25.8" "@babel/plugin-transform-async-to-generator": "npm:^7.25.7" "@babel/plugin-transform-block-scoped-functions": "npm:^7.25.7" "@babel/plugin-transform-block-scoping": "npm:^7.25.7" "@babel/plugin-transform-class-properties": "npm:^7.25.7" - "@babel/plugin-transform-class-static-block": "npm:^7.25.7" + "@babel/plugin-transform-class-static-block": "npm:^7.25.8" "@babel/plugin-transform-classes": "npm:^7.25.7" "@babel/plugin-transform-computed-properties": "npm:^7.25.7" "@babel/plugin-transform-destructuring": "npm:^7.25.7" "@babel/plugin-transform-dotall-regex": "npm:^7.25.7" "@babel/plugin-transform-duplicate-keys": "npm:^7.25.7" "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.25.7" - "@babel/plugin-transform-dynamic-import": "npm:^7.25.7" + "@babel/plugin-transform-dynamic-import": "npm:^7.25.8" "@babel/plugin-transform-exponentiation-operator": "npm:^7.25.7" - "@babel/plugin-transform-export-namespace-from": "npm:^7.25.7" + "@babel/plugin-transform-export-namespace-from": "npm:^7.25.8" "@babel/plugin-transform-for-of": "npm:^7.25.7" "@babel/plugin-transform-function-name": "npm:^7.25.7" - "@babel/plugin-transform-json-strings": "npm:^7.25.7" + "@babel/plugin-transform-json-strings": "npm:^7.25.8" "@babel/plugin-transform-literals": "npm:^7.25.7" - "@babel/plugin-transform-logical-assignment-operators": "npm:^7.25.7" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.25.8" "@babel/plugin-transform-member-expression-literals": "npm:^7.25.7" "@babel/plugin-transform-modules-amd": "npm:^7.25.7" "@babel/plugin-transform-modules-commonjs": "npm:^7.25.7" @@ -1439,15 +1390,15 @@ __metadata: "@babel/plugin-transform-modules-umd": "npm:^7.25.7" "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.25.7" "@babel/plugin-transform-new-target": "npm:^7.25.7" - "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.25.7" - "@babel/plugin-transform-numeric-separator": "npm:^7.25.7" - "@babel/plugin-transform-object-rest-spread": "npm:^7.25.7" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.25.8" + "@babel/plugin-transform-numeric-separator": "npm:^7.25.8" + "@babel/plugin-transform-object-rest-spread": "npm:^7.25.8" "@babel/plugin-transform-object-super": "npm:^7.25.7" - "@babel/plugin-transform-optional-catch-binding": "npm:^7.25.7" - "@babel/plugin-transform-optional-chaining": "npm:^7.25.7" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.25.8" + "@babel/plugin-transform-optional-chaining": "npm:^7.25.8" "@babel/plugin-transform-parameters": "npm:^7.25.7" "@babel/plugin-transform-private-methods": "npm:^7.25.7" - "@babel/plugin-transform-private-property-in-object": "npm:^7.25.7" + "@babel/plugin-transform-private-property-in-object": "npm:^7.25.8" "@babel/plugin-transform-property-literals": "npm:^7.25.7" "@babel/plugin-transform-regenerator": "npm:^7.25.7" "@babel/plugin-transform-reserved-words": "npm:^7.25.7" @@ -1468,7 +1419,7 @@ __metadata: semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/2d09aa2cb765a9a1c8a7566e22948e69d69de6b9b763322790bd72ec0bd5bcadf7b02abbc6ad1bab917e1f6d60b1a7f4606778d88cc7d7e1388cc52d292db97d + checksum: 10/501d78f56df8bf6f98a42da5db475db183048c4280b3292cf988b6baf01843915161f3b341ed525e2fcafcc47726798532b0e1dc7eb80aa29cc88c9d6f94ee6e languageName: node linkType: hard @@ -1554,7 +1505,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.25.7, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:7.25.7, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": version: 7.25.7 resolution: "@babel/runtime@npm:7.25.7" dependencies: @@ -1589,14 +1540,14 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.5, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.7, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": - version: 7.25.7 - resolution: "@babel/types@npm:7.25.7" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.5, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.7, @babel/types@npm:^7.25.8, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.25.8 + resolution: "@babel/types@npm:7.25.8" dependencies: "@babel/helper-string-parser": "npm:^7.25.7" "@babel/helper-validator-identifier": "npm:^7.25.7" to-fast-properties: "npm:^2.0.0" - checksum: 10/4504e16a95b6a67d50cfaa389bcbc0621019084cff73784ad4797f82d1bb76c870cb0abb6d9881d5776eb06b4607419a2b1205a08c3e87b152d74bd0884b822a + checksum: 10/973108dbb189916bb87360f2beff43ae97f1b08f1c071bc6499d363cce48b3c71674bf3b59dfd617f8c5062d1c76dc2a64232bc07b6ccef831fd0c06162d44d9 languageName: node linkType: hard @@ -3035,13 +2986,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.0.0": - version: 2.0.0 - resolution: "@formatjs/ecma402-abstract@npm:2.0.0" +"@formatjs/ecma402-abstract@npm:2.1.0": + version: 2.1.0 + resolution: "@formatjs/ecma402-abstract@npm:2.1.0" dependencies: + "@formatjs/fast-memoize": "npm:2.2.0" "@formatjs/intl-localematcher": "npm:0.5.4" tslib: "npm:^2.4.0" - checksum: 10/41543ba509ea3c7d6530d57b888115f7ca242f13462a951fae4d1d1f28bae10c999f4dea28a71d2f08366d4889a3f5276cae3a16c6f6417b841a84fd314c2234 + checksum: 10/8e25d2739c03913c61f0ec12a71ab7baf03386cff540e7470bee73e78e4fe9f09c9dfa860223f1177acdcfd0fa341041d25b2fe4e0a77e7209d0680831982bac languageName: node linkType: hard @@ -3054,6 +3006,15 @@ __metadata: languageName: node linkType: hard +"@formatjs/fast-memoize@npm:2.2.0": + version: 2.2.0 + resolution: "@formatjs/fast-memoize@npm:2.2.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/8697fe72a7ece252d600a7d08105f2a2f758e2dd96f54ac0a4c508b1205a559fc08835635e1f8e5ca9dcc3ee61ce1fca4a0e7047b402f29fc96051e293a280ff + languageName: node + linkType: hard + "@formatjs/icu-messageformat-parser@npm:2.1.7": version: 2.1.7 resolution: "@formatjs/icu-messageformat-parser@npm:2.1.7" @@ -3075,14 +3036,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-durationformat@npm:^0.2.4": - version: 0.2.4 - resolution: "@formatjs/intl-durationformat@npm:0.2.4" +"@formatjs/intl-durationformat@npm:^0.3.0": + version: 0.3.0 + resolution: "@formatjs/intl-durationformat@npm:0.3.0" dependencies: - "@formatjs/ecma402-abstract": "npm:2.0.0" + "@formatjs/ecma402-abstract": "npm:2.1.0" "@formatjs/intl-localematcher": "npm:0.5.4" tslib: "npm:^2.4.0" - checksum: 10/5f500409a20d18967e17ffbc222f9b4c4bf7ef08cce20023c33f06d1989c2bc4cf700d1dd1d048748d0a36c882109d5375896a4964d6700f73ec18914c6de4ba + checksum: 10/96625ba190040e8233a8fd0b8c73347a1e780641868ba429910da046fc971b3c6116b1f93f6020e0eafc1d7ecda6ea1cd25f6048087a10300278fa31495534d3 languageName: node linkType: hard @@ -3133,16 +3094,16 @@ __metadata: resolution: "@grafana-plugins/grafana-azure-monitor-datasource@workspace:public/app/plugins/datasource/azuremonitor" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" - "@grafana/experimental": "npm:2.1.1" - "@grafana/plugin-configs": "npm:11.3.0-pre" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/schema": "npm:11.3.0-pre" - "@grafana/ui": "npm:11.3.0-pre" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" + "@grafana/experimental": "npm:2.1.2" + "@grafana/plugin-configs": "npm:11.4.0-pre" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/schema": "npm:11.4.0-pre" + "@grafana/ui": "npm:11.4.0-pre" "@kusto/monaco-kusto": "npm:^10.0.0" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/react": "npm:15.0.2" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.13" "@types/lodash": "npm:4.17.10" @@ -3177,14 +3138,15 @@ __metadata: resolution: "@grafana-plugins/grafana-postgresql-datasource@workspace:public/app/plugins/datasource/grafana-postgresql-datasource" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" - "@grafana/experimental": "npm:2.1.1" - "@grafana/plugin-configs": "npm:11.3.0-pre" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/sql": "npm:11.3.0-pre" - "@grafana/ui": "npm:11.3.0-pre" - "@testing-library/react": "npm:15.0.2" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" + "@grafana/experimental": "npm:2.1.2" + "@grafana/plugin-configs": "npm:11.4.0-pre" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/sql": "npm:11.4.0-pre" + "@grafana/ui": "npm:11.4.0-pre" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.13" "@types/lodash": "npm:4.17.10" @@ -3208,14 +3170,14 @@ __metadata: resolution: "@grafana-plugins/grafana-pyroscope-datasource@workspace:public/app/plugins/datasource/grafana-pyroscope-datasource" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/plugin-configs": "npm:11.3.0-pre" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/schema": "npm:11.3.0-pre" - "@grafana/ui": "npm:11.3.0-pre" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/jest-dom": "npm:6.4.2" - "@testing-library/react": "npm:15.0.2" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/plugin-configs": "npm:11.4.0-pre" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/schema": "npm:11.4.0-pre" + "@grafana/ui": "npm:11.4.0-pre" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.13" "@types/lodash": "npm:4.17.10" @@ -3249,15 +3211,15 @@ __metadata: resolution: "@grafana-plugins/grafana-testdata-datasource@workspace:public/app/plugins/datasource/grafana-testdata-datasource" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" - "@grafana/experimental": "npm:2.1.1" - "@grafana/plugin-configs": "npm:11.3.0-pre" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/schema": "npm:11.3.0-pre" - "@grafana/ui": "npm:11.3.0-pre" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/react": "npm:15.0.2" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" + "@grafana/experimental": "npm:2.1.2" + "@grafana/plugin-configs": "npm:11.4.0-pre" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/schema": "npm:11.4.0-pre" + "@grafana/ui": "npm:11.4.0-pre" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.13" @@ -3292,14 +3254,14 @@ __metadata: "@emotion/css": "npm:11.13.4" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" - "@grafana/experimental": "npm:2.1.1" + "@grafana/experimental": "npm:2.1.2" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-configs": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/jest-dom": "npm:6.4.2" - "@testing-library/react": "npm:15.0.2" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.13" "@types/lodash": "npm:4.17.10" @@ -3332,14 +3294,15 @@ __metadata: resolution: "@grafana-plugins/mssql@workspace:public/app/plugins/datasource/mssql" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" - "@grafana/experimental": "npm:2.1.1" - "@grafana/plugin-configs": "npm:11.3.0-pre" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/sql": "npm:11.3.0-pre" - "@grafana/ui": "npm:11.3.0-pre" - "@testing-library/react": "npm:15.0.2" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" + "@grafana/experimental": "npm:2.1.2" + "@grafana/plugin-configs": "npm:11.4.0-pre" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/sql": "npm:11.4.0-pre" + "@grafana/ui": "npm:11.4.0-pre" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.13" "@types/lodash": "npm:4.17.10" @@ -3363,14 +3326,15 @@ __metadata: resolution: "@grafana-plugins/mysql@workspace:public/app/plugins/datasource/mysql" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" - "@grafana/experimental": "npm:2.1.1" - "@grafana/plugin-configs": "npm:11.3.0-pre" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/sql": "npm:11.3.0-pre" - "@grafana/ui": "npm:11.3.0-pre" - "@testing-library/react": "npm:15.0.2" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" + "@grafana/experimental": "npm:2.1.2" + "@grafana/plugin-configs": "npm:11.4.0-pre" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/sql": "npm:11.4.0-pre" + "@grafana/ui": "npm:11.4.0-pre" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.13" "@types/lodash": "npm:4.17.10" @@ -3394,13 +3358,13 @@ __metadata: resolution: "@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/plugin-configs": "npm:11.3.0-pre" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/schema": "npm:11.3.0-pre" - "@grafana/ui": "npm:11.3.0-pre" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/react": "npm:15.0.2" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/plugin-configs": "npm:11.4.0-pre" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/schema": "npm:11.4.0-pre" + "@grafana/ui": "npm:11.4.0-pre" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.17.10" "@types/node": "npm:20.16.11" @@ -3426,16 +3390,16 @@ __metadata: resolution: "@grafana-plugins/stackdriver@workspace:public/app/plugins/datasource/cloud-monitoring" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" - "@grafana/experimental": "npm:2.1.1" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" + "@grafana/experimental": "npm:2.1.2" "@grafana/google-sdk": "npm:0.1.2" - "@grafana/plugin-configs": "npm:11.3.0-pre" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/schema": "npm:11.3.0-pre" - "@grafana/ui": "npm:11.3.0-pre" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/react": "npm:15.0.2" + "@grafana/plugin-configs": "npm:11.4.0-pre" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/schema": "npm:11.4.0-pre" + "@grafana/ui": "npm:11.4.0-pre" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.13" @@ -3476,12 +3440,12 @@ __metadata: "@emotion/css": "npm:11.13.4" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" - "@grafana/experimental": "npm:2.1.1" + "@grafana/experimental": "npm:2.1.2" "@grafana/lezer-logql": "npm:0.2.6" "@grafana/lezer-traceql": "npm:0.0.19" "@grafana/monaco-logql": "npm:^0.0.7" "@grafana/o11y-ds-frontend": "workspace:*" - "@grafana/plugin-configs": "npm:11.3.0-pre" + "@grafana/plugin-configs": "npm:11.4.0-pre" "@grafana/runtime": "workspace:*" "@grafana/schema": "workspace:*" "@grafana/ui": "workspace:*" @@ -3490,9 +3454,9 @@ __metadata: "@opentelemetry/api": "npm:1.9.0" "@opentelemetry/exporter-collector": "npm:0.25.0" "@opentelemetry/semantic-conventions": "npm:1.27.0" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/jest-dom": "npm:6.4.2" - "@testing-library/react": "npm:15.0.2" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.13" "@types/lodash": "npm:4.17.10" @@ -3536,13 +3500,14 @@ __metadata: "@emotion/css": "npm:11.13.4" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" - "@grafana/experimental": "npm:2.1.1" + "@grafana/experimental": "npm:2.1.2" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-configs": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" - "@testing-library/jest-dom": "npm:6.4.2" - "@testing-library/react": "npm:15.0.2" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/react": "npm:16.0.1" "@types/jest": "npm:29.5.13" "@types/lodash": "npm:4.17.10" "@types/node": "npm:20.16.11" @@ -3591,12 +3556,12 @@ __metadata: languageName: node linkType: hard -"@grafana/data@npm:11.3.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": +"@grafana/data@npm:11.4.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": "npm:7.0.1" - "@grafana/schema": "npm:11.3.0-pre" + "@grafana/schema": "npm:11.4.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:15.3.0" "@types/d3-interpolate": "npm:^3.0.0" @@ -3644,7 +3609,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@npm:11.3.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": +"@grafana/e2e-selectors@npm:11.4.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: @@ -3732,6 +3697,32 @@ __metadata: languageName: node linkType: hard +"@grafana/experimental@npm:2.1.2": + version: 2.1.2 + resolution: "@grafana/experimental@npm:2.1.2" + dependencies: + "@hello-pangea/dnd": "npm:^16.6.0" + "@types/uuid": "npm:^8.3.3" + lodash: "npm:^4.17.21" + prismjs: "npm:^1.29.0" + react-popper-tooltip: "npm:^4.4.2" + react-use: "npm:^17.4.2" + semver: "npm:^7.5.4" + uuid: "npm:^8.3.2" + peerDependencies: + "@emotion/css": ^11.11.2 + "@grafana/data": ^10.4.0 || ^11.0.0 + "@grafana/e2e-selectors": ^10.0.0 || ^11.0.0 + "@grafana/runtime": ^10.4.0 || ^11.0.0 + "@grafana/ui": ^10.4.0 || ^11.0.0 + react: ^18.2.0 + react-dom: ^18.2.0 + react-select: ^5.8.0 + rxjs: ^7.8.1 + checksum: 10/6a00737f870a842d1178ab8515de06fb5eb845a5d6f2adcb32e3dcb5bc76af7d2f72c8c5ea5cedbb22526277bdd32916089bad47b9b04a65b95780244ac1558f + languageName: node + linkType: hard + "@grafana/faro-core@npm:^1.10.2, @grafana/faro-core@npm:^1.3.6": version: 1.10.2 resolution: "@grafana/faro-core@npm:1.10.2" @@ -3777,18 +3768,18 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/flamegraph@workspace:packages/grafana-flamegraph" dependencies: - "@babel/core": "npm:7.25.7" - "@babel/preset-env": "npm:7.25.7" + "@babel/core": "npm:7.25.8" + "@babel/preset-env": "npm:7.25.8" "@babel/preset-react": "npm:7.25.7" "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" + "@grafana/data": "npm:11.4.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:11.3.0-pre" + "@grafana/ui": "npm:11.4.0-pre" "@leeoniya/ufuzzy": "npm:1.0.14" "@rollup/plugin-node-resolve": "npm:15.3.0" - "@testing-library/dom": "npm:10.0.0" + "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.1.2" - "@testing-library/react": "npm:15.0.2" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" @@ -3865,16 +3856,16 @@ __metadata: resolution: "@grafana/o11y-ds-frontend@workspace:packages/grafana-o11y-ds-frontend" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" - "@grafana/experimental": "npm:2.1.1" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/schema": "npm:11.3.0-pre" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" + "@grafana/experimental": "npm:2.1.2" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/schema": "npm:11.4.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:11.3.0-pre" - "@testing-library/dom": "npm:10.0.0" + "@grafana/ui": "npm:11.4.0-pre" + "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.1.2" - "@testing-library/react": "npm:15.0.2" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/node": "npm:20.16.11" @@ -3896,13 +3887,13 @@ __metadata: languageName: unknown linkType: soft -"@grafana/plugin-configs@npm:11.3.0-pre, @grafana/plugin-configs@workspace:*, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs": +"@grafana/plugin-configs@npm:11.4.0-pre, @grafana/plugin-configs@workspace:*, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs": version: 0.0.0-use.local resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs" dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@swc/core": "npm:1.4.2" - "@types/eslint": "npm:8.56.10" + "@types/eslint": "npm:9.6.1" copy-webpack-plugin: "npm:12.0.2" eslint: "npm:8.57.0" eslint-webpack-plugin: "npm:4.2.0" @@ -3936,15 +3927,15 @@ __metadata: "@emotion/css": "npm:11.13.4" "@emotion/eslint-plugin": "npm:11.12.0" "@floating-ui/react": "npm:0.26.24" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" - "@grafana/experimental": "npm:2.1.1" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" + "@grafana/experimental": "npm:2.1.2" "@grafana/faro-web-sdk": "npm:1.10.2" - "@grafana/runtime": "npm:11.3.0-pre" - "@grafana/schema": "npm:11.3.0-pre" + "@grafana/runtime": "npm:11.4.0-pre" + "@grafana/schema": "npm:11.4.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:11.3.0-pre" - "@hello-pangea/dnd": "npm:16.6.0" + "@grafana/ui": "npm:11.4.0-pre" + "@hello-pangea/dnd": "npm:17.0.0" "@leeoniya/ufuzzy": "npm:1.0.14" "@lezer/common": "npm:1.2.2" "@lezer/highlight": "npm:1.2.1" @@ -3955,13 +3946,13 @@ __metadata: "@rollup/plugin-node-resolve": "npm:15.3.0" "@swc/core": "npm:1.4.2" "@swc/helpers": "npm:0.5.13" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/jest-dom": "npm:6.4.2" - "@testing-library/react": "npm:15.0.2" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:7.4.3" "@types/debounce-promise": "npm:3.1.9" - "@types/eslint": "npm:8.56.10" + "@types/eslint": "npm:9.6.1" "@types/jest": "npm:29.5.13" "@types/jquery": "npm:3.5.31" "@types/lodash": "npm:4.17.10" @@ -4022,7 +4013,7 @@ __metadata: rollup-plugin-esbuild: "npm:6.1.1" rollup-plugin-node-externals: "npm:^7.1.3" rxjs: "npm:7.8.1" - sass: "npm:1.79.3" + sass: "npm:1.79.5" sass-loader: "npm:16.0.2" semver: "npm:7.6.3" style-loader: "npm:4.0.0" @@ -4040,20 +4031,20 @@ __metadata: languageName: unknown linkType: soft -"@grafana/runtime@npm:11.3.0-pre, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime": +"@grafana/runtime@npm:11.4.0-pre, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime": version: 0.0.0-use.local resolution: "@grafana/runtime@workspace:packages/grafana-runtime" dependencies: - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" "@grafana/faro-web-sdk": "npm:^1.3.6" - "@grafana/schema": "npm:11.3.0-pre" + "@grafana/schema": "npm:11.4.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:11.3.0-pre" + "@grafana/ui": "npm:11.4.0-pre" "@rollup/plugin-node-resolve": "npm:15.3.0" "@rollup/plugin-terser": "npm:0.4.4" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/react": "npm:15.0.2" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/angular": "npm:1.8.9" "@types/history": "npm:4.7.11" @@ -4086,7 +4077,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/saga-icons@workspace:packages/grafana-icons" dependencies: - "@babel/core": "npm:7.25.7" + "@babel/core": "npm:7.25.8" "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:^15.3.0" "@rollup/plugin-typescript": "npm:^12.1.0" @@ -4117,9 +4108,27 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:5.18.3": - version: 5.18.3 - resolution: "@grafana/scenes@npm:5.18.3" +"@grafana/scenes-react@npm:5.20.0": + version: 5.20.0 + resolution: "@grafana/scenes-react@npm:5.20.0" + dependencies: + "@grafana/e2e-selectors": "npm:^11.0.0" + "@grafana/scenes": "npm:5.20.0" + react-use: "npm:17.4.0" + peerDependencies: + "@grafana/data": ^11.0.0 + "@grafana/runtime": ^11.0.0 + "@grafana/schema": ^11.0.0 + "@grafana/ui": ^11.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10/9e4b4f678ad4e072876eaa018e6c67e2fd19d125202b95f8a2f18f113d25693a4a2387ada17cd032f5f11ed2fd189ef471148a25e74f55cc620f22c5e6016a48 + languageName: node + linkType: hard + +"@grafana/scenes@npm:5.20.0": + version: 5.20.0 + resolution: "@grafana/scenes@npm:5.20.0" dependencies: "@floating-ui/react": "npm:0.26.16" "@grafana/e2e-selectors": "npm:^11.0.0" @@ -4136,11 +4145,11 @@ __metadata: "@grafana/ui": ">=10.4" react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/fb1179db5b53d709b859facc7742473dbd13947c2ec9593035e617f32166716de20b712002c14e1eec7cf90a80e58c60cdc039a5252a54c7ab1f8659bba64476 + checksum: 10/0bc36324f5c109175199b5e9523323397acd6b95c0be11856f54394bb2a51eb4e0720ecabd37acac9966a7de69f9ddcf330c1ed03e0c196b76e7188f65de88c1 languageName: node linkType: hard -"@grafana/schema@npm:11.3.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": +"@grafana/schema@npm:11.4.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: @@ -4158,21 +4167,21 @@ __metadata: languageName: unknown linkType: soft -"@grafana/sql@npm:11.3.0-pre, @grafana/sql@workspace:*, @grafana/sql@workspace:packages/grafana-sql": +"@grafana/sql@npm:11.4.0-pre, @grafana/sql@workspace:*, @grafana/sql@workspace:packages/grafana-sql": version: 0.0.0-use.local resolution: "@grafana/sql@workspace:packages/grafana-sql" dependencies: "@emotion/css": "npm:11.13.4" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" - "@grafana/experimental": "npm:2.1.1" - "@grafana/runtime": "npm:11.3.0-pre" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" + "@grafana/experimental": "npm:2.1.2" + "@grafana/runtime": "npm:11.4.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:11.3.0-pre" + "@grafana/ui": "npm:11.4.0-pre" "@react-awesome-query-builder/ui": "npm:6.6.3" - "@testing-library/dom": "npm:10.0.0" + "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.1.2" - "@testing-library/react": "npm:15.0.2" + "@testing-library/react": "npm:16.0.1" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" @@ -4218,22 +4227,22 @@ __metadata: languageName: node linkType: hard -"@grafana/ui@npm:11.3.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": +"@grafana/ui@npm:11.4.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": "npm:7.25.7" + "@babel/core": "npm:7.25.8" "@emotion/css": "npm:11.13.4" "@emotion/react": "npm:11.13.3" "@emotion/serialize": "npm:1.3.2" "@faker-js/faker": "npm:^9.0.0" "@floating-ui/react": "npm:0.26.24" - "@grafana/data": "npm:11.3.0-pre" - "@grafana/e2e-selectors": "npm:11.3.0-pre" + "@grafana/data": "npm:11.4.0-pre" + "@grafana/e2e-selectors": "npm:11.4.0-pre" "@grafana/faro-web-sdk": "npm:^1.3.6" - "@grafana/schema": "npm:11.3.0-pre" + "@grafana/schema": "npm:11.4.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@hello-pangea/dnd": "npm:16.6.0" + "@hello-pangea/dnd": "npm:17.0.0" "@leeoniya/ufuzzy": "npm:1.0.14" "@monaco-editor/react": "npm:4.6.0" "@popperjs/core": "npm:2.11.8" @@ -4259,9 +4268,9 @@ __metadata: "@storybook/react-webpack5": "npm:^8.1.6" "@storybook/theming": "npm:^8.1.6" "@tanstack/react-virtual": "npm:^3.5.1" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/jest-dom": "npm:6.4.2" - "@testing-library/react": "npm:15.0.2" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/chance": "npm:1.1.6" "@types/common-tags": "npm:^1.8.0" @@ -4375,7 +4384,25 @@ __metadata: languageName: node linkType: hard -"@hello-pangea/dnd@npm:16.6.0, @hello-pangea/dnd@npm:^16.6.0": +"@hello-pangea/dnd@npm:17.0.0": + version: 17.0.0 + resolution: "@hello-pangea/dnd@npm:17.0.0" + dependencies: + "@babel/runtime": "npm:^7.25.6" + css-box-model: "npm:^1.2.1" + memoize-one: "npm:^6.0.0" + raf-schd: "npm:^4.0.3" + react-redux: "npm:^9.1.2" + redux: "npm:^5.0.1" + use-memo-one: "npm:^1.1.3" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10/4795063e249a818c60e223f3527797878cb546ef007a52a7dd6c1a01094d3b2107820476a10fc83c0ba9dc4387c1ae49e70c8f8cff9722636219773caad19372 + languageName: node + linkType: hard + +"@hello-pangea/dnd@npm:^16.6.0": version: 16.6.0 resolution: "@hello-pangea/dnd@npm:16.6.0" dependencies: @@ -6120,6 +6147,140 @@ __metadata: languageName: node linkType: hard +"@parcel/watcher-android-arm64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-android-arm64@npm:2.4.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-arm64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-darwin-arm64@npm:2.4.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-x64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-darwin-x64@npm:2.4.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-freebsd-x64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-freebsd-x64@npm:2.4.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-glibc@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-arm-glibc@npm:2.4.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-glibc@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.4.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-musl@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-arm64-musl@npm:2.4.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-glibc@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-x64-glibc@npm:2.4.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-musl@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-linux-x64-musl@npm:2.4.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-win32-arm64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-win32-arm64@npm:2.4.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-win32-ia32@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-win32-ia32@npm:2.4.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@parcel/watcher-win32-x64@npm:2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher-win32-x64@npm:2.4.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher@npm:^2.4.1": + version: 2.4.1 + resolution: "@parcel/watcher@npm:2.4.1" + dependencies: + "@parcel/watcher-android-arm64": "npm:2.4.1" + "@parcel/watcher-darwin-arm64": "npm:2.4.1" + "@parcel/watcher-darwin-x64": "npm:2.4.1" + "@parcel/watcher-freebsd-x64": "npm:2.4.1" + "@parcel/watcher-linux-arm-glibc": "npm:2.4.1" + "@parcel/watcher-linux-arm64-glibc": "npm:2.4.1" + "@parcel/watcher-linux-arm64-musl": "npm:2.4.1" + "@parcel/watcher-linux-x64-glibc": "npm:2.4.1" + "@parcel/watcher-linux-x64-musl": "npm:2.4.1" + "@parcel/watcher-win32-arm64": "npm:2.4.1" + "@parcel/watcher-win32-ia32": "npm:2.4.1" + "@parcel/watcher-win32-x64": "npm:2.4.1" + detect-libc: "npm:^1.0.3" + is-glob: "npm:^4.0.3" + micromatch: "npm:^4.0.5" + node-addon-api: "npm:^7.0.0" + node-gyp: "npm:latest" + dependenciesMeta: + "@parcel/watcher-android-arm64": + optional: true + "@parcel/watcher-darwin-arm64": + optional: true + "@parcel/watcher-darwin-x64": + optional: true + "@parcel/watcher-freebsd-x64": + optional: true + "@parcel/watcher-linux-arm-glibc": + optional: true + "@parcel/watcher-linux-arm64-glibc": + optional: true + "@parcel/watcher-linux-arm64-musl": + optional: true + "@parcel/watcher-linux-x64-glibc": + optional: true + "@parcel/watcher-linux-x64-musl": + optional: true + "@parcel/watcher-win32-arm64": + optional: true + "@parcel/watcher-win32-ia32": + optional: true + "@parcel/watcher-win32-x64": + optional: true + checksum: 10/c163dff1828fa249c00f24931332dea5a8f2fcd1bfdd0e304ccdf7619c58bff044526fa39241fd2121d2a2141f71775ce3117450d78c4df3070d152282017644 + languageName: node + linkType: hard + "@petamoriken/float16@npm:^3.4.7": version: 3.5.0 resolution: "@petamoriken/float16@npm:3.5.0" @@ -6837,18 +6998,7 @@ __metadata: languageName: node linkType: hard -"@react-types/button@npm:3.9.6": - version: 3.9.6 - resolution: "@react-types/button@npm:3.9.6" - dependencies: - "@react-types/shared": "npm:^3.24.1" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/348096091b39b9cfeaf3b11b4ff262652954ea1793008aa2acc005ca32f299db550f08fe076498501547c2a9a06c46d2000f202fc0dbe853a1202d6523b71449 - languageName: node - linkType: hard - -"@react-types/button@npm:^3.10.0": +"@react-types/button@npm:3.10.0, @react-types/button@npm:^3.10.0": version: 3.10.0 resolution: "@react-types/button@npm:3.10.0" dependencies: @@ -6894,16 +7044,7 @@ __metadata: languageName: node linkType: hard -"@react-types/shared@npm:3.24.1": - version: 3.24.1 - resolution: "@react-types/shared@npm:3.24.1" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 - checksum: 10/5472ae35f65b2ed7c12d5ea4459f34b4aec065d2633844031d27945495b6dca6fa9bf02b6392b901fac97252e58d9b91a4baf53f4c281397fb81ce85c73b8648 - languageName: node - linkType: hard - -"@react-types/shared@npm:^3.24.1, @react-types/shared@npm:^3.25.0": +"@react-types/shared@npm:3.25.0, @react-types/shared@npm:^3.25.0": version: 3.25.0 resolution: "@react-types/shared@npm:3.25.0" peerDependencies: @@ -7036,114 +7177,114 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.22.5" +"@rollup/rollup-android-arm-eabi@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.24.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-android-arm64@npm:4.22.5" +"@rollup/rollup-android-arm64@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-android-arm64@npm:4.24.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-darwin-arm64@npm:4.22.5" +"@rollup/rollup-darwin-arm64@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.24.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-darwin-x64@npm:4.22.5" +"@rollup/rollup-darwin-x64@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.24.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.22.5" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.24.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.22.5" +"@rollup/rollup-linux-arm-musleabihf@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.24.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.22.5" +"@rollup/rollup-linux-arm64-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.24.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.22.5" +"@rollup/rollup-linux-arm64-musl@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.24.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.22.5" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.24.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.22.5" +"@rollup/rollup-linux-riscv64-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.24.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.22.5" +"@rollup/rollup-linux-s390x-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.24.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.22.5" +"@rollup/rollup-linux-x64-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.24.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.22.5" +"@rollup/rollup-linux-x64-musl@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.24.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.22.5" +"@rollup/rollup-win32-arm64-msvc@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.24.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.22.5" +"@rollup/rollup-win32-ia32-msvc@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.24.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.22.5": - version: 4.22.5 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.22.5" +"@rollup/rollup-win32-x64-msvc@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.24.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -9172,7 +9313,7 @@ __metadata: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*" "@grafana/eslint-config": "npm:7.0.0" - "@grafana/plugin-configs": "npm:11.3.0-pre" + "@grafana/plugin-configs": "npm:11.4.0-pre" "@grafana/runtime": "workspace:*" "@grafana/schema": "workspace:*" "@grafana/ui": "workspace:*" @@ -9198,9 +9339,9 @@ __metadata: languageName: unknown linkType: soft -"@testing-library/dom@npm:10.0.0, @testing-library/dom@npm:>=7, @testing-library/dom@npm:^10.0.0": - version: 10.0.0 - resolution: "@testing-library/dom@npm:10.0.0" +"@testing-library/dom@npm:10.4.0, @testing-library/dom@npm:>=7": + version: 10.4.0 + resolution: "@testing-library/dom@npm:10.4.0" dependencies: "@babel/code-frame": "npm:^7.10.4" "@babel/runtime": "npm:^7.12.5" @@ -9210,40 +9351,22 @@ __metadata: dom-accessibility-api: "npm:^0.5.9" lz-string: "npm:^1.5.0" pretty-format: "npm:^27.0.2" - checksum: 10/d0d0ffffed0dae705c5c615d8779348743e66704b2da1ce3e2df7c2d038cde55d7f11819e9b50ca53d3ec815050fab928c8845316c2f713141b9fb8ad50921f6 + checksum: 10/05825ee9a15b88cbdae12c137db7111c34069ed3c7a1bd03b6696cb1b37b29f6f2d2de581ebf03033e7df1ab7ebf08399310293f440a4845d95c02c0a9ecc899 languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.4.2, @testing-library/jest-dom@npm:^6.1.2": - version: 6.4.2 - resolution: "@testing-library/jest-dom@npm:6.4.2" +"@testing-library/jest-dom@npm:6.5.0, @testing-library/jest-dom@npm:^6.1.2": + version: 6.5.0 + resolution: "@testing-library/jest-dom@npm:6.5.0" dependencies: - "@adobe/css-tools": "npm:^4.3.2" - "@babel/runtime": "npm:^7.9.2" + "@adobe/css-tools": "npm:^4.4.0" aria-query: "npm:^5.0.0" chalk: "npm:^3.0.0" css.escape: "npm:^1.5.1" dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.15" + lodash: "npm:^4.17.21" redent: "npm:^3.0.0" - peerDependencies: - "@jest/globals": ">= 28" - "@types/bun": "*" - "@types/jest": ">= 28" - jest: ">= 28" - vitest: ">= 0.32" - peerDependenciesMeta: - "@jest/globals": - optional: true - "@types/bun": - optional: true - "@types/jest": - optional: true - jest: - optional: true - vitest: - optional: true - checksum: 10/7ee1e51caffad032734a4a43a00bf72d49080cf1bbf53021b443e91c7fa3762a66f55ce68f1c6643590fe66fbc4df92142659b8cf17c92166a3fb22691987e0d + checksum: 10/3d2080888af5fd7306f57448beb5a23f55d965e265b5e53394fffc112dfb0678d616a5274ff0200c46c7618f293520f86fc8562eecd8bdbc0dbb3294d63ec431 languageName: node linkType: hard @@ -9269,17 +9392,23 @@ __metadata: languageName: node linkType: hard -"@testing-library/react@npm:15.0.2": - version: 15.0.2 - resolution: "@testing-library/react@npm:15.0.2" +"@testing-library/react@npm:16.0.1": + version: 16.0.1 + resolution: "@testing-library/react@npm:16.0.1" dependencies: "@babel/runtime": "npm:^7.12.5" - "@testing-library/dom": "npm:^10.0.0" - "@types/react-dom": "npm:^18.0.0" peerDependencies: + "@testing-library/dom": ^10.0.0 + "@types/react": ^18.0.0 + "@types/react-dom": ^18.0.0 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/6327e465733cec9455792acee7781d1437f7494ddbb976ca72e07446daaf8e7fe5dadd27d05690f09e8d4adce651984ed9aeca4ede9b87bf69b172f450085ede + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10/904b48881cf5bd208e25899e168f5c99c78ed6d77389544838d9d861a038d2c5c5385863ee9a367436770cbf7d21c5e05a991b9e24a33806e9ac985df2448185 languageName: node linkType: hard @@ -9894,7 +10023,17 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:8.56.10, @types/eslint@npm:^8.56.10": +"@types/eslint@npm:*, @types/eslint@npm:9.6.1": + version: 9.6.1 + resolution: "@types/eslint@npm:9.6.1" + dependencies: + "@types/estree": "npm:*" + "@types/json-schema": "npm:*" + checksum: 10/719fcd255760168a43d0e306ef87548e1e15bffe361d5f4022b0f266575637acc0ecb85604ac97879ee8ae83c6a6d0613b0ed31d0209ddf22a0fe6d608fc56fe + languageName: node + linkType: hard + +"@types/eslint@npm:^8.56.10": version: 8.56.10 resolution: "@types/eslint@npm:8.56.10" dependencies: @@ -10444,7 +10583,7 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:*, @types/react-dom@npm:18.2.25, @types/react-dom@npm:^18.0.0": +"@types/react-dom@npm:*, @types/react-dom@npm:18.2.25": version: 18.2.25 resolution: "@types/react-dom@npm:18.2.25" dependencies: @@ -15358,15 +15497,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6": - version: 4.3.6 - resolution: "debug@npm:4.3.6" +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7": + version: 4.3.7 + resolution: "debug@npm:4.3.7" dependencies: - ms: "npm:2.1.2" + ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/d3adb9af7d57a9e809a68f404490cf776122acca16e6359a2702c0f462e510e91f9765c07f707b8ab0d91e03bad57328f3256f5082631cefb5393d0394d50fb7 + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a languageName: node linkType: hard @@ -15652,6 +15791,15 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^1.0.3": + version: 1.0.3 + resolution: "detect-libc@npm:1.0.3" + bin: + detect-libc: ./bin/detect-libc.js + checksum: 10/3849fe7720feb153e4ac9407086956e073f1ce1704488290ef0ca8aab9430a8d48c8a9f8351889e7cdc64e5b1128589501e4fef48f3a4a49ba92cd6d112d0757 + languageName: node + linkType: hard + "detect-libc@npm:^2.0.0": version: 2.0.3 resolution: "detect-libc@npm:2.0.3" @@ -16484,16 +16632,16 @@ __metadata: languageName: node linkType: hard -"esbuild-plugin-browserslist@npm:^0.14.0": - version: 0.14.0 - resolution: "esbuild-plugin-browserslist@npm:0.14.0" +"esbuild-plugin-browserslist@npm:^0.15.0": + version: 0.15.0 + resolution: "esbuild-plugin-browserslist@npm:0.15.0" dependencies: - debug: "npm:^4.3.5" + debug: "npm:^4.3.7" zod: "npm:^3.23.8" peerDependencies: browserslist: ^4.21.8 - esbuild: ~0.23.0 - checksum: 10/634d8a562597dd3c21cad10785153968d7b80f97ee8d11bac9611e1aa524291084e1c5965213442f1a9d07a98fc8862bd00921aa7cb130716f67d2ac8c2a6cc2 + esbuild: ~0.24.0 + checksum: 10/b1afe26f5c013a37664ff6ca2f8190d50e36f719afca8aba2c0b18f0fd0f34de3334db1435154a70dbfeb5e91acf732cc20296bb3fd3cbdfe68ae37dadcffe32 languageName: node linkType: hard @@ -16927,21 +17075,21 @@ __metadata: languageName: node linkType: hard -"eslint-module-utils@npm:^2.9.0": - version: 2.9.0 - resolution: "eslint-module-utils@npm:2.9.0" +"eslint-module-utils@npm:^2.12.0": + version: 2.12.0 + resolution: "eslint-module-utils@npm:2.12.0" dependencies: debug: "npm:^3.2.7" peerDependenciesMeta: eslint: optional: true - checksum: 10/13e001c96a6ce8d3d7ad6798c9b86351820c9c4a9abc5a152e84b838d7937a781471b0128ee690d18def226741fc96e8c5cff78c059bdcafe9ab8625777fcf2a + checksum: 10/dd27791147eca17366afcb83f47d6825b6ce164abb256681e5de4ec1d7e87d8605641eb869298a0dbc70665e2446dbcc2f40d3e1631a9475dd64dd23d4ca5dee languageName: node linkType: hard "eslint-plugin-import@npm:^2.26.0": - version: 2.30.0 - resolution: "eslint-plugin-import@npm:2.30.0" + version: 2.31.0 + resolution: "eslint-plugin-import@npm:2.31.0" dependencies: "@rtsao/scc": "npm:^1.1.0" array-includes: "npm:^3.1.8" @@ -16951,7 +17099,7 @@ __metadata: debug: "npm:^3.2.7" doctrine: "npm:^2.1.0" eslint-import-resolver-node: "npm:^0.3.9" - eslint-module-utils: "npm:^2.9.0" + eslint-module-utils: "npm:^2.12.0" hasown: "npm:^2.0.2" is-core-module: "npm:^2.15.1" is-glob: "npm:^4.0.3" @@ -16960,10 +17108,11 @@ __metadata: object.groupby: "npm:^1.0.3" object.values: "npm:^1.2.0" semver: "npm:^6.3.1" + string.prototype.trimend: "npm:^1.0.8" tsconfig-paths: "npm:^3.15.0" peerDependencies: - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - checksum: 10/a5f85dfe76e27286c28a01d137769726ce3f758bcc03aa6b6f9e18700a40a08f57239f82e07efcab763c4b03a02d425edcc29fbecf40aad0124286978c6bc63c + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + checksum: 10/6b76bd009ac2db0615d9019699d18e2a51a86cb8c1d0855a35fb1b418be23b40239e6debdc6e8c92c59f1468ed0ea8d7b85c817117a113d5cc225be8a02ad31c languageName: node linkType: hard @@ -17182,12 +17331,12 @@ __metadata: linkType: hard "eslint-scope@npm:^8.0.0": - version: 8.0.2 - resolution: "eslint-scope@npm:8.0.2" + version: 8.1.0 + resolution: "eslint-scope@npm:8.1.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10/d17c2e1ff4d3a98911414a954531078db912e2747d6da8ea4cafd16d0526e32086c676ce9aeaffb3ca0ff695fc951ac3169d7f08a0b42962db683dff126cc95b + checksum: 10/4c34a12fbeb0677822a9e93e81f2027e39e6f27557c17bc1e5ff76debbd41e748c3673517561792bda9e276245f89fbfd9b0b24fcec3b33a04ee2196729b3489 languageName: node linkType: hard @@ -18889,8 +19038,8 @@ __metadata: version: 0.0.0-use.local resolution: "grafana@workspace:." dependencies: - "@babel/core": "npm:7.25.7" - "@babel/preset-env": "npm:7.25.7" + "@babel/core": "npm:7.25.8" + "@babel/preset-env": "npm:7.25.8" "@babel/runtime": "npm:7.25.7" "@betterer/betterer": "npm:5.4.0" "@betterer/cli": "npm:5.4.0" @@ -18901,7 +19050,7 @@ __metadata: "@emotion/react": "npm:11.13.3" "@fingerprintjs/fingerprintjs": "npm:^3.4.2" "@floating-ui/react": "npm:0.26.24" - "@formatjs/intl-durationformat": "npm:^0.2.4" + "@formatjs/intl-durationformat": "npm:^0.3.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/aws-sdk": "npm:0.5.0" "@grafana/azure-sdk": "npm:0.0.3" @@ -18909,7 +19058,7 @@ __metadata: "@grafana/e2e-selectors": "workspace:*" "@grafana/eslint-config": "npm:7.0.0" "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules" - "@grafana/experimental": "npm:2.1.1" + "@grafana/experimental": "npm:2.1.2" "@grafana/faro-core": "npm:^1.3.6" "@grafana/faro-web-sdk": "npm:^1.3.6" "@grafana/faro-web-tracing": "npm:^1.8.2" @@ -18922,12 +19071,13 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:5.18.3" + "@grafana/scenes": "npm:5.20.0" + "@grafana/scenes-react": "npm:5.20.0" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" "@grafana/ui": "workspace:*" - "@hello-pangea/dnd": "npm:16.6.0" + "@hello-pangea/dnd": "npm:17.0.0" "@kusto/monaco-kusto": "npm:^10.0.0" "@leeoniya/ufuzzy": "npm:1.0.14" "@lezer/common": "npm:1.2.2" @@ -18950,18 +19100,18 @@ __metadata: "@react-aria/overlays": "npm:3.23.3" "@react-aria/utils": "npm:3.25.3" "@react-awesome-query-builder/ui": "npm:6.6.3" - "@react-types/button": "npm:3.9.6" + "@react-types/button": "npm:3.10.0" "@react-types/menu": "npm:3.9.12" "@react-types/overlays": "npm:3.8.10" - "@react-types/shared": "npm:3.24.1" + "@react-types/shared": "npm:3.25.0" "@reduxjs/toolkit": "npm:2.2.8" "@rtk-query/codegen-openapi": "npm:^1.2.0" "@rtsao/plugin-proposal-class-properties": "npm:7.0.1-patch.1" "@swc/core": "npm:1.4.2" "@swc/helpers": "npm:0.5.13" - "@testing-library/dom": "npm:10.0.0" - "@testing-library/jest-dom": "npm:6.4.2" - "@testing-library/react": "npm:15.0.2" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/react": "npm:16.0.1" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/angular": "npm:1.8.9" @@ -18975,7 +19125,7 @@ __metadata: "@types/d3-scale-chromatic": "npm:3.0.3" "@types/debounce-promise": "npm:3.1.9" "@types/diff": "npm:^5" - "@types/eslint": "npm:8.56.10" + "@types/eslint": "npm:9.6.1" "@types/eslint-scope": "npm:^3.7.7" "@types/file-saver": "npm:2.0.7" "@types/glob": "npm:^8.0.0" @@ -19064,7 +19214,7 @@ __metadata: diff: "npm:^5.1.0" esbuild: "npm:0.24.0" esbuild-loader: "npm:4.2.2" - esbuild-plugin-browserslist: "npm:^0.14.0" + esbuild-plugin-browserslist: "npm:^0.15.0" eslint: "npm:8.57.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.26.0" @@ -19127,7 +19277,7 @@ __metadata: moment-timezone: "npm:0.5.46" monaco-editor: "npm:0.34.1" moveable: "npm:0.53.0" - msw: "npm:2.4.9" + msw: "npm:2.4.10" mutationobserver-shim: "npm:0.3.7" nanoid: "npm:^5.0.4" ngtemplate-loader: "npm:2.1.0" @@ -19188,7 +19338,7 @@ __metadata: rimraf: "npm:6.0.1" rudder-sdk-js: "npm:2.48.19" rxjs: "npm:7.8.1" - sass: "npm:1.79.3" + sass: "npm:1.79.5" sass-loader: "npm:16.0.2" selecto: "npm:1.26.3" semver: "npm:7.6.3" @@ -21639,7 +21789,7 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.20.0, jiti@npm:^1.21.6": +"jiti@npm:^1.20.0": version: 1.21.6 resolution: "jiti@npm:1.21.6" bin: @@ -21648,6 +21798,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^2.3.3": + version: 2.3.3 + resolution: "jiti@npm:2.3.3" + bin: + jiti: lib/jiti-cli.mjs + checksum: 10/21d1e89d909101c702769537e75ad87b433f980bc6938a6f64a90d1fbe7cb1510a0d4b82d4020b3093b47cebff5568af5e39d883e26aa4413564ced43b8cfd84 + languageName: node + linkType: hard + "jju@npm:^1.4.0": version: 1.4.0 resolution: "jju@npm:1.4.0" @@ -22111,15 +22270,15 @@ __metadata: linkType: hard "knip@npm:^5.10.0": - version: 5.30.6 - resolution: "knip@npm:5.30.6" + version: 5.33.3 + resolution: "knip@npm:5.33.3" dependencies: "@nodelib/fs.walk": "npm:1.2.8" "@snyk/github-codeowners": "npm:1.1.0" easy-table: "npm:1.2.0" enhanced-resolve: "npm:^5.17.1" fast-glob: "npm:^3.3.2" - jiti: "npm:^1.21.6" + jiti: "npm:^2.3.3" js-yaml: "npm:^4.1.0" minimist: "npm:^1.2.8" picocolors: "npm:^1.0.0" @@ -22136,7 +22295,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/42973ec1f5208017c63232dd5a8c29b32accc9bbc00712afbb247fe14b2e8687e27a66a91cf48250cdae30aff6c456d71271c82dda9136785f96520b8c8a049d + checksum: 10/d6227a43666ce9732fe6b797a0c348bc46f59603826766c2049f44682ce96c23f0fb85b659d243dd36f98098a83a4e500d1044f3114a32564a39cf92dce8e853 languageName: node linkType: hard @@ -24148,23 +24307,16 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.2": - version: 2.1.2 - resolution: "ms@npm:2.1.2" - checksum: 10/673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f - languageName: node - linkType: hard - -"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d languageName: node linkType: hard -"msw@npm:2.4.9": - version: 2.4.9 - resolution: "msw@npm:2.4.9" +"msw@npm:2.4.10": + version: 2.4.10 + resolution: "msw@npm:2.4.10" dependencies: "@bundled-es-modules/cookie": "npm:^2.0.0" "@bundled-es-modules/statuses": "npm:^1.0.1" @@ -24190,7 +24342,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/fe00b2d2934993cfb26661ab919944a677d68f097d1e5990a0a4245334741412855abe499dd77822212c586dd8dd002ef8062f0f4f451a6955bd5dccab1905a8 + checksum: 10/f8fb491f91f24a5f17369f4263e99b5609871854422eb6d7c959f782d568fd6229f2dee84e6db72a19e8583073513d9f651517b8cb9a57a1b6849aff1499cba2 languageName: node linkType: hard @@ -24249,7 +24401,7 @@ __metadata: languageName: node linkType: hard -"nano-css@npm:^5.6.1, nano-css@npm:^5.6.2": +"nano-css@npm:^5.3.1, nano-css@npm:^5.6.1, nano-css@npm:^5.6.2": version: 5.6.2 resolution: "nano-css@npm:5.6.2" dependencies: @@ -24357,6 +24509,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^7.0.0": + version: 7.1.1 + resolution: "node-addon-api@npm:7.1.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10/ee1e1ed6284a2f8cd1d59ac6175ecbabf8978dcf570345e9a8095a9d0a2b9ced591074ae77f9009287b00c402352b38aa9322a34f2199cdc9f567b842a636b94 + languageName: node + linkType: hard + "node-dir@npm:^0.1.17": version: 0.1.17 resolution: "node-dir@npm:0.1.17" @@ -28003,6 +28164,31 @@ __metadata: languageName: node linkType: hard +"react-use@npm:17.4.0": + version: 17.4.0 + resolution: "react-use@npm:17.4.0" + dependencies: + "@types/js-cookie": "npm:^2.2.6" + "@xobotyi/scrollbar-width": "npm:^1.9.5" + copy-to-clipboard: "npm:^3.3.1" + fast-deep-equal: "npm:^3.1.3" + fast-shallow-equal: "npm:^1.0.0" + js-cookie: "npm:^2.2.1" + nano-css: "npm:^5.3.1" + react-universal-interface: "npm:^0.6.2" + resize-observer-polyfill: "npm:^1.5.1" + screenfull: "npm:^5.1.0" + set-harmonic-interval: "npm:^1.0.1" + throttle-debounce: "npm:^3.0.1" + ts-easing: "npm:^0.2.0" + tslib: "npm:^2.1.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/98566c4817b00251107824743ea9dff41f167b548bd5f249f6eb9e2ec09388a2de1e89988e4432cead3f8aa83cf706e0255db8a20c0615768c670751973d2761 + languageName: node + linkType: hard + "react-use@npm:17.5.0": version: 17.5.0 resolution: "react-use@npm:17.5.0" @@ -28947,25 +29133,25 @@ __metadata: linkType: hard "rollup@npm:^4.22.4": - version: 4.22.5 - resolution: "rollup@npm:4.22.5" + version: 4.24.0 + resolution: "rollup@npm:4.24.0" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.22.5" - "@rollup/rollup-android-arm64": "npm:4.22.5" - "@rollup/rollup-darwin-arm64": "npm:4.22.5" - "@rollup/rollup-darwin-x64": "npm:4.22.5" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.22.5" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.22.5" - "@rollup/rollup-linux-arm64-gnu": "npm:4.22.5" - "@rollup/rollup-linux-arm64-musl": "npm:4.22.5" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.22.5" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.22.5" - "@rollup/rollup-linux-s390x-gnu": "npm:4.22.5" - "@rollup/rollup-linux-x64-gnu": "npm:4.22.5" - "@rollup/rollup-linux-x64-musl": "npm:4.22.5" - "@rollup/rollup-win32-arm64-msvc": "npm:4.22.5" - "@rollup/rollup-win32-ia32-msvc": "npm:4.22.5" - "@rollup/rollup-win32-x64-msvc": "npm:4.22.5" + "@rollup/rollup-android-arm-eabi": "npm:4.24.0" + "@rollup/rollup-android-arm64": "npm:4.24.0" + "@rollup/rollup-darwin-arm64": "npm:4.24.0" + "@rollup/rollup-darwin-x64": "npm:4.24.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.24.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.24.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.24.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.24.0" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.24.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.24.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.24.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.24.0" + "@rollup/rollup-linux-x64-musl": "npm:4.24.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.24.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.24.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.24.0" "@types/estree": "npm:1.0.6" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -29005,7 +29191,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/f34812fa982442ab71410b649630c24434b2dc02485e543607734766eb7211ce7e0a79102f27210f337af00f3617006adebb4f87fb2e9d24cac7100d0e599352 + checksum: 10/291dce8f180628a73d6749119a3e50aa917c416075302bc6f6ac655affc7f0ce9d7f025bef7318d424d0c5623dcb83e360f9ea0125273b6a2285c232172800cc languageName: node linkType: hard @@ -29155,16 +29341,17 @@ __metadata: languageName: node linkType: hard -"sass@npm:1.79.3": - version: 1.79.3 - resolution: "sass@npm:1.79.3" +"sass@npm:1.79.5": + version: 1.79.5 + resolution: "sass@npm:1.79.5" dependencies: + "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" immutable: "npm:^4.0.0" source-map-js: "npm:>=0.6.2 <2.0.0" bin: sass: sass.js - checksum: 10/9b83e91c44a4c5d738ded27fcd2c88260f5f407e49c6aab92f75a1f768831182a8f4f89f30e2599b74447451d6536ea6f3a55cac34c5ec00b45983d493732e5d + checksum: 10/86f6ebe9c3c8d86cf8fff2e12f93109697140090604fb6180c8792c0e4fc98377c433dbd4c86a0272c47ba4c307edf24435a06d7e5995956048a23f9e187fedc languageName: node linkType: hard