diff --git a/.drone.yml b/.drone.yml index d3fb536d1e0..d010b7598b2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -942,8 +942,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -1383,8 +1384,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -2455,8 +2457,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -3039,8 +3042,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -4978,8 +4982,9 @@ services: path: /var/lib/mysql - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled + -alertmanager.utf8-strict-mode-enabled environment: {} - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -5451,7 +5456,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM python:3.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM postgres:12.3-alpine - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/mimir-alpine:r304-3872ccb + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/mimir-alpine:r316-55f47f8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:5.7.39 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:8.0.32 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM redis:6.2.11-alpine @@ -5489,7 +5494,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack - trivy --exit-code 1 --severity HIGH,CRITICAL python:3.8 - trivy --exit-code 1 --severity HIGH,CRITICAL postgres:12.3-alpine - - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/mimir-alpine:r304-3872ccb + - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/mimir-alpine:r316-55f47f8 - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:5.7.39 - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:8.0.32 - trivy --exit-code 1 --severity HIGH,CRITICAL redis:6.2.11-alpine @@ -5735,6 +5740,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 250d3c2ae9ddf7cbfd2a9fed01fe81312a882ba09b8f8725f4704629da00a774 +hmac: bb28bcd274c9d2ce724db12659ed6e3dcc461f8a07ae0eb9385a64ca5daad4de ... diff --git a/.yarn/patches/history-npm-4.10.1-ee217563ae.patch b/.yarn/patches/history-npm-4.10.1-ee217563ae.patch deleted file mode 100644 index 6cd9f7f4b42..00000000000 --- a/.yarn/patches/history-npm-4.10.1-ee217563ae.patch +++ /dev/null @@ -1,63 +0,0 @@ -diff --git a/cjs/history.js b/cjs/history.js -index fcd8ebab613c6d87b9ac824feb30ab1080cf0ef2..4df20d5cb2f9ba5fc8777899aada53f49399560b 100644 ---- a/cjs/history.js -+++ b/cjs/history.js -@@ -103,16 +103,6 @@ function createLocation(path, state, key, currentLocation) { - if (state !== undefined && location.state === undefined) location.state = state; - } - -- try { -- location.pathname = decodeURI(location.pathname); -- } catch (e) { -- if (e instanceof URIError) { -- throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.'); -- } else { -- throw e; -- } -- } -- - if (key) location.key = key; - - if (currentLocation) { -diff --git a/esm/history.js b/esm/history.js -index df67820fe3eed558c44fca07a82b0cd409d46720..e0e0d4f69a407e8de782b3fdf8297d42708e110a 100644 ---- a/esm/history.js -+++ b/esm/history.js -@@ -80,16 +80,6 @@ function createLocation(path, state, key, currentLocation) { - if (state !== undefined && location.state === undefined) location.state = state; - } - -- try { -- location.pathname = decodeURI(location.pathname); -- } catch (e) { -- if (e instanceof URIError) { -- throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.'); -- } else { -- throw e; -- } -- } -- - if (key) location.key = key; - - if (currentLocation) { -diff --git a/umd/history.js b/umd/history.js -index 80e4ff66c44a2a71d4f842cc05a252e48dd18e9a..f8f4901be95e48c66f5626fbf051747a2ffbe41d 100644 ---- a/umd/history.js -+++ b/umd/history.js -@@ -207,16 +207,6 @@ - if (state !== undefined && location.state === undefined) location.state = state; - } - -- try { -- location.pathname = decodeURI(location.pathname); -- } catch (e) { -- if (e instanceof URIError) { -- throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.'); -- } else { -- throw e; -- } -- } -- - if (key) location.key = key; - - if (currentLocation) { diff --git a/CHANGELOG.md b/CHANGELOG.md index e5206c9da37..af808ed98a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ + + +# 11.3.0+security-01 (2024-11-12) + +### Bug fixes + +- **MigrationAssistant:** Fix Migration Assistant issue [CVE-2024-9476] + + + + +# 11.2.3+security-01 (2024-11-12) + +- **MigrationAssistant:** Fix Migration Assistant issue [CVE-2024-9476] + + # 10.4.12 (2024-11-08) diff --git a/devenv/docker/blocks/mimir_backend/docker-compose.yaml b/devenv/docker/blocks/mimir_backend/docker-compose.yaml index a09635e80e3..544b836e1dd 100644 --- a/devenv/docker/blocks/mimir_backend/docker-compose.yaml +++ b/devenv/docker/blocks/mimir_backend/docker-compose.yaml @@ -1,9 +1,10 @@ mimir_backend: - image: grafana/mimir-alpine:r304-3872ccb + image: grafana/mimir-alpine:r316-55f47f8 container_name: mimir_backend command: - -target=backend - -alertmanager.grafana-alertmanager-compatibility-enabled + - -alertmanager.utf8-strict-mode-enabled nginx: environment: - NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx diff --git a/docs/sources/datasources/postgres/_index.md b/docs/sources/datasources/postgres/_index.md index 223c47868ed..3489b760f70 100644 --- a/docs/sources/datasources/postgres/_index.md +++ b/docs/sources/datasources/postgres/_index.md @@ -428,9 +428,7 @@ SELECT hostname FROM my_host WHERE hostname LIKE '$__searchFilter' ### Using Variables in Queries -From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically. If your template variables are strings, do not wrap them in quotes in where clauses. - -From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`. +Template variable values are only quoted when the template variable is a `multi-value`. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index 0131e1688ae..5e4cdaab205 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -43,7 +43,7 @@ With all of these dynamic elements, there's almost no limit to what a canvas can We'd love your feedback on the canvas visualization. Please check out the [open Github issues](https://github.com/grafana/grafana/issues?page=1&q=is%3Aopen+is%3Aissue+label%3Aarea%2Fpanel%2Fcanvas) and [submit a new feature request](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request,area%2Fpanel%2Fcanvas&title=Canvas:&projects=grafana-dataviz&template=1-feature_requests.md) as needed. {{< /admonition >}} -## Configure a bar gauge visualization +## Configure a canvas visualization The following video shows you how to create and configure a canvas visualization: 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 80e8e20257d..61dee43370e 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -222,6 +222,7 @@ Experimental features might be changed or removed without prior notice. | `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode | | `zipkinBackendMigration` | Enables querying Zipkin data source without the proxy | | `enableSCIM` | Enables SCIM support for user and group management | +| `crashDetection` | Enables browser crash detection reporting to Faro. | ## Development feature toggles diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md index 56c8eac59f9..fe98f938a20 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md @@ -17,12 +17,14 @@ weight: 200 Grafana provides many ways to authenticate users. Some authentication integrations also enable syncing user permissions and org memberships. -The following table shows all supported authentication providers and the features available for them. [Team sync]({{< relref "../configure-team-sync" >}}) and [active sync]({{< relref "./enhanced-ldap#active-ldap-synchronization" >}}) are only available in Grafana Enterprise. +The following table shows all supported authentication methods and the features available for them. [Team sync]({{< relref "../configure-team-sync" >}}) and [active sync]({{< relref "./enhanced-ldap#active-ldap-synchronization" >}}) are only available in Grafana Enterprise. -| Provider | Multi Org Mapping | Enforce Sync | Role Mapping | Grafana Admin Mapping | Team Sync | Allowed groups | Active Sync | Skip OrgRole mapping | Auto Login | Single Logout | +| Authentication method | Multi Org Mapping | Enforce Sync | Role Mapping | Grafana Admin Mapping | Team Sync | Allowed groups | Active Sync | Skip OrgRole mapping | Auto Login | Single Logout | | :---------------------------------------------------- | :---------------- | :----------- | :----------- | :-------------------- | :-------- | :------------- | :---------- | :------------------- | :--------- | :------------ | +| [Anonymous access]({{< relref "./anonymous-auth" >}}) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | | [Auth Proxy]({{< relref "./auth-proxy" >}}) | no | yes | yes | no | yes | no | N/A | no | N/A | N/A | | [Azure AD OAuth]({{< relref "./azuread" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [Basic auth]({{< relref "./grafana" >}}) | yes | N/A | yes | yes | N/A | N/A | N/A | N/A | N/A | N/A | | [Generic OAuth]({{< relref "./generic-oauth" >}}) | yes | yes | yes | yes | yes | no | N/A | yes | yes | yes | | [GitHub OAuth]({{< relref "./github" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | | [GitLab OAuth]({{< relref "./gitlab" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | @@ -33,17 +35,9 @@ The following table shows all supported authentication providers and the feature | [LDAP]({{< relref "./ldap" >}}) | yes | yes | yes | yes | yes | yes | yes | no | N/A | N/A | | [JWT Proxy]({{< relref "./jwt" >}}) | no | yes | yes | yes | no | no | N/A | no | N/A | N/A | -N/A = Not applicable +Fields explanation: -## Auth Proxy - -| Feature | Supported? | -| :---------------- | :--------- | -| Multi Org Mapping | no | -| Enforce Sync | N/A | -| Role Mapping | yes | - -**Multi Org Mapping:** Able to add a user and role map him to multiple orgs +**Multi Org Mapping:** Able to add a user and map roles to multiple organizations **Enforce Sync:** If the information provided by the identity provider is empty, does the integration skip setting that user’s field or does it enforce a default. @@ -82,15 +76,9 @@ If users want to use the same email address with multiple identity providers (fo To enable this option, refer to the [Enable email lookup](#enable-email-lookup) section. -## Grafana Auth +## Login and short-lived tokens -Grafana of course has a built in user authentication system with password authentication enabled by default. You can -disable authentication by enabling anonymous access. You can also hide the login form and only allow login through an auth -provider (listed above). There are also options for allowing self sign up. - -### Login and short-lived tokens - -> The following applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration. +> The following applies when using Grafana's basic authentication, LDAP (without Auth proxy) or OAuth integration. Grafana uses short-lived tokens as a mechanism for verifying authenticated users. These short-lived tokens are rotated on an interval specified by `token_rotation_interval_minutes` for active authenticated users. @@ -99,11 +87,6 @@ Inactive authenticated users will remain logged in for a duration specified by ` This means that a user can close a Grafana window and return before `now + login_maximum_inactive_lifetime_duration` to continue their session. This is true as long as the time since last user login is less than `login_maximum_lifetime_duration`. -#### Remote logout - -You can logout from other devices by removing login sessions from the bottom of your profile page. If you are -a Grafana admin user, you can also do the same for any user from the Server Admin / Edit User view. - ## Settings Example: @@ -130,50 +113,7 @@ api_key_max_seconds_to_live = -1 oauth_allow_insecure_email_lookup = false ``` -### Anonymous authentication - -You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. - -Example: - -```bash -[auth.anonymous] -enabled = true - -{{< admonition type="note" >}} -Enabling anonymous access is a disallowed configuration setting on Hosted Grafana and not recommended due [security implications](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/#implications-of-enabling-anonymous-access-to-dashboards). -For sharing dashboards with a wider audience, consider using the [public dashboard feature](https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/) instead. -{{< /admonition >}} - -# Organization name that should be used for unauthenticated users -org_name = Main Org. - -# Role for unauthenticated users, other valid values are `Editor` and `Admin` -org_role = Viewer -``` - -If you change your organization name in the Grafana UI, this setting needs to be updated to match the new name. - -### Basic authentication - -Basic auth is enabled by default and works with the built-in Grafana user-password authentication system and LDAP -authentication integration. - -To disable basic auth: - -```bash -[auth.basic] -enabled = false -``` - -### Disable login form - -Hide the Grafana login form using the below configuration settings. - -```bash -[auth] -disable_login_form = true -``` +## Extended authentication settings ### Enable email lookup @@ -244,6 +184,11 @@ Example for Generic OAuth: signout_redirect_url = ``` +### Remote logout + +You can log out from other devices by removing login sessions from the bottom of your profile page. If you are +a Grafana admin user, you can also do the same for any user from the Server Admin / Edit User view. + ### Protected roles {{% admonition type="note" %}} diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/anonymous-auth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/anonymous-auth/index.md new file mode 100644 index 00000000000..2d33683a7de --- /dev/null +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/anonymous-auth/index.md @@ -0,0 +1,61 @@ +--- +aliases: + - ../../../auth/anonymous-auth/ +description: Learn how to configure anonymous access in Grafana +labels: + products: + - enterprise + - oss +menuTitle: Anonymous access +title: Configure anonymous access +weight: 250 +--- + +# Anonymous authentication + +You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. + +{{< admonition type="note" >}} +Anonymous users are charged as active users in Grafana Enterprise +{{< /admonition >}} + +## Anonymous devices + +The anonymous devices feature enhances the management and monitoring of anonymous access within your Grafana instance. This feature is part of ongoing efforts to provide more control and transparency over anonymous usage. + +Users can now view anonymous usage statistics, including the count of devices and users over the last 30 days. + +- Go to **Administration -> Users** to access the anonymous devices tab. +- A new stat for the usage stats page -> Usage & Stats page shows the active anonymous devices last 30 days. + +The number of anonymous devices is not limited by default. The configuration option `device_limit` allows you to enforce a limit on the number of anonymous devices. This enables you to have greater control over the usage within your Grafana instance and keep the usage within the limits of your environment. Once the limit is reached, any new devices that try to access Grafana will be denied access. + +To display anonymous users and devices for versions 10.2, 10.3, 10.4, you need to enable the feature toggle `displayAnonymousStats` + +```bash +[feature_toggles] +enable = displayAnonymousStats +``` + +## Configuration + +Example: + +```bash +[auth.anonymous] +enabled = true + +# Organization name that should be used for unauthenticated users +org_name = Main Org. + +# Role for unauthenticated users, other valid values are `Editor` and `Admin` +org_role = Viewer + +# Hide the Grafana version text from the footer and help tooltip for unauthenticated users (default: false) +hide_version = true + +# Setting this limits the number of anonymous devices in your instance. Any new anonymous devices added after the limit has been reached will be denied access. +device_limit = +``` + +If you change your organization name in the Grafana UI this setting needs to be updated to match the new name. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index b9ecc19f0e1..532d992b665 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -461,7 +461,7 @@ The following table outlines the various Azure AD/Entra ID configuration options | `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | | | `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | | `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `openid email profile` | -| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Azure AD/Entra ID login. Only existing Grafana users can log in with generic OAuth if set to `false`. | `true` | +| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Azure AD/Entra ID login. Only existing Grafana users can log in with Azure AD/Entra ID if set to `false`. | `true` | | `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | | `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Map roles]({{< relref "#map-roles" >}}). | `false` | | `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | @@ -470,7 +470,7 @@ The following table outlines the various Azure AD/Entra ID configuration options | `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | | `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Azure AD/Entra ID to include the `groups` claim following [Configure group membership claims on the Azure Portal]({{< relref "#configure-group-membership-claims-on-the-azure-portal" >}}). | | | `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | | -| `allowed_domains` | No | Yes | List comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | | `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | | `tls_client_cert` | No | No | The path to the certificate. | | | `tls_client_key` | No | No | The path to the key. | | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index 5b038280047..177e24dbebd 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -382,7 +382,7 @@ The following table outlines the various generic OAuth2 configuration options. Y | `auth_style` | No | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | | `scopes` | No | List of comma- or space-separated OAuth2 scopes. | `user:email` | | `empty_scopes` | No | Set to `true` to use an empty scope during authentication. | `false` | -| `allow_sign_up` | No | Controls Grafana user creation through the generic OAuth2 login. Only existing Grafana users can log in with generic OAuth if set to `false`. | `true` | +| `allow_sign_up` | No | Controls Grafana user creation through the generic OAuth2 login. Only existing Grafana users can log in with Generic OAuth if set to `false`. | `true` | | `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | | `id_token_attribute_name` | No | The name of the key used to extract the ID token from the returned OAuth2 token. | `id_token` | | `login_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user login lookup from the user ID token. For more information on how user login is retrieved, refer to [Configure login]({{< relref "#configure-login" >}}). | | @@ -398,7 +398,7 @@ The following table outlines the various generic OAuth2 configuration options. Y | `groups_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user group lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no groups are found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a string array of groups. | | | `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | | | `allowed_organizations` | No | List of comma- or space-separated organizations. The user should be a member of at least one organization to log in. | | -| `allowed_domains` | No | List comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `allowed_domains` | No | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | | `team_ids` | No | String list of team IDs. If set, the user must be a member of one of the given teams to log in. If you configure `team_ids`, you must also configure `teams_url` and `team_ids_attribute_path`. | | | `team_ids_attribute_path` | No | The [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana team ID lookup within the results returned by the `teams_url` endpoint. | | | `teams_url` | No | The URL used to query for team IDs. If not set, the default value is `/teams`. If you configure `teams_url`, you must also configure `team_ids_attribute_path`. | | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index 338b95a7644..f80bd7a9703 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -7,14 +7,14 @@ labels: - cloud - enterprise - oss -menuTitle: Google OAuth2 -title: Configure Google OAuth2 authentication +menuTitle: Google OAuth +title: Configure Google OAuth authentication weight: 1100 --- -# Configure Google OAuth2 authentication +# Configure Google OAuth authentication -To enable Google OAuth2 you must register your application with Google. Google will generate a client ID and secret key for you to use. +To enable Google OAuth you must register your application with Google. Google will generate a client ID and secret key for you to use. {{% admonition type="note" %}} If Users use the same email address in Google that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information. @@ -25,13 +25,21 @@ If Users use the same email address in Google that they use with other authentic First, you need to create a Google OAuth Client: 1. Go to https://console.developers.google.com/apis/credentials. +1. Create a new project if you don't have one already. + 1. Enter a project name. The **Organization** and **Location** fields should both be set to your organization's information. + 1. In **OAuth consent screen** select the **External** User Type. Click **CREATE**. + 1. Fill out the requested information using the URL of your Grafana Cloud instance. + 1. Accept the defaults, or customize the consent screen options. 1. Click **Create Credentials**, then click **OAuth Client ID** in the drop-down menu 1. Enter the following: - - Application Type: Web Application - - Name: Grafana - - Authorized JavaScript Origins: https://grafana.mycompany.com - - Authorized Redirect URLs: https://grafana.mycompany.com/login/google - - Replace https://grafana.mycompany.com with the URL of your Grafana instance. + - **Application Type**: Web application + - **Name**: Grafana + - **Authorized JavaScript origins**: `https://` + - **Authorized redirect URIs**: `https:///login/google` + - Replace `` with the URL of your Grafana instance. + {{< admonition type="note" >}} + The URL you enter is the one for your Grafana instance home page, not your Grafana Cloud portal URL. + {{< /admonition >}} 1. Click Create 1. Copy the Client ID and Client Secret from the 'OAuth Client' modal @@ -41,7 +49,7 @@ First, you need to create a Google OAuth Client: Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle. {{% /admonition %}} -As a Grafana Admin, you can configure Google OAuth2 client from within Grafana using the Google UI. To do this, navigate to **Administration > Authentication > Google** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. +As a Grafana Admin, you can configure Google OAuth client from within Grafana using the Google UI. To do this, navigate to **Administration > Authentication > Google** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. After you have filled in the form, click **Save**. If the save was successful, Grafana will apply the new configurations. @@ -130,7 +138,9 @@ introduces "proof key for code exchange" (PKCE) which provides additional protection against some forms of authorization code interception attacks. PKCE will be required in [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-03). -> You can disable PKCE in Grafana by setting `use_pkce` to `false` in the`[auth.google]` section. +{{% admonition type="note" %}} +You can disable PKCE in Grafana by setting `use_pkce` to `false` in the`[auth.google]` section. +{{% /admonition %}} #### Configure refresh token @@ -183,7 +193,9 @@ to a comma or space separated list of groups. Google groups are referenced by the group email key. For example, `developers@google.com`. -> Note: Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups +{{% admonition type="note" %}} +Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups. +{{% /admonition %}} #### Configure role mapping @@ -196,8 +208,9 @@ If no valid role is found, the user is assigned the role specified by [the `auto You can disable this default role assignment by setting `role_attribute_strict = true`. This setting denies user access if no role or an invalid role is returned after evaluating the `role_attribute_path` and the `org_mapping` expressions. To ease configuration of a proper JMESPath expression, go to [JMESPath](http://jmespath.org/) to test and evaluate expressions with custom payloads. - -> By default skip_org_role_sync is enabled. skip_org_role_sync will default to false in Grafana v10.3.0 and later versions. +{{% admonition type="note" %}} +By default skip_org_role_sync is enabled. skip_org_role_sync will default to false in Grafana v10.3.0 and later versions. +{{% /admonition %}} ##### Role mapping examples @@ -237,7 +250,9 @@ role_attribute_path = contains(groups[*], 'example-group@google.com') && 'Editor skip_org_role_sync = false ``` -> Note: Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups +{{% admonition type="note" %}} +Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups. +{{% /admonition %}} ###### Map server administrator role @@ -258,3 +273,39 @@ In this example, all users will be assigned `Viewer` role regardless of the user role_attribute_path = "'Viewer'" skip_org_role_sync = false ``` + +## Configuration options + +The following table outlines the various Google OAuth configuration options. You can apply these options as environment variables, similar to any other configuration within Grafana. For more information, refer to [Override configuration with environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}). + +| Setting | Required | Supported on Cloud | Description | Default | +| ---------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | +| `enabled` | No | Yes | Enables Google authentication. | `false` | +| `name` | No | Yes | Name that refers to the Google authentication from the Grafana user interface. | `Google` | +| `icon` | No | Yes | Icon used for the Google authentication in the Grafana user interface. | `google` | +| `client_id` | Yes | Yes | Client ID of the App. | | +| `client_secret` | Yes | Yes | Client secret of the App. | | +| `auth_url` | Yes | Yes | Authorization endpoint of the Google OAuth provider. | `https://accounts.google.com/o/oauth2/v2/auth` | +| `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | `https://oauth2.googleapis.com/token` | +| `api_url` | Yes | Yes | Endpoint used to obtain user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://openidconnect.googleapis.com/v1/userinfo` | +| `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | +| `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `openid email profile` | +| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Google login. Only existing Grafana users can log in with Google if set to `false`. | `true` | +| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | +| `hosted_domain` | No | Yes | Specifies the domain to restrict access to users from that domain. This value is appended to the authorization request using the `hd` parameter. | | +| `validate_hd` | No | Yes | Set to `false` to disable the validation of the `hd` parameter from the Google ID token. For more informatiion, refer to [Enable Google OAuth in Grafana]({{< relref "#enable-google-oauth-in-grafana" >}}). | `true` | +| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` | +| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | +| `org_mapping` | No | No | List of comma- or space-separated `::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | +| `allow_assign_grafana_admin` | No | No | Set to `true` to automatically sync the Grafana server administrator role. When enabled, if the Google user's App role is `GrafanaAdmin`, Grafana grants the user server administrator privileges and the organization administrator role. If disabled, the user will only receive the organization administrator role. For more details on user role mapping, refer to [Map roles]({{< relref "#map-roles" >}}). | `false` | +| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | +| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Google to include the `groups` claim following [Configure allowed groups]({{< relref "#configure-allowed-groups" >}}). | | +| `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | | +| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | +| `tls_client_cert` | No | No | The path to the certificate. | | +| `tls_client_key` | No | No | The path to the key. | | +| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | | +| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | +| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `promp=consent` and `access_type=offline` parameters to the authorization request. | `true` | +| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md index 3d74d1bd6fe..c0d97fcbb16 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md @@ -1,127 +1,32 @@ --- aliases: - ../../../auth/grafana/ -description: Grafana OAuthentication Guide +description: Learn how to configure basic authentication in Grafana labels: products: - enterprise - oss menuTitle: Basic auth -title: Configure Grafana authentication +title: Configure basic authentication weight: 200 --- -## Configure Grafana authentication +# Configure basic authentication -Grafana of course has a built in user authentication system with password authentication enabled by default. You can -disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth -provider (listed above). There is also options for allowing self sign up. +Grafana provides a basic authentication system with password authentication enabled by default. This document details configuration options to manage and enhance basic authentication. -### Login and short-lived tokens +## Disable basic authentication -> The following applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration. - -Grafana uses short-lived tokens as a mechanism for verifying authenticated users. -These short-lived tokens are rotated each `token_rotation_interval_minutes` for an active authenticated user. - -An active authenticated user that gets it token rotated will extend the `login_maximum_inactive_lifetime_duration` time from "now" that Grafana will remember the user. -This means that a user can close its browser and come back before `now + login_maximum_inactive_lifetime_duration` and still being authenticated. -This is true as long as the time since user login is less than `login_maximum_lifetime_duration`. - -#### Remote logout - -You can logout from other devices by removing login sessions from the bottom of your profile page. If you are -a Grafana admin user you can also do the same for any user from the Server Admin / Edit User view. - -## Settings - -Example: - -```bash -[auth] - -# Login cookie name -login_cookie_name = grafana_session - -# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days. -login_maximum_inactive_lifetime_duration = 7d - -# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days. -login_maximum_lifetime_duration = 30d - -# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. -token_rotation_interval_minutes = 10 - -# The maximum lifetime (seconds) an api key can be used. If it is set all the api keys should have limited lifetime that is lower than this value. -api_key_max_seconds_to_live = -1 -``` - -### Anonymous authentication - -You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. For more information, refer to [Anonymous authentication]({{< relref "../../configure-authentication#anonymous-authentication" >}}). - -#### Anonymous devices - -The anonymous devices feature enhances the management and monitoring of anonymous access within your Grafana instance. This feature is part of ongoing efforts to provide more control and transparency over anonymous usage. - -Users can now view anonymous usage statistics, including the count of devices and users over the last 30 days. - -- Go to **Administration -> Users** to access the anonymous devices tab. -- A new stat for the usage stats page -> Usage & Stats page shows the active anonymous devices last 30 days. - -The number of anonymous devices is not limited by default. The configuration option `device_limit` allows you to enforce a limit on the number of anonymous devices. This enables you to have greater control over the usage within your Grafana instance and keep the usage within the limits of your environment. Once the limit is reached, any new devices that try to access Grafana will be denied access. - -To display anonymous users and devices for versions 10.2, 10.3, 10.4, you need to enable the feature toggle `displayAnonymousStats` - -```bash -[feature_toggles] -enable = displayAnonymousStats -``` - -#### Anonymous users - -{{< admonition type="note" >}} -Anonymous users are charged as active users in Grafana Enterprise -{{< /admonition >}} - -#### Configuration - -Example: - -```bash -[auth.anonymous] -enabled = true - -# Organization name that should be used for unauthenticated users -org_name = Main Org. - -# Role for unauthenticated users, other valid values are `Editor` and `Admin` -org_role = Viewer - -# Hide the Grafana version text from the footer and help tooltip for unauthenticated users (default: false) -hide_version = true - -# Setting this limits the number of anonymous devices in your instance. Any new anonymous devices added after the limit has been reached will be denied access. -device_limit = -``` - -If you change your organization name in the Grafana UI this setting needs to be updated to match the new name. - -### Basic authentication - -Basic auth is enabled by default and works with the built in Grafana user password authentication system and LDAP -authentication integration. - -To disable basic auth: +To disable basic authentication, use the following configuration: ```bash [auth.basic] enabled = false ``` -### Strong password policy +## Password policy -By default, the password policy for all basic auth users is set to a minimum of four characters. You can enable a stronger password policy with the `password_policy` configuration option. +By default, Grafana’s password policy requires a minimum of four characters for basic auth users. For a stronger password policy, enable the `password_policy` configuration option. With the `password_policy` option enabled, new and updated passwords must meet the following criteria: @@ -137,63 +42,16 @@ password_policy = true ``` {{% admonition type="note" %}} -Existing passwords that don't comply with the new password policy will not be impacted until the user updates their password. +Existing passwords that do not comply with the new password policy will not be affected until the user updates their password. {{% /admonition %}} -### Disable login form +## Disable login form -You can hide the Grafana login form using the below configuration settings. +To hide the Grafana login form, use the following configuration setting: ```bash [auth] disable_login_form = true ``` -### Automatic OAuth login - -Set to true to attempt login with specific OAuth provider automatically, skipping the login screen. -This setting is ignored if multiple auth providers are configured to use auto login. -Defaults to `false`. - -```bash -[auth.generic_oauth] -auto_login = true -``` - -### Hide sign-out menu - -Set the option detailed below to true to hide sign-out menu link. Useful if you use an auth proxy or JWT authentication. - -```bash -[auth] -disable_signout_menu = true -``` - -### URL redirect after signing out - -The URL to redirect the user to after signing out from Grafana can be configured under `[auth]` or under a specific OAuth provider section (for example, `[auth.generic_oauth]`). The URL configured under a specific OAuth provider section takes precedence over the URL configured in `[auth]` section. This can, for example, enable signout from the OAuth provider. - -```bash -[auth.generic_oauth] -signout_redirect_url = - -[auth] -signout_redirect_url = -``` - -### Protected roles - -{{% admonition type="note" %}} -Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}) and [Grafana Cloud]({{< relref "../../../../introduction/grafana-cloud" >}}). -{{% /admonition %}} - -By default, after you configure an authorization provider, Grafana will adopt existing users into the new authentication scheme. For example, if you have created a user with basic authentication having the login `jsmith@example.com`, then set up SAML authentication where `jsmith@example.com` is an account, the user's authentication type will be changed to SAML if they perform a SAML sign-in. - -You can disable this user adoption for certain roles using the `protected_roles` property: - -```bash -[auth.security] -protected_roles = server_admins org_admins -``` - -The value of `protected_roles` should be a list of roles to protect, separated by spaces. Valid roles are `viewers`, `editors`, `org_admins`, `server_admins`, and `all` (a superset of the other roles). +This can be helpful in setups where authentication is handled entirely through external mechanisms or single sign-on (SSO). diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index 09ad9c98fb6..dba1580f535 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -281,6 +281,6 @@ The following table outlines the various Okta OIDC configuration options. You ca | `org_mapping` | No | List of comma- or space-separated `::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | | `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | | `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. | | -| `allowed_domains` | No | List comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `allowed_domains` | No | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | | `use_pkce` | No | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | | `use_refresh_token` | No | Set to `true` to use refresh token and check access token expiration. | `false` | 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 dca76450dc2..08e01f5dcc2 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 @@ -284,68 +284,31 @@ Grafana supports user authentication through Okta, which is useful when you want 1. Log in to the [Okta portal](https://login.okta.com/). 1. Go to the Admin Console in your Okta organization by clicking **Admin** in the upper-right corner. If you are in the Developer Console, then click **Developer Console** in the upper-left corner and then click **Classic UI** to switch over to the Admin Console. 1. In the Admin Console, navigate to **Applications** > **Applications**. -1. Click **Add Application**. -1. Click **Create New App** to start the Application Integration Wizard. -1. Choose **Web** as a platform. -1. Select **SAML 2.0** in the Sign on method section. +1. Click **Create App Integration** to start the Application Integration Wizard. +1. Choose **SAML 2.0** as the **Sign-in method**. 1. Click **Create**. 1. On the **General Settings** tab, enter a name for your Grafana integration. You can also upload a logo. 1. On the **Configure SAML** tab, enter the SAML information related to your Grafana instance: - In the **Single sign on URL** field, use the `/saml/acs` endpoint URL of your Grafana instance, for example, `https://grafana.example.com/saml/acs`. - - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, for example, `https://grafana.example.com/saml/metadata`. + - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, by default it is the `/saml/metadata` endpoint of your Grafana instance (for example `https://example.grafana.com/saml/metadata`). This could be configured differently, but the value here must match the `entity_id` setting of the SAML settings of Grafana. - Leave the default values for **Name ID format** and **Application username**. + {{% admonition type="note" %}} + If you plan to enable SAML Single Logout, consider setting the **Name ID format** to `EmailAddress` or `Persistent`. This must match the `name_id_format` setting of the Grafana instance. + {{% /admonition %}} - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana. The attribute names in Okta need to match exactly what is defined within Grafana, for example: - | Attribute name (in Grafana) | Name and value (in Okta profile) | - | --------------------------- | -------------------------------------------------- | - | Login | Login `user.login` | - | Email | Email `user.email` | - | DisplayName | DisplayName `user.firstName + " " + user.lastName` | + | Attribute name (in Grafana) | Name and value (in Okta profile) | Grafana configuration (under `auth.saml`) | + | --------------------------- | ---------------------------------------------------- | ----------------------------------------- | + | Login | Login - `user.login` | `assertion_attribute_login = Login` | + | Email | Email - `user.email` | `assertion_attribute_email = Email` | + | DisplayName | DisplayName - `user.firstName + " " + user.lastName` | `assertion_attribute_name = DisplayName` | - - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`) and set filter to `Matches regex .*` to return all user groups. + - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`, ensure it matches the `asssertion_attribute_groups` setting in Grafana) and set filter to `Matches regex .*` to return all user groups. 1. Click **Next**. 1. On the final Feedback tab, fill out the form and then click **Finish**. -## Configure SAML authentication in Grafana - -The table below describes all SAML configuration options. Continue reading below for details on specific options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}). - -| Setting | Required | Description | Default | -| ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | -| `enabled` | No | Whether SAML authentication is allowed | `false` | -| `name` | No | Name used to refer to the SAML authentication in the Grafana user interface. | `SAML` | -| `single_logout` | No | Whether SAML Single Logout enabled | `false` | -| `allow_sign_up` | No | Whether to allow new Grafana user creation through SAML login. If set to `false`, then only existing Grafana users can log in with SAML. | `true` | -| `auto_login` | No | Whether SAML auto login is enabled | `false` | -| `allow_idp_initiated` | No | Whether SAML IdP-initiated login is allowed | `false` | -| `certificate` or `certificate_path` | Yes | Base64-encoded string or Path for the SP X.509 certificate | | -| `private_key` or `private_key_path` | Yes | Base64-encoded string or Path for the SP private key | | -| `signature_algorithm` | No | Signature algorithm used for signing requests to the IdP. Supported values are rsa-sha1, rsa-sha256, rsa-sha512. | | -| `idp_metadata`, `idp_metadata_path`, or `idp_metadata_url` | Yes | Base64-encoded string, Path or URL for the IdP SAML metadata XML | | -| `max_issue_delay` | No | Duration, since the IdP issued a response and the SP is allowed to process it | `90s` | -| `metadata_valid_duration` | No | Duration, for how long the SP metadata is valid | `48h` | -| `relay_state` | No | Relay state for IdP-initiated login. Should match relay state configured in IdP | | -| `assertion_attribute_name` | No | Friendly name or name of the attribute within the SAML assertion to use as the user name. Alternatively, this can be a template with variables that match the names of attributes within the SAML assertion. | `displayName` | -| `assertion_attribute_login` | No | Friendly name or name of the attribute within the SAML assertion to use as the user login handle | `mail` | -| `assertion_attribute_email` | No | Friendly name or name of the attribute within the SAML assertion to use as the user email | `mail` | -| `assertion_attribute_groups` | No | Friendly name or name of the attribute within the SAML assertion to use as the user groups | | -| `assertion_attribute_role` | No | Friendly name or name of the attribute within the SAML assertion to use as the user roles | | -| `assertion_attribute_org` | No | Friendly name or name of the attribute within the SAML assertion to use as the user organization | | -| `allowed_organizations` | No | List of comma- or space-separated organizations. User should be a member of at least one organization to log in. | | -| `org_mapping` | No | List of comma- or space-separated Organization:OrgId:Role mappings. Organization can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. | | -| `role_values_none` | No | List of comma- or space-separated roles which will be mapped into the None role | | -| `role_values_viewer` | No | List of comma- or space-separated roles which will be mapped into the Viewer role | | -| `role_values_editor` | No | List of comma- or space-separated roles which will be mapped into the Editor role | | -| `role_values_admin` | No | List of comma- or space-separated roles which will be mapped into the Admin role | | -| `role_values_grafana_admin` | No | List of comma- or space-separated roles which will be mapped into the Grafana Admin (Super Admin) role | | -| `name_id_format` | No | The Name ID Format to request within the SAML assertion | `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | -| `client_id` | No | Client ID of the IdP service application used to retrieve more information about the user from the IdP. | | -| `client_secret` | No | Client secret of the IdP service application used to retrieve more information about the user from the IdP. | | -| `token_url` | No | URL to retrieve the access token from the IdP. | | -| `force_use_graph_api` | No | Whether to use the IdP service application retrieve more information about the user from the IdP. | `false` | - ### Signature algorithm The SAML standard recommends using a digital signature for some types of messages, like authentication or logout requests. If the `signature_algorithm` option is configured, Grafana will put a digital signature into SAML requests. Supported signature types are `rsa-sha1`, `rsa-sha256`, `rsa-sha512`. This option should match your IdP configuration, otherwise, signature validation will fail. Grafana uses key and certificate configured with `private_key` and `certificate` options for signing SAML requests. @@ -358,10 +321,13 @@ By default, this is set to `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` The following list includes valid configuration field values: -- `urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified` -- `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` -- `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` -- `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` +| `name_id_format` value in the configuration file or Terraform | `Name identifier format` on the UI | +| ------------------------------------------------------------- | ---------------------------------- | +| `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | Default | +| `urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified` | Unspecified | +| `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` | Email address | +| `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` | Persistent | +| `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | Transient | ### IdP metadata @@ -721,3 +687,43 @@ cookie_secure = true ``` Ensure cookie_secure is set to true to ensure that cookies are only sent over HTTPS. + +## Configure SAML authentication in Grafana + +The table below describes all SAML configuration options. Continue reading below for details on specific options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../../../configure-grafana#override-configuration-with-environment-variables" >}}). + +| Setting | Required | Description | Default | +| ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | +| `enabled` | No | Whether SAML authentication is allowed. | `false` | +| `name` | No | Name used to refer to the SAML authentication in the Grafana user interface. | `SAML` | +| `entity_id` | No | The entity ID of the service provider. This is the unique identifier of the service provider. | `https://{Grafana URL}/saml/metadata` | +| `single_logout` | No | Whether SAML Single Logout is enabled. | `false` | +| `allow_sign_up` | No | Whether to allow new Grafana user creation through SAML login. If set to `false`, then only existing Grafana users can log in with SAML. | `true` | +| `auto_login` | No | Whether SAML auto login is enabled. | `false` | +| `allow_idp_initiated` | No | Whether SAML IdP-initiated login is allowed. | `false` | +| `certificate` or `certificate_path` | Yes | Base64-encoded string or Path for the SP X.509 certificate. | | +| `private_key` or `private_key_path` | Yes | Base64-encoded string or Path for the SP private key. | | +| `signature_algorithm` | No | Signature algorithm used for signing requests to the IdP. Supported values are rsa-sha1, rsa-sha256, rsa-sha512. | | +| `idp_metadata`, `idp_metadata_path`, or `idp_metadata_url` | Yes | Base64-encoded string, Path or URL for the IdP SAML metadata XML. | | +| `max_issue_delay` | No | Maximum time allowed between the issuance of an AuthnRequest by the SP and the processing of the Response. | `90s` | +| `metadata_valid_duration` | No | Duration for which the SP metadata remains valid. | `48h` | +| `relay_state` | No | Relay state for IdP-initiated login. This should match the relay state configured in the IdP. | | +| `assertion_attribute_name` | No | Friendly name or name of the attribute within the SAML assertion to use as the user name. Alternatively, this can be a template with variables that match the names of attributes within the SAML assertion. | `displayName` | +| `assertion_attribute_login` | No | Friendly name or name of the attribute within the SAML assertion to use as the user login handle. | `mail` | +| `assertion_attribute_email` | No | Friendly name or name of the attribute within the SAML assertion to use as the user email. | `mail` | +| `assertion_attribute_groups` | No | Friendly name or name of the attribute within the SAML assertion to use as the user groups. | | +| `assertion_attribute_role` | No | Friendly name or name of the attribute within the SAML assertion to use as the user roles. | | +| `assertion_attribute_org` | No | Friendly name or name of the attribute within the SAML assertion to use as the user organization | | +| `allowed_organizations` | No | List of comma- or space-separated organizations. User should be a member of at least one organization to log in. | | +| `org_mapping` | No | List of comma- or space-separated Organization:OrgId:Role mappings. Organization can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. | | +| `role_values_none` | No | List of comma- or space-separated roles which will be mapped into the None role. | | +| `role_values_viewer` | No | List of comma- or space-separated roles which will be mapped into the Viewer role. | | +| `role_values_editor` | No | List of comma- or space-separated roles which will be mapped into the Editor role. | | +| `role_values_admin` | No | List of comma- or space-separated roles which will be mapped into the Admin role. | | +| `role_values_grafana_admin` | No | List of comma- or space-separated roles which will be mapped into the Grafana Admin (Super Admin) role. | | +| `skip_org_role_sync` | No | Whether to skip organization role synchronization. | `false` | +| `name_id_format` | No | Specifies the format of the requested NameID element in the SAML AuthnRequest. | `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | +| `client_id` | No | Client ID of the IdP service application used to retrieve more information about the user from the IdP. (Microsoft Entra ID only) | | +| `client_secret` | No | Client secret of the IdP service application used to retrieve more information about the user from the IdP. (Microsoft Entra ID only) | | +| `token_url` | No | URL to retrieve the access token from the IdP. (Microsoft Entra ID only) | | +| `force_use_graph_api` | No | Whether to use the IdP service application retrieve more information about the user from the IdP. (Microsoft Entra ID only) | `false` | diff --git a/docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md b/docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md new file mode 100644 index 00000000000..1377f885397 --- /dev/null +++ b/docs/sources/setup-grafana/configure-security/configure-group-attribute-sync.md @@ -0,0 +1,78 @@ +--- +description: Learn how to use group attribute sync to synchronize between groups in your authentication provider and Grafana RBAC roles. +labels: + products: + - cloud + - enterprise +title: Configure group attribute sync +weight: 1000 +--- + +# Configure group attribute sync + +Group attribute sync allows you to manage user permissions in Grafana based on group membership sourced from the user's identity provider (IdP). +Groups are mapped to [fixed](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control#fixed-roles) and [custom](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control#custom-roles) role-based access control roles in Grafana. + +> **Note:** Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). + +{{% admonition type="note" %}} +This feature is behind the `groupAttributeSync` feature toggle. +You can enable feature toggles through configuration file or environment variables. See configuration [docs](/docs/grafana//setup-grafana/configure-grafana/#feature_toggles) for details. +{{% /admonition %}} + +When a user logs in, Grafana checks the user's external group memberships and the configured group to role mappings to assign the corresponding roles to the user. +If the user's group memberships change or a new mapping is created, the user's role assignments are updated the next time the user logs in. +If a group mapping is removed, the role assignment to users for the group mapping is revoked immediately. + +Role mappings are tied to organizations, so you can have different mappings for different organizations. + +## Create role mappings for a new group + +For information about creating group mappings via the API, refer to [create group mappings reference](https://grafana.com/docs/grafana//developers/http_api/group_attribute_sync#create-group-mappings). + +### Before you begin + +Ensure you have permission to create and update group mappings. By default, the organization administrator role is required to create and edit group mappings. For more information about user permissions, refer to [roles and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions). + +### To create mappings between an external group and RBAC roles + +1. Sign in to Grafana and click **Administration** in the left-side menu. +1. Click **Users and access**. +1. Click **External group sync**. +1. Click **New**. +1. Insert the group identifier for the group that you want to map. +1. Use the role picker to select the roles that you want to map to the group and click **Update**. +1. Click **Save**. + +## Update role mappings for a group + +For information about updating group mappings via the API, refer to [update group mappings reference](https://grafana.com/docs/grafana//developers/http_api/group_attribute_sync#update-group-mappings). + +### Before you begin + +Ensure you have permission to update group mappings. By default, the organization administrator role is required to edit group mappings. For more information about user permissions, refer to [roles and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions). + +### To update role mappings for an external group + +1. Sign in to Grafana and click **Administration** in the left-side menu. +1. Click **Users and access**. +1. Click **External group sync**. +1. Find the group whose mappings you want to update. +1. Click on the role picker corresponding to the group and select the roles that you want to map. +1. Click **Apply**. + +## Remove role mappings for a group + +For information about deleting group mappings via the API, refer to [delete group mappings reference](https://grafana.com/docs/grafana//developers/http_api/group_attribute_sync#delete-group-mappings). + +### Before you begin + +Ensure you have permission to update group mappings. By default, the organization administrator role is required to edit group mappings. For more information about user permissions, refer to [roles and permissions](https://grafana.com/docs/grafana//administration/roles-and-permissions). + +### To remove role mappings for an external group + +1. Sign in to Grafana and click **Administration** in the left-side menu. +1. Click **Users and access**. +1. Click **External group sync**. +1. Find the group whose mappings you want to remove. +1. Click on the trash bin icon corresponding to the group mappings you want to remove. diff --git a/docs/sources/whatsnew/whats-new-in-v11-2.md b/docs/sources/whatsnew/whats-new-in-v11-2.md index 1fe71c25854..921739746b8 100644 --- a/docs/sources/whatsnew/whats-new-in-v11-2.md +++ b/docs/sources/whatsnew/whats-new-in-v11-2.md @@ -277,3 +277,33 @@ _Available in public preview in all editions of Grafana_ [The SSO settings API](https://grafana.com/docs/grafana//developers/http_api/sso-settings/) has been updated to include support for LDAP settings. This feature is experimental behind the feature flag `ssoSettingsLDAP`. You will soon be able to configure LDAP from the UI and Terraform. + +### Reduce number of required fields from the SAML form + + + +_Generally available in Grafana Enterprise and Grafana Cloud Pro and Advanced_ + +The private key and certificate fields are no longer mandatory in the SAML form. To configure SAML without providing a private key and a certificate you have to opt out from using signed requests. + +{{< figure src="/media/docs/grafana/screenshot-grafana-11-2-saml-sign-requests.png" alt="Sign requests in SAML config form" >}} + +### Generate SAML certificate and private key + + + +_Generally available in Grafana Enterprise and Grafana Cloud Pro_ + +You can generate a new certificate and private key for SAML directly from the UI form. Click on the **Generate key and certificate** button from the **Sign requests** tab in the SAML form and then fill in the information you want to be embedded in your generated certificate. + +{{< video-embed src="/media/docs/grafana/screen-recording-11-2-generate-saml-certificate.mp4" >}} + +### OpenID Connect Discovery URL for Generic OAuth + + + +_Generally available in all editions of Grafana_ + +The OpenID Connect Discovery URL is available in the Generic OAuth form. The info extracted from this URL will be used to populate the Auth URL, Token URL and API URL fields. + +{{< video-embed src="/media/docs/grafana/screen-recording-11-2-openid-discovery-url.mp4" >}} diff --git a/package.json b/package.json index b98d85a4cf4..5a422a6f0be 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,9 @@ "releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/" }, "devDependencies": { - "@babel/core": "7.25.9", - "@babel/preset-env": "7.25.9", - "@babel/runtime": "7.25.9", + "@babel/core": "7.26.0", + "@babel/preset-env": "7.26.0", + "@babel/runtime": "7.26.0", "@betterer/betterer": "5.4.0", "@betterer/cli": "5.4.0", "@betterer/eslint": "5.4.0", @@ -95,7 +95,7 @@ "@swc/core": "1.4.2", "@swc/helpers": "0.5.15", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/angular": "1.8.9", @@ -164,7 +164,8 @@ "chrome-remote-interface": "0.33.2", "codeowners": "^5.1.1", "copy-webpack-plugin": "12.0.2", - "core-js": "3.38.1", + "core-js": "3.39.0", + "crashme": "0.0.15", "css-loader": "7.1.2", "css-minimizer-webpack-plugin": "6.0.0", "cypress": "13.10.0", @@ -173,12 +174,12 @@ "esbuild": "0.24.0", "esbuild-loader": "4.2.2", "esbuild-plugin-browserslist": "^0.15.0", - "eslint": "9.12.0", + "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "28.8.3", + "eslint-plugin-jest": "28.9.0", "eslint-plugin-jest-dom": "^5.4.0", - "eslint-plugin-jsdoc": "50.4.3", + "eslint-plugin-jsdoc": "50.5.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-lodash": "8.0.0", "eslint-plugin-no-barrel-files": "^1.1.1", @@ -223,9 +224,9 @@ "react-test-renderer": "18.2.0", "redux-mock-store": "1.5.5", "rimraf": "6.0.1", - "rudder-sdk-js": "2.48.23", + "rudder-sdk-js": "2.48.26", "sass": "1.79.5", - "sass-loader": "16.0.2", + "sass-loader": "16.0.3", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", "stylelint": "16.9.0", @@ -253,7 +254,7 @@ "@emotion/react": "11.13.3", "@fingerprintjs/fingerprintjs": "^3.4.2", "@floating-ui/react": "0.26.27", - "@formatjs/intl-durationformat": "^0.5.0", + "@formatjs/intl-durationformat": "^0.6.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/aws-sdk": "0.5.0", "@grafana/azure-sdk": "0.0.5", @@ -296,7 +297,7 @@ "@react-aria/overlays": "3.23.4", "@react-aria/utils": "3.25.3", "@react-awesome-query-builder/ui": "6.6.3", - "@reduxjs/toolkit": "2.2.8", + "@reduxjs/toolkit": "2.3.0", "@testing-library/react-hooks": "^8.0.1", "@visx/event": "3.3.0", "@visx/gradient": "3.3.0", @@ -380,7 +381,7 @@ "react-router": "5.3.3", "react-router-dom": "5.3.3", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-split-pane": "0.1.92", "react-table": "7.8.0", "react-transition-group": "4.4.5", @@ -426,8 +427,6 @@ "debug@npm:^0.7.4": "2.6.9", "slate-dev-environment@^0.2.2": "patch:slate-dev-environment@npm:0.2.5#.yarn/patches/slate-dev-environment-npm-0.2.5-9aeb7da7b5.patch", "react-split-pane@0.1.92": "patch:react-split-pane@npm:0.1.92#.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch", - "history@4.10.1": "patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch", - "history@^4.9.0": "patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch", "redux": "^5.0.0", "@storybook/blocks@npm:8.1.6": "patch:@storybook/blocks@npm%3A8.1.6#~/.yarn/patches/@storybook-blocks-npm-8.1.6-892f57a6d7.patch", "react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch", diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index c5e5911ce88..59872c2019b 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -728,6 +728,35 @@ describe('calculateField transformer w/ timeseries', () => { }); }); + it('calculates fixed, trailing moving average with missing values', async () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Trailing, + field: 'x', + windowSize: 2, + windowSizeMode: WindowSizeMode.Fixed, + reducer: ReducerID.mean, + }, + }, + }; + + const series = toDataFrame({ + fields: [{ name: 'x', type: FieldType.number, values: [1, undefined, 3, 4, 5] }], + }); + + await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { + const data = received[0][0]; + + //console.log(data.fields); + + expect(data.fields.length).toEqual(2); + expect(data.fields[1].values).toEqual([1, 1, 3, 3.5, 4.5]); + }); + }); + it('throws error when calculating moving average if window size < 1', async () => { const cfg = { id: DataTransformerID.calculateField, diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 1a1ada7a5b8..80f41cc985e 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -357,8 +357,11 @@ function getTrailingWindowValues(frame: DataFrame, reducer: ReducerID, selectedF sum += currentValue; if (i > window - 1) { - sum -= selectedField.values[i - window]; - count--; + const value = selectedField.values[i - window]; + if (value != null) { + sum -= value; + count--; + } } } vals.push(count === 0 ? 0 : sum / count); diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 0081e6fc721..d3dbd6c2c7f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -236,4 +236,5 @@ export interface FeatureToggles { enableExtensionsAdminPage?: boolean; zipkinBackendMigration?: boolean; enableSCIM?: boolean; + crashDetection?: boolean; } diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index 6950ce91f24..06e02805a6f 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@typescript-eslint/types": "^8.9.0", - "eslint": "9.12.0", + "eslint": "9.14.0", "tslib": "2.7.0" }, "private": true diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index e3eb85bba5d..4de626ec320 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -56,8 +56,8 @@ "tslib": "2.7.0" }, "devDependencies": { - "@babel/core": "7.25.9", - "@babel/preset-env": "7.25.9", + "@babel/core": "7.26.0", + "@babel/preset-env": "7.26.0", "@babel/preset-react": "7.25.9", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.3.0", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index b3a753cce6c..caba46ada87 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -34,7 +34,7 @@ "build": "yarn generate && rollup -c rollup.config.ts --configPlugin esbuild" }, "devDependencies": { - "@babel/core": "7.25.9", + "@babel/core": "7.26.0", "@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 964787d4819..577a6adcd29 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -24,7 +24,7 @@ "@grafana/runtime": "11.4.0-pre", "@grafana/schema": "11.4.0-pre", "@grafana/ui": "11.4.0-pre", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0" diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index f13dff00b8b..ee0fa912cc0 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -11,7 +11,7 @@ "@swc/core": "1.4.2", "@types/eslint": "9.6.1", "copy-webpack-plugin": "12.0.2", - "eslint": "9.12.0", + "eslint": "9.14.0", "eslint-webpack-plugin": "4.2.0", "fork-ts-checker-webpack-plugin": "9.0.2", "glob": "11.0.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index cb9b5d14e41..b305c672b5d 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -50,7 +50,7 @@ "@lezer/highlight": "1.2.1", "@lezer/lr": "1.4.2", "@prometheus-io/lezer-promql": "0.55.1", - "@reduxjs/toolkit": "2.2.8", + "@reduxjs/toolkit": "2.3.0", "d3": "7.9.0", "date-fns": "3.6.0", "debounce-promise": "3.1.2", @@ -65,7 +65,7 @@ "pluralize": "8.0.0", "prismjs": "1.29.0", "react-highlight-words": "0.20.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "react-window": "1.8.10", "rxjs": "7.8.1", @@ -83,7 +83,7 @@ "@swc/core": "1.4.2", "@swc/helpers": "0.5.15", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/d3": "7.4.3", @@ -106,11 +106,11 @@ "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", "esbuild": "0.24.0", - "eslint": "9.12.0", + "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "28.8.3", - "eslint-plugin-jsdoc": "50.4.3", + "eslint-plugin-jest": "28.9.0", + "eslint-plugin-jsdoc": "50.5.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-lodash": "8.0.0", "eslint-plugin-react": "7.37.2", @@ -131,7 +131,7 @@ "rollup-plugin-esbuild": "6.1.1", "rollup-plugin-node-externals": "^7.1.3", "sass": "1.79.5", - "sass-loader": "16.0.2", + "sass-loader": "16.0.3", "style-loader": "4.0.0", "testing-library-selector": "0.3.1", "ts-node": "10.9.2", diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx index bdfdd52f3f0..4aa698fa7cb 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx @@ -12,7 +12,6 @@ import { config } from '@grafana/runtime'; import { AsyncSelect, Button, - CustomScrollbar, FormatOptionLabelMeta, getSelectStyles, Icon, @@ -22,6 +21,7 @@ import { useStyles2, useTheme2, } from '@grafana/ui'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { PrometheusDatasource } from '../../datasource'; import { truncateResult } from '../../language_utils'; @@ -245,15 +245,9 @@ export function MetricSelect({ style={{ maxHeight: Math.round(maxHeight * 0.9) }} aria-label="Select options menu" > - + {children} - + {optionsLoaded && (
diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 32105e354ee..611de96aad4 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -25,7 +25,7 @@ "lodash": "4.17.21", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "react-virtualized-auto-sizer": "1.0.24", "rxjs": "7.8.1", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index f7f631e3d05..67dff576b28 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -99,7 +99,7 @@ "react-inlinesvg": "3.0.2", "react-loading-skeleton": "3.5.0", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-table": "7.8.0", "react-transition-group": "4.4.5", "react-use": "17.5.1", @@ -114,7 +114,7 @@ "uuid": "9.0.1" }, "devDependencies": { - "@babel/core": "7.25.9", + "@babel/core": "7.26.0", "@faker-js/faker": "^9.0.0", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.3.0", @@ -135,7 +135,7 @@ "@storybook/react-webpack5": "^8.1.6", "@storybook/theming": "^8.1.6", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/chance": "1.1.6", @@ -161,7 +161,7 @@ "@types/uuid": "9.0.8", "chance": "1.1.12", "common-tags": "1.8.2", - "core-js": "3.38.1", + "core-js": "3.39.0", "css-loader": "7.1.2", "csstype": "3.1.3", "esbuild": "0.24.0", @@ -179,7 +179,7 @@ "rollup-plugin-esbuild": "6.1.1", "rollup-plugin-node-externals": "^7.1.3", "rollup-plugin-svg-import": "3.0.0", - "sass-loader": "16.0.2", + "sass-loader": "16.0.3", "storybook": "^8.1.6", "storybook-dark-mode": "^4.0.1", "style-loader": "4.0.0", diff --git a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts index bedebb4b749..e2f7e3ac851 100644 --- a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts +++ b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts @@ -41,7 +41,7 @@ export const useComboboxFloat = ( const preferredMaxHeight = availableHeight - POPOVER_PADDING; const width = Math.max(preferredMaxWidth, 0); - const height = Math.min(Math.max(preferredMaxHeight, OPTION_HEIGHT), POPOVER_MAX_HEIGHT); + const height = Math.min(Math.max(preferredMaxHeight, OPTION_HEIGHT * 6), POPOVER_MAX_HEIGHT); setPopoverMaxSize({ width, height }); }, diff --git a/pkg/services/authz/zanzana/client/client.go b/pkg/services/authz/zanzana/client/client.go index 01e7ed8e60b..819cf2b91a5 100644 --- a/pkg/services/authz/zanzana/client/client.go +++ b/pkg/services/authz/zanzana/client/client.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/authlib/authz" authzv1 "github.com/grafana/authlib/authz/proto/v1" "github.com/grafana/authlib/claims" - openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel" "google.golang.org/grpc" @@ -37,7 +36,6 @@ func WithLogger(logger log.Logger) ClientOption { type Client struct { logger log.Logger - openfga openfgav1.OpenFGAServiceClient authz authzv1.AuthzServiceClient authzext authzextv1.AuthzExtentionServiceClient tenantID string @@ -59,7 +57,6 @@ func NewClient(ctx context.Context, cc grpc.ClientConnInterface, cfg *setting.Cf func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption) (*Client, error) { c := &Client{ - openfga: openfgav1.NewOpenFGAServiceClient(cc), authz: authzv1.NewAuthzServiceClient(cc), authzext: authzextv1.NewAuthzExtentionServiceClient(cc), } @@ -103,7 +100,7 @@ func (c *Client) Compile(ctx context.Context, id claims.AuthInfo, req authz.List ctx, span := tracer.Start(ctx, "authz.zanzana.client.Compile") defer span.End() - _, err := c.authzext.List(ctx, &authzextv1.ListRequest{ + res, err := c.authzext.List(ctx, &authzextv1.ListRequest{ Subject: id.GetUID(), Group: req.Group, Verb: utils.VerbList, @@ -115,8 +112,34 @@ func (c *Client) Compile(ctx context.Context, id claims.AuthInfo, req authz.List return nil, err } - // FIXME: implement checker - return func(namespace, name, folder string) bool { return false }, nil + return newItemChecker(res), nil +} + +func newItemChecker(res *authzextv1.ListResponse) authz.ItemChecker { + // if we can see all resource of this type we can just return a function that always return true + if res.GetAll() { + return func(_, _, _ string) bool { return true } + } + + folders := make(map[string]struct{}, len(res.Folders)) + for _, f := range res.Folders { + folders[f] = struct{}{} + } + + items := make(map[string]struct{}, len(res.Items)) + for _, i := range res.Items { + items[i] = struct{}{} + } + + return func(_, name, folder string) bool { + if _, ok := items[name]; ok { + return true + } + if _, ok := folders[folder]; ok { + return true + } + return false + } } func (c *Client) List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error) { diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index fde10066063..61457166380 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -175,7 +175,7 @@ func (cma *CloudMigrationAPI) GetSessionList(c *contextmodel.ReqContext) respons ctx, span := cma.tracer.Start(c.Req.Context(), "MigrationAPI.GetSessionList") defer span.End() - sl, err := cma.cloudMigrationService.GetSessionList(ctx) + sl, err := cma.cloudMigrationService.GetSessionList(ctx, c.OrgID) if err != nil { span.SetStatus(codes.Error, "session list error") span.RecordError(err) @@ -208,7 +208,7 @@ func (cma *CloudMigrationAPI) GetSession(c *contextmodel.ReqContext) response.Re return response.Error(http.StatusBadRequest, "invalid session uid", err) } - s, err := cma.cloudMigrationService.GetSession(ctx, uid) + s, err := cma.cloudMigrationService.GetSession(ctx, c.OrgID, uid) if err != nil { span.SetStatus(codes.Error, "session not found") span.RecordError(err) @@ -245,8 +245,9 @@ func (cma *CloudMigrationAPI) CreateSession(c *contextmodel.ReqContext) response return response.ErrOrFallback(http.StatusBadRequest, "bad request data", err) } - s, err := cma.cloudMigrationService.CreateSession(ctx, cloudmigration.CloudMigrationSessionRequest{ + s, err := cma.cloudMigrationService.CreateSession(ctx, c.SignedInUser, cloudmigration.CloudMigrationSessionRequest{ AuthToken: cmd.AuthToken, + OrgID: c.SignedInUser.OrgID, }) if err != nil { span.SetStatus(codes.Error, "session creation error") @@ -285,7 +286,7 @@ func (cma *CloudMigrationAPI) DeleteSession(c *contextmodel.ReqContext) response return response.ErrOrFallback(http.StatusBadRequest, "invalid session uid", err) } - _, err := cma.cloudMigrationService.DeleteSession(ctx, uid) + _, err := cma.cloudMigrationService.DeleteSession(ctx, c.OrgID, c.SignedInUser, uid) if err != nil { span.SetStatus(codes.Error, "session delete error") span.RecordError(err) @@ -365,6 +366,7 @@ func (cma *CloudMigrationAPI) GetSnapshot(c *contextmodel.ReqContext) response.R SessionUID: sessUid, ResultPage: c.QueryInt("resultPage"), ResultLimit: c.QueryInt("resultLimit"), + OrgID: c.SignedInUser.OrgID, } if q.ResultLimit == 0 { q.ResultLimit = 100 @@ -448,6 +450,7 @@ func (cma *CloudMigrationAPI) GetSnapshotList(c *contextmodel.ReqContext) respon Limit: c.QueryInt("limit"), Page: c.QueryInt("page"), Sort: c.Query("sort"), + OrgID: c.SignedInUser.OrgID, } if q.Limit == 0 { q.Limit = 100 @@ -508,7 +511,7 @@ func (cma *CloudMigrationAPI) UploadSnapshot(c *contextmodel.ReqContext) respons return response.ErrOrFallback(http.StatusBadRequest, "invalid snapshot uid", err) } - if err := cma.cloudMigrationService.UploadSnapshot(ctx, sessUid, snapshotUid); err != nil { + if err := cma.cloudMigrationService.UploadSnapshot(ctx, c.OrgID, c.SignedInUser, sessUid, snapshotUid); err != nil { span.SetStatus(codes.Error, "error uploading snapshot") span.RecordError(err) diff --git a/pkg/services/cloudmigration/cloudmigration.go b/pkg/services/cloudmigration/cloudmigration.go index 8e93df6d024..faebba11d50 100644 --- a/pkg/services/cloudmigration/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigration.go @@ -16,14 +16,14 @@ type Service interface { ValidateToken(ctx context.Context, mig CloudMigrationSession) error DeleteToken(ctx context.Context, uid string) error - CreateSession(ctx context.Context, req CloudMigrationSessionRequest) (*CloudMigrationSessionResponse, error) - GetSession(ctx context.Context, migUID string) (*CloudMigrationSession, error) - DeleteSession(ctx context.Context, migUID string) (*CloudMigrationSession, error) - GetSessionList(context.Context) (*CloudMigrationSessionListResponse, error) + CreateSession(ctx context.Context, signedInUser *user.SignedInUser, req CloudMigrationSessionRequest) (*CloudMigrationSessionResponse, error) + GetSession(ctx context.Context, orgID int64, migUID string) (*CloudMigrationSession, error) + DeleteSession(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, migUID string) (*CloudMigrationSession, error) + GetSessionList(ctx context.Context, orgID int64) (*CloudMigrationSessionListResponse, error) CreateSnapshot(ctx context.Context, signedInUser *user.SignedInUser, sessionUid string) (*CloudMigrationSnapshot, error) GetSnapshot(ctx context.Context, query GetSnapshotsQuery) (*CloudMigrationSnapshot, error) GetSnapshotList(ctx context.Context, query ListSnapshotsQuery) ([]CloudMigrationSnapshot, error) - UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error + UploadSnapshot(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, sessionUid string, snapshotUid string) error CancelSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index e7b58fa5ebf..2540df5442e 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -362,10 +362,10 @@ func (s *Service) DeleteToken(ctx context.Context, tokenID string) error { return nil } -func (s *Service) GetSession(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { +func (s *Service) GetSession(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.GetSession") defer span.End() - migration, err := s.store.GetMigrationSessionByUID(ctx, uid) + migration, err := s.store.GetMigrationSessionByUID(ctx, orgID, uid) if err != nil { return nil, err } @@ -373,11 +373,11 @@ func (s *Service) GetSession(ctx context.Context, uid string) (*cloudmigration.C return migration, nil } -func (s *Service) GetSessionList(ctx context.Context) (*cloudmigration.CloudMigrationSessionListResponse, error) { +func (s *Service) GetSessionList(ctx context.Context, orgID int64) (*cloudmigration.CloudMigrationSessionListResponse, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.GetSessionList") defer span.End() - values, err := s.store.GetCloudMigrationSessionList(ctx) + values, err := s.store.GetCloudMigrationSessionList(ctx, orgID) if err != nil { return nil, fmt.Errorf("retrieving session list from store: %w", err) } @@ -394,7 +394,7 @@ func (s *Service) GetSessionList(ctx context.Context) (*cloudmigration.CloudMigr return &cloudmigration.CloudMigrationSessionListResponse{Sessions: migrations}, nil } -func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { +func (s *Service) CreateSession(ctx context.Context, signedInUser *user.SignedInUser, cmd cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.CreateSession") defer span.End() @@ -408,7 +408,7 @@ func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMig return nil, cloudmigration.ErrTokenInvalid.Errorf("token could not be decoded") // don't want to leak info here } - migration := token.ToMigration() + migration := token.ToMigration(cmd.OrgID) // validate token against GMS before saving if err := s.ValidateToken(ctx, migration); err != nil { return nil, err @@ -419,7 +419,7 @@ func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMig return nil, cloudmigration.ErrSessionCreationFailure.Errorf("error creating migration") } - s.report(ctx, &migration, gmsclient.EventConnect, 0, nil) + s.report(ctx, &migration, gmsclient.EventConnect, 0, nil, signedInUser.UserUID) return &cloudmigration.CloudMigrationSessionResponse{ UID: cm.UID, @@ -429,18 +429,18 @@ func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMig }, nil } -func (s *Service) DeleteSession(ctx context.Context, sessionUID string) (*cloudmigration.CloudMigrationSession, error) { +func (s *Service) DeleteSession(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, sessionUID string) (*cloudmigration.CloudMigrationSession, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.DeleteSession") defer span.End() - session, snapshots, err := s.store.DeleteMigrationSessionByUID(ctx, sessionUID) + session, snapshots, err := s.store.DeleteMigrationSessionByUID(ctx, orgID, sessionUID) if err != nil { - s.report(ctx, session, gmsclient.EventDisconnect, 0, err) + s.report(ctx, session, gmsclient.EventDisconnect, 0, err, signedInUser.UserUID) return nil, fmt.Errorf("deleting migration from db for session %v: %w", sessionUID, err) } err = s.deleteLocalFiles(snapshots) - s.report(ctx, session, gmsclient.EventDisconnect, 0, err) + s.report(ctx, session, gmsclient.EventDisconnect, 0, err, signedInUser.UserUID) return session, nil } @@ -451,7 +451,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI defer span.End() // fetch session for the gms auth token - session, err := s.store.GetMigrationSessionByUID(ctx, sessionUid) + session, err := s.store.GetMigrationSessionByUID(ctx, signedInUser.GetOrgID(), sessionUid) if err != nil { return nil, fmt.Errorf("fetching migration session for uid %s: %w", sessionUid, err) } @@ -506,7 +506,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI asyncCtx, cancelFunc := context.WithCancel(asyncCtx) s.cancelFunc = cancelFunc - s.report(asyncCtx, session, gmsclient.EventStartBuildingSnapshot, 0, nil) + s.report(asyncCtx, session, gmsclient.EventStartBuildingSnapshot, 0, nil, signedInUser.UserUID) start := time.Now() err := s.buildSnapshot(asyncCtx, signedInUser, initResp.MaxItemsPerPartition, initResp.Metadata, snapshot) @@ -527,7 +527,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI } span.SetStatus(codes.Ok, "snapshot built") - s.report(asyncCtx, session, gmsclient.EventDoneBuildingSnapshot, time.Since(start), err) + s.report(asyncCtx, session, gmsclient.EventDoneBuildingSnapshot, time.Since(start), err, signedInUser.UserUID) }() return &snapshot, nil @@ -538,13 +538,13 @@ func (s *Service) GetSnapshot(ctx context.Context, query cloudmigration.GetSnaps ctx, span := s.tracer.Start(ctx, "CloudMigrationService.GetSnapshot") defer span.End() - sessionUid, snapshotUid := query.SessionUID, query.SnapshotUID - snapshot, err := s.store.GetSnapshotByUID(ctx, sessionUid, snapshotUid, query.ResultPage, query.ResultLimit) + orgID, sessionUid, snapshotUid := query.OrgID, query.SessionUID, query.SnapshotUID + snapshot, err := s.store.GetSnapshotByUID(ctx, orgID, sessionUid, snapshotUid, query.ResultPage, query.ResultLimit) if err != nil { return nil, fmt.Errorf("fetching snapshot for uid %s: %w", snapshotUid, err) } - session, err := s.store.GetMigrationSessionByUID(ctx, sessionUid) + session, err := s.store.GetMigrationSessionByUID(ctx, orgID, sessionUid) if err != nil { return nil, fmt.Errorf("fetching session for uid %s: %w", sessionUid, err) } @@ -594,7 +594,7 @@ func (s *Service) GetSnapshot(ctx context.Context, query cloudmigration.GetSnaps } // Refresh the snapshot after the update - snapshot, err = s.store.GetSnapshotByUID(ctx, sessionUid, snapshotUid, query.ResultPage, query.ResultLimit) + snapshot, err = s.store.GetSnapshotByUID(ctx, orgID, sessionUid, snapshotUid, query.ResultPage, query.ResultLimit) if err != nil { return nil, fmt.Errorf("fetching snapshot for uid %s: %w", snapshotUid, err) } @@ -685,7 +685,7 @@ func (s *Service) GetSnapshotList(ctx context.Context, query cloudmigration.List return snapshotList, nil } -func (s *Service) UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error { +func (s *Service) UploadSnapshot(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, sessionUid string, snapshotUid string) error { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.UploadSnapshot", trace.WithAttributes( attribute.String("sessionUid", sessionUid), @@ -695,7 +695,7 @@ func (s *Service) UploadSnapshot(ctx context.Context, sessionUid string, snapsho defer span.End() // fetch session for the gms auth token - session, err := s.store.GetMigrationSessionByUID(ctx, sessionUid) + session, err := s.store.GetMigrationSessionByUID(ctx, orgID, sessionUid) if err != nil { return fmt.Errorf("fetching migration session for uid %s: %w", sessionUid, err) } @@ -703,6 +703,7 @@ func (s *Service) UploadSnapshot(ctx context.Context, sessionUid string, snapsho snapshot, err := s.GetSnapshot(ctx, cloudmigration.GetSnapshotsQuery{ SnapshotUID: snapshotUid, SessionUID: sessionUid, + OrgID: orgID, }) if err != nil { return fmt.Errorf("fetching snapshot with uid %s: %w", snapshotUid, err) @@ -739,7 +740,7 @@ func (s *Service) UploadSnapshot(ctx context.Context, sessionUid string, snapsho asyncCtx, s.cancelFunc = context.WithCancel(asyncCtx) - s.report(asyncCtx, session, gmsclient.EventStartUploadingSnapshot, 0, nil) + s.report(asyncCtx, session, gmsclient.EventStartUploadingSnapshot, 0, nil, signedInUser.UserUID) start := time.Now() err := s.uploadSnapshot(asyncCtx, session, snapshot, uploadUrl) @@ -759,7 +760,7 @@ func (s *Service) UploadSnapshot(ctx context.Context, sessionUid string, snapsho } } - s.report(asyncCtx, session, gmsclient.EventDoneUploadingSnapshot, time.Since(start), err) + s.report(asyncCtx, session, gmsclient.EventDoneUploadingSnapshot, time.Since(start), err, signedInUser.UserUID) }() return nil @@ -807,6 +808,7 @@ func (s *Service) report( t gmsclient.LocalEventType, d time.Duration, evtErr error, + userUID string, ) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.report") defer span.End() @@ -831,6 +833,7 @@ func (s *Service) report( e := gmsclient.EventRequestDTO{ Event: t, LocalID: id, + UserUID: userUID, } if d != 0 { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go index 5db6e03304e..28bd9ef5cc0 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go @@ -29,19 +29,19 @@ func (s *NoopServiceImpl) ValidateToken(ctx context.Context, cm cloudmigration.C return cloudmigration.ErrMigrationDisabled } -func (s *NoopServiceImpl) GetSession(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { +func (s *NoopServiceImpl) GetSession(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) { return nil, cloudmigration.ErrFeatureDisabledError } -func (s *NoopServiceImpl) GetSessionList(ctx context.Context) (*cloudmigration.CloudMigrationSessionListResponse, error) { +func (s *NoopServiceImpl) GetSessionList(ctx context.Context, orgID int64) (*cloudmigration.CloudMigrationSessionListResponse, error) { return nil, cloudmigration.ErrFeatureDisabledError } -func (s *NoopServiceImpl) CreateSession(ctx context.Context, cm cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { +func (s *NoopServiceImpl) CreateSession(ctx context.Context, signedInUser *user.SignedInUser, cm cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { return nil, cloudmigration.ErrMigrationDisabled } -func (s *NoopServiceImpl) DeleteSession(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { +func (s *NoopServiceImpl) DeleteSession(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, uid string) (*cloudmigration.CloudMigrationSession, error) { return nil, cloudmigration.ErrFeatureDisabledError } @@ -57,7 +57,7 @@ func (s *NoopServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrat return nil, cloudmigration.ErrFeatureDisabledError } -func (s *NoopServiceImpl) UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error { +func (s *NoopServiceImpl) UploadSnapshot(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, sessionUid string, snapshotUid string) error { return cloudmigration.ErrFeatureDisabledError } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 18b28f7ad2a..67554200b90 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -2,6 +2,7 @@ package cloudmigrationimpl import ( "context" + "errors" "maps" "os" "path/filepath" @@ -48,7 +49,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/sdk/trace/tracetest" ) func Test_NoopServiceDoesNothing(t *testing.T) { @@ -83,8 +83,8 @@ func Test_CreateGetAndDeleteToken(t *testing.T) { func Test_GetSnapshotStatusFromGMS(t *testing.T) { s := setUpServiceTest(t, false).(*Service) - gmsClientMock := &gmsClientMock{} - s.gmsClient = gmsClientMock + gmsClientFake := &gmsClientMock{} + s.gmsClient = gmsClientFake // Insert a session and snapshot into the database before we start createTokenResp, err := s.CreateToken(context.Background()) @@ -112,7 +112,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) assert.Equal(t, cloudmigration.SnapshotStatusCreating, snapshot.Status) - assert.Never(t, func() bool { return gmsClientMock.GetSnapshotStatusCallCount() > 0 }, time.Second, 10*time.Millisecond) + assert.Never(t, func() bool { return gmsClientFake.GetSnapshotStatusCallCount() > 0 }, time.Second, 10*time.Millisecond) // Make the status pending processing and ensure GMS gets called err = s.store.UpdateSnapshot(context.Background(), cloudmigration.UpdateSnapshotCmd{ @@ -123,12 +123,13 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { assert.NoError(t, err) cleanupFunc := func() { - gmsClientMock.getStatusCalled = 0 - gmsClientMock.getSnapshotResponse = nil if s.cancelFunc != nil { s.cancelFunc() } + gmsClientFake = &gmsClientMock{} + s.gmsClient = gmsClientFake + err = s.store.UpdateSnapshot(context.Background(), cloudmigration.UpdateSnapshotCmd{ UID: uid, SessionID: sess.UID, @@ -152,7 +153,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { } t.Run("test case: gms snapshot initialized", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateInitialized, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -161,12 +162,12 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusPendingProcessing), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot processing", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateProcessing, } _, err := s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -175,12 +176,12 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusProcessing), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot finished", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateFinished, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -189,13 +190,13 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusFinished), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot canceled", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateCanceled, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -204,13 +205,13 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusCanceled), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot error", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateError, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -219,13 +220,13 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { }) assert.NoError(t, err) require.Eventually(t, checkStatusSync(cloudmigration.SnapshotStatusError), time.Second, 10*time.Millisecond) - assert.Equal(t, 1, gmsClientMock.GetSnapshotStatusCallCount()) + assert.Equal(t, 1, gmsClientFake.GetSnapshotStatusCallCount()) t.Cleanup(cleanupFunc) }) t.Run("test case: gms snapshot unknown", func(t *testing.T) { - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateUnknown, } snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ @@ -235,7 +236,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { assert.NoError(t, err) // snapshot status should remain unchanged - require.Eventually(t, func() bool { return gmsClientMock.GetSnapshotStatusCallCount() == 1 }, time.Second, 10*time.Millisecond) + require.Eventually(t, func() bool { return gmsClientFake.GetSnapshotStatusCallCount() == 1 }, time.Second, 10*time.Millisecond) snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ SnapshotUID: uid, SessionUID: sess.UID, @@ -249,7 +250,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { t.Run("GMS results applied to local snapshot", func(t *testing.T) { t.Skip("This test is flaky and needs to be fixed") - gmsClientMock.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ + gmsClientFake.getSnapshotResponse = &cloudmigration.GetSnapshotStatusResponse{ State: cloudmigration.SnapshotStateFinished, Results: []cloudmigration.CloudMigrationResource{ { @@ -268,7 +269,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { SessionUID: sess.UID, }) assert.NoError(t, err) - require.Eventually(t, func() bool { return gmsClientMock.GetSnapshotStatusCallCount() == 1 }, time.Second, 10*time.Millisecond) + require.Eventually(t, func() bool { return gmsClientFake.GetSnapshotStatusCallCount() == 1 }, time.Second, 10*time.Millisecond) snapshot, err = s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ SnapshotUID: uid, @@ -358,22 +359,19 @@ func Test_OnlyQueriesStatusFromGMSWhenRequired(t *testing.T) { func Test_DeletedDashboardsNotMigrated(t *testing.T) { s := setUpServiceTest(t, false).(*Service) + + /** NOTE: this is not used at the moment since we changed the service + // modify what the mock returns for just this test case dashMock := s.dashboardService.(*dashboards.FakeDashboardService) dashMock.On("GetAllDashboards", mock.Anything).Return( []*dashboards.Dashboard{ - { - UID: "1", - Data: simplejson.New(), - }, - { - UID: "2", - Data: simplejson.New(), - Deleted: time.Now(), - }, + {UID: "1", OrgID: 1, Data: simplejson.New()}, + {UID: "2", OrgID: 1, Data: simplejson.New(), Deleted: time.Now()}, }, nil, ) + */ data, err := s.getMigrationDataJSON(context.TODO(), &user.SignedInUser{OrgID: 1}) assert.NoError(t, err) @@ -551,12 +549,13 @@ func Test_NonCoreDataSourcesHaveWarning(t *testing.T) { func TestDeleteSession(t *testing.T) { s := setUpServiceTest(t, false).(*Service) + user := &user.SignedInUser{UserUID: "user123"} t.Run("when deleting a session that does not exist in the database, it returns an error", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - session, err := s.DeleteSession(ctx, "invalid-session-uid") + session, err := s.DeleteSession(ctx, 2, user, "invalid-session-uid") require.Nil(t, session) require.Error(t, err) }) @@ -571,19 +570,20 @@ func TestDeleteSession(t *testing.T) { cmd := cloudmigration.CloudMigrationSessionRequest{ AuthToken: createTokenResp.Token, + OrgID: 3, } - createResp, err := s.CreateSession(ctx, cmd) + createResp, err := s.CreateSession(ctx, user, cmd) require.NoError(t, err) require.NotEmpty(t, createResp.UID) require.NotEmpty(t, createResp.Slug) - deletedSession, err := s.DeleteSession(ctx, createResp.UID) + deletedSession, err := s.DeleteSession(ctx, cmd.OrgID, user, createResp.UID) require.NoError(t, err) require.NotNil(t, deletedSession) require.Equal(t, deletedSession.UID, createResp.UID) - notFoundSession, err := s.GetSession(ctx, deletedSession.UID) + notFoundSession, err := s.GetSession(ctx, cmd.OrgID, deletedSession.UID) require.ErrorIs(t, err, cloudmigration.ErrMigrationNotFound) require.Nil(t, notFoundSession) }) @@ -600,7 +600,7 @@ func TestReportEvent(t *testing.T) { s.gmsClient = gmsMock require.NotPanics(t, func() { - s.report(ctx, nil, gmsclient.EventConnect, time.Minute, nil) + s.report(ctx, nil, gmsclient.EventConnect, time.Minute, nil, "user123") }) require.Zero(t, gmsMock.reportEventCalled) @@ -616,7 +616,7 @@ func TestReportEvent(t *testing.T) { s.gmsClient = gmsMock require.NotPanics(t, func() { - s.report(ctx, &cloudmigration.CloudMigrationSession{}, gmsclient.EventConnect, time.Minute, nil) + s.report(ctx, &cloudmigration.CloudMigrationSession{}, gmsclient.EventConnect, time.Minute, nil, "user123") }) require.Equal(t, 1, gmsMock.reportEventCalled) @@ -699,6 +699,7 @@ func TestGetParentNames(t *testing.T) { folders []folder.CreateFolderCommand dashboards []dashboards.Dashboard libraryElements []libraryElement + alertRules []alertRule expectedParentNames map[cloudmigration.MigrateDataType][]string }{ { @@ -719,10 +720,15 @@ func TestGetParentNames(t *testing.T) { {UID: "libraryElementUID-0", FolderUID: &libraryElementFolderUID}, {UID: "libraryElementUID-1"}, }, + alertRules: []alertRule{ + {UID: "alertRuleUID-0", FolderUID: ""}, + {UID: "alertRuleUID-1", FolderUID: "folderUID-B"}, + }, expectedParentNames: map[cloudmigration.MigrateDataType][]string{ cloudmigration.DashboardDataType: {"", "Folder A", "Folder B"}, cloudmigration.FolderDataType: {"Folder A"}, cloudmigration.LibraryElementDataType: {"Folder A"}, + cloudmigration.AlertRuleType: {"Folder B"}, }, }, } @@ -730,7 +736,7 @@ func TestGetParentNames(t *testing.T) { for _, tc := range testcases { s.folderService = &foldertest.FakeService{ExpectedFolders: tc.fakeFolders} - dataUIDsToParentNamesByType, err := s.getParentNames(ctx, user, tc.dashboards, tc.folders, tc.libraryElements) + dataUIDsToParentNamesByType, err := s.getParentNames(ctx, user, tc.dashboards, tc.folders, tc.libraryElements, tc.alertRules) require.NoError(t, err) for dataType, expectedParentNames := range tc.expectedParentNames { @@ -784,8 +790,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi sqlStore := db.InitTestDB(t) secretsService := secretsfakes.NewFakeSecretsService() rr := routing.NewRouteRegister() - spanRecorder := tracetest.NewSpanRecorder() - tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) + tracer := tracing.InitializeTracerForTest() mockFolder := &foldertest.FakeService{ ExpectedFolder: &folder.Folder{UID: "folderUID", Title: "Folder"}, } @@ -870,6 +875,21 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi LastApplied: time.Now().Unix(), })) + // Insert test data for dashboard test, should be removed later when we move GetAllDashboardsByOrgId() to the dashboard service + _, err = sqlStore.GetSqlxSession().Exec(context.Background(), ` + INSERT INTO + dashboard (id, org_id, data, deleted, slug, title, created, version, updated ) + VALUES + (1, 1, '{}', null, 'asdf', 'ghjk', '2024-03-27 15:30:43.000' , '1','2024-03-27 15:30:43.000' ), + (2, 1, '{}', '2024-03-27 15:30:43.000','qwert', 'yuio', '2024-03-27 15:30:43.000' , '2','2024-03-27 15:30:43.000'), + (3, 2, '{}', null, 'asdf', 'ghjk', '2024-03-27 15:30:43.000' , '1','2024-03-27 15:30:43.000' ), + (4, 2, '{}', '2024-03-27 15:30:43.000','qwert', 'yuio', '2024-03-27 15:30:43.000' , '2','2024-03-27 15:30:43.000'); + `, + ) + if err != nil { + require.NoError(t, err) + } + s, err := ProvideService( cfg, httpclient.NewProvider(), @@ -923,6 +943,9 @@ func (m *gmsClientMock) GetSnapshotStatus(_ context.Context, _ cloudmigration.Cl m.mu.Lock() m.getStatusCalled++ m.mu.Unlock() + if m.getSnapshotResponse == nil { + return nil, errors.New("no response set") + } return m.getSnapshotResponse, nil } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go index a9d5838113c..020e5e96122 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go @@ -45,7 +45,7 @@ func (m FakeServiceImpl) DeleteToken(_ context.Context, _ string) error { return nil } -func (m FakeServiceImpl) CreateSession(_ context.Context, _ cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { +func (m FakeServiceImpl) CreateSession(_ context.Context, _ *user.SignedInUser, _ cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) { if m.ReturnError { return nil, cloudmigration.ErrSessionCreationFailure } @@ -57,21 +57,21 @@ func (m FakeServiceImpl) CreateSession(_ context.Context, _ cloudmigration.Cloud }, nil } -func (m FakeServiceImpl) GetSession(_ context.Context, _ string) (*cloudmigration.CloudMigrationSession, error) { +func (m FakeServiceImpl) GetSession(_ context.Context, _ int64, _ string) (*cloudmigration.CloudMigrationSession, error) { if m.ReturnError { return nil, fmt.Errorf("mock error") } return &cloudmigration.CloudMigrationSession{UID: "fake"}, nil } -func (m FakeServiceImpl) DeleteSession(_ context.Context, _ string) (*cloudmigration.CloudMigrationSession, error) { +func (m FakeServiceImpl) DeleteSession(_ context.Context, _ int64, _ *user.SignedInUser, _ string) (*cloudmigration.CloudMigrationSession, error) { if m.ReturnError { return nil, fmt.Errorf("mock error") } return &cloudmigration.CloudMigrationSession{UID: "fake"}, nil } -func (m FakeServiceImpl) GetSessionList(_ context.Context) (*cloudmigration.CloudMigrationSessionListResponse, error) { +func (m FakeServiceImpl) GetSessionList(_ context.Context, _ int64) (*cloudmigration.CloudMigrationSessionListResponse, error) { if m.ReturnError { return nil, fmt.Errorf("mock error") } @@ -154,7 +154,7 @@ func (m FakeServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrati return cloudSnapshots, nil } -func (m FakeServiceImpl) UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error { +func (m FakeServiceImpl) UploadSnapshot(ctx context.Context, _ int64, _ *user.SignedInUser, sessionUid string, snapshotUid string) error { if m.ReturnError { return fmt.Errorf("mock error") } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index df57e1f2fc1..2daa17bc0b4 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -44,7 +44,7 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S defer span.End() // Data sources - dataSources, err := s.getDataSourceCommands(ctx) + dataSources, err := s.getDataSourceCommands(ctx, signedInUser) if err != nil { s.log.Error("Failed to get datasources", "err", err) return nil, err @@ -195,7 +195,7 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S } // Obtain the names of parent elements for Dashboard and Folders data types - parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements) + parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements, alertRules) if err != nil { s.log.Error("Failed to get parent folder names", "err", err) } @@ -208,17 +208,17 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return migrationData, nil } -func (s *Service) getDataSourceCommands(ctx context.Context) ([]datasources.AddDataSourceCommand, error) { +func (s *Service) getDataSourceCommands(ctx context.Context, signedInUser *user.SignedInUser) ([]datasources.AddDataSourceCommand, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getDataSourceCommands") defer span.End() - dataSources, err := s.dsService.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{}) + dataSources, err := s.dsService.GetDataSources(ctx, &datasources.GetDataSourcesQuery{OrgID: signedInUser.GetOrgID()}) if err != nil { s.log.Error("Failed to get all datasources", "err", err) return nil, err } - result := []datasources.AddDataSourceCommand{} + result := make([]datasources.AddDataSourceCommand, 0, len(dataSources)) for _, dataSource := range dataSources { // Decrypt secure json to send raw credentials decryptedData, err := s.secretsService.DecryptJsonData(ctx, dataSource.SecureJsonData) @@ -253,7 +253,7 @@ func (s *Service) getDashboardAndFolderCommands(ctx context.Context, signedInUse ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getDashboardAndFolderCommands") defer span.End() - dashs, err := s.dashboardService.GetAllDashboards(ctx) + dashs, err := s.store.GetAllDashboardsByOrgId(ctx, signedInUser.GetOrgID()) if err != nil { return nil, nil, err } @@ -279,20 +279,21 @@ func (s *Service) getDashboardAndFolderCommands(ctx context.Context, signedInUse folders, err := s.folderService.GetFolders(ctx, folder.GetFoldersQuery{ UIDs: folderUids, SignedInUser: signedInUser, + OrgID: signedInUser.GetOrgID(), WithFullpathUIDs: true, }) if err != nil { return nil, nil, err } - folderCmds := make([]folder.CreateFolderCommand, len(folders)) - for i, f := range folders { - folderCmds[i] = folder.CreateFolderCommand{ + folderCmds := make([]folder.CreateFolderCommand, 0, len(folders)) + for _, f := range folders { + folderCmds = append(folderCmds, folder.CreateFolderCommand{ UID: f.UID, Title: f.Title, Description: f.Description, ParentUID: f.ParentUID, - } + }) } return dashboardCmds, folderCmds, nil @@ -641,6 +642,7 @@ func (s *Service) getFolderNamesForFolderUIDs(ctx context.Context, signedInUser folders, err := s.folderService.GetFolders(ctx, folder.GetFoldersQuery{ UIDs: folderUIDs, SignedInUser: signedInUser, + OrgID: signedInUser.GetOrgID(), WithFullpathUIDs: true, }) if err != nil { @@ -660,16 +662,26 @@ func (s *Service) getFolderNamesForFolderUIDs(ctx context.Context, signedInUser // getParentNames finds the parent names for resources and returns a map of data type: {data UID : parentName} // for dashboards, folders and library elements - the parent is the parent folder -func (s *Service) getParentNames(ctx context.Context, signedInUser *user.SignedInUser, dashboards []dashboards.Dashboard, folders []folder.CreateFolderCommand, libraryElements []libraryElement) (map[cloudmigration.MigrateDataType]map[string](string), error) { - parentNamesByType := make(map[cloudmigration.MigrateDataType]map[string](string)) +func (s *Service) getParentNames( + ctx context.Context, + signedInUser *user.SignedInUser, + dashboards []dashboards.Dashboard, + folders []folder.CreateFolderCommand, + libraryElements []libraryElement, + alertRules []alertRule, +) (map[cloudmigration.MigrateDataType]map[string](string), error) { + parentNamesByType := make(map[cloudmigration.MigrateDataType]map[string]string) for _, dataType := range currentMigrationTypes { parentNamesByType[dataType] = make(map[string]string) } // Obtain list of unique folderUIDs - parentFolderUIDsSet := make(map[string]struct{}, len(dashboards)+len(folders)+len(libraryElements)) + parentFolderUIDsSet := make(map[string]struct{}) for _, dashboard := range dashboards { - parentFolderUIDsSet[dashboard.FolderUID] = struct{}{} + // we dont need the root folder + if dashboard.FolderUID != "" { + parentFolderUIDsSet[dashboard.FolderUID] = struct{}{} + } } for _, f := range folders { parentFolderUIDsSet[f.ParentUID] = struct{}{} @@ -679,6 +691,11 @@ func (s *Service) getParentNames(ctx context.Context, signedInUser *user.SignedI parentFolderUIDsSet[*libraryElement.FolderUID] = struct{}{} } } + for _, alertRule := range alertRules { + if alertRule.FolderUID != "" { + parentFolderUIDsSet[alertRule.FolderUID] = struct{}{} + } + } parentFolderUIDsSlice := make([]string, 0, len(parentFolderUIDsSet)) for parentFolderUID := range parentFolderUIDsSet { parentFolderUIDsSlice = append(parentFolderUIDsSlice, parentFolderUID) @@ -703,6 +720,11 @@ func (s *Service) getParentNames(ctx context.Context, signedInUser *user.SignedI parentNamesByType[cloudmigration.LibraryElementDataType][libraryElement.UID] = foldersUIDsToFolderName[*libraryElement.FolderUID] } } + for _, alertRule := range alertRules { + if alertRule.FolderUID != "" { + parentNamesByType[cloudmigration.AlertRuleType][alertRule.UID] = foldersUIDsToFolderName[alertRule.FolderUID] + } + } return parentNamesByType, err } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/store.go b/pkg/services/cloudmigration/cloudmigrationimpl/store.go index cbc4b7c4d99..2a36c1acfdc 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/store.go @@ -4,24 +4,29 @@ import ( "context" "github.com/grafana/grafana/pkg/services/cloudmigration" + "github.com/grafana/grafana/pkg/services/dashboards" ) type store interface { CreateMigrationSession(ctx context.Context, session cloudmigration.CloudMigrationSession) (*cloudmigration.CloudMigrationSession, error) - GetMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) - GetCloudMigrationSessionList(ctx context.Context) ([]*cloudmigration.CloudMigrationSession, error) + GetMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) + GetCloudMigrationSessionList(ctx context.Context, orgID int64) ([]*cloudmigration.CloudMigrationSession, error) // DeleteMigrationSessionByUID deletes the migration session, and all the related snapshot and resources. // the work is done in a transaction. - DeleteMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error) + DeleteMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error) CreateSnapshot(ctx context.Context, snapshot cloudmigration.CloudMigrationSnapshot) (string, error) UpdateSnapshot(ctx context.Context, snapshot cloudmigration.UpdateSnapshotCmd) error - GetSnapshotByUID(ctx context.Context, sessUid, id string, resultPage int, resultLimit int) (*cloudmigration.CloudMigrationSnapshot, error) + GetSnapshotByUID(ctx context.Context, orgID int64, sessUid, id string, resultPage int, resultLimit int) (*cloudmigration.CloudMigrationSnapshot, error) GetSnapshotList(ctx context.Context, query cloudmigration.ListSnapshotsQuery) ([]cloudmigration.CloudMigrationSnapshot, error) - DeleteSnapshot(ctx context.Context, snapshotUid string) error - CreateUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error - GetSnapshotResources(ctx context.Context, snapshotUid string, page int, limit int) ([]cloudmigration.CloudMigrationResource, error) - GetSnapshotResourceStats(ctx context.Context, snapshotUid string) (*cloudmigration.SnapshotResourceStats, error) - DeleteSnapshotResources(ctx context.Context, snapshotUid string) error + // Deleted because were not used externally + // - DeleteSnapshot(ctx context.Context, snapshotUid string) error + // - CreateUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error + // - GetSnapshotResources(ctx context.Context, snapshotUid string, page int, limit int) ([]cloudmigration.CloudMigrationResource, error) + // - GetSnapshotResourceStats(ctx context.Context, snapshotUid string) (*cloudmigration.SnapshotResourceStats, error) + // - DeleteSnapshotResources(ctx context.Context, snapshotUid string) error + + // TODO move this function dashboards/databases/databases.go + GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index b5189fffcff..a540beb10c0 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/cloudmigration" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/secrets" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -29,10 +30,10 @@ const ( GetSnapshotListSortingLatest = "latest" ) -func (ss *sqlStore) GetMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { +func (ss *sqlStore) GetMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, error) { var cm cloudmigration.CloudMigrationSession err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - exist, err := sess.Where("uid=?", uid).Get(&cm) + exist, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&cm) if err != nil { return err } @@ -74,11 +75,10 @@ func (ss *sqlStore) CreateMigrationSession(ctx context.Context, migration cloudm return &migration, nil } -func (ss *sqlStore) GetCloudMigrationSessionList(ctx context.Context) ([]*cloudmigration.CloudMigrationSession, error) { +func (ss *sqlStore) GetCloudMigrationSessionList(ctx context.Context, orgID int64) ([]*cloudmigration.CloudMigrationSession, error) { var migrations = make([]*cloudmigration.CloudMigrationSession, 0) err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - sess.OrderBy("created DESC") - return sess.Find(&migrations) + return sess.Where("org_id=?", orgID).OrderBy("created DESC").Find(&migrations) }) if err != nil { return nil, err @@ -95,10 +95,10 @@ func (ss *sqlStore) GetCloudMigrationSessionList(ctx context.Context) ([]*cloudm return migrations, nil } -func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error) { +func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error) { var c cloudmigration.CloudMigrationSession err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - exist, err := sess.Where("uid=?", uid).Get(&c) + exist, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&c) if err != nil { return err } @@ -124,11 +124,11 @@ func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, uid string) err = ss.db.InTransaction(ctx, func(ctx context.Context) error { for _, snapshot := range snapshots { - err := ss.DeleteSnapshotResources(ctx, snapshot.UID) + err := ss.deleteSnapshotResources(ctx, snapshot.UID) if err != nil { return fmt.Errorf("deleting snapshot resource from db: %w", err) } - err = ss.DeleteSnapshot(ctx, snapshot.UID) + err = ss.deleteSnapshot(ctx, orgID, snapshot.UID) if err != nil { return fmt.Errorf("deleting snapshot from db: %w", err) } @@ -214,7 +214,7 @@ func (ss *sqlStore) UpdateSnapshot(ctx context.Context, update cloudmigration.Up // Update resources if set if len(update.Resources) > 0 { - if err := ss.CreateUpdateSnapshotResources(ctx, update.UID, update.Resources); err != nil { + if err := ss.createUpdateSnapshotResources(ctx, update.UID, update.Resources); err != nil { return err } } @@ -224,7 +224,7 @@ func (ss *sqlStore) UpdateSnapshot(ctx context.Context, update cloudmigration.Up return err } -func (ss *sqlStore) DeleteSnapshot(ctx context.Context, snapshotUid string) error { +func (ss *sqlStore) deleteSnapshot(ctx context.Context, orgID int64, snapshotUid string) error { return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { _, err := sess.Delete(cloudmigration.CloudMigrationSnapshot{ UID: snapshotUid, @@ -233,9 +233,16 @@ func (ss *sqlStore) DeleteSnapshot(ctx context.Context, snapshotUid string) erro }) } -func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, sessionUid, uid string, resultPage int, resultLimit int) (*cloudmigration.CloudMigrationSnapshot, error) { +func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, orgID int64, sessionUid, uid string, resultPage int, resultLimit int) (*cloudmigration.CloudMigrationSnapshot, error) { + // first we check if the session exists, using orgId and sessionUid + session, err := ss.GetMigrationSessionByUID(ctx, orgID, sessionUid) + if err != nil || session == nil { + return nil, err + } + + // now we get the snapshot var snapshot cloudmigration.CloudMigrationSnapshot - err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { + err = ss.db.WithDbSession(ctx, func(sess *db.Session) error { exist, err := sess.Where("session_uid=? AND uid=?", sessionUid, uid).Get(&snapshot) if err != nil { return err @@ -257,11 +264,11 @@ func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, sessionUid, uid string snapshot.EncryptionKey = []byte(secret) } - resources, err := ss.GetSnapshotResources(ctx, uid, resultPage, resultLimit) + resources, err := ss.getSnapshotResources(ctx, uid, resultPage, resultLimit) if err == nil { snapshot.Resources = resources } - stats, err := ss.GetSnapshotResourceStats(ctx, uid) + stats, err := ss.getSnapshotResourceStats(ctx, uid) if err == nil { snapshot.StatsRollup = *stats } @@ -274,7 +281,9 @@ func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, sessionUid, uid string func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.ListSnapshotsQuery) ([]cloudmigration.CloudMigrationSnapshot, error) { var snapshots = make([]cloudmigration.CloudMigrationSnapshot, 0) err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - sess.Join("INNER", "cloud_migration_session", "cloud_migration_session.uid = cloud_migration_snapshot.session_uid") + sess.Join("INNER", "cloud_migration_session", + "cloud_migration_session.uid = cloud_migration_snapshot.session_uid AND cloud_migration_session.org_id = ?", query.OrgID, + ) if query.Limit != GetAllSnapshots { offset := (query.Page - 1) * query.Limit sess.Limit(query.Limit, offset) @@ -298,7 +307,7 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li snapshot.EncryptionKey = []byte(secret) } - if stats, err := ss.GetSnapshotResourceStats(ctx, snapshot.UID); err != nil { + if stats, err := ss.getSnapshotResourceStats(ctx, snapshot.UID); err != nil { return nil, err } else { snapshot.StatsRollup = *stats @@ -310,7 +319,7 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li // CreateUpdateSnapshotResources either updates a migration resource for a snapshot, or creates it if it does not exist // If the uid is not known, it uses snapshot_uid + resource_uid as a lookup -func (ss *sqlStore) CreateUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error { +func (ss *sqlStore) createUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error { return ss.db.InTransaction(ctx, func(ctx context.Context) error { sql := "UPDATE cloud_migration_resource SET status=?, error_string=?, error_code=? WHERE uid=? OR (snapshot_uid=? AND resource_uid=?)" err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { @@ -344,7 +353,7 @@ func (ss *sqlStore) CreateUpdateSnapshotResources(ctx context.Context, snapshotU }) } -func (ss *sqlStore) GetSnapshotResources(ctx context.Context, snapshotUid string, page int, limit int) ([]cloudmigration.CloudMigrationResource, error) { +func (ss *sqlStore) getSnapshotResources(ctx context.Context, snapshotUid string, page int, limit int) ([]cloudmigration.CloudMigrationResource, error) { if page < 1 { page = 1 } @@ -366,7 +375,7 @@ func (ss *sqlStore) GetSnapshotResources(ctx context.Context, snapshotUid string return resources, nil } -func (ss *sqlStore) GetSnapshotResourceStats(ctx context.Context, snapshotUid string) (*cloudmigration.SnapshotResourceStats, error) { +func (ss *sqlStore) getSnapshotResourceStats(ctx context.Context, snapshotUid string) (*cloudmigration.SnapshotResourceStats, error) { typeCounts := make([]struct { Count int `json:"count"` Type string `json:"type"` @@ -413,7 +422,7 @@ func (ss *sqlStore) GetSnapshotResourceStats(ctx context.Context, snapshotUid st return stats, nil } -func (ss *sqlStore) DeleteSnapshotResources(ctx context.Context, snapshotUid string) error { +func (ss *sqlStore) deleteSnapshotResources(ctx context.Context, snapshotUid string) error { return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { _, err := sess.Delete(cloudmigration.CloudMigrationResource{ SnapshotUID: snapshotUid, @@ -456,3 +465,19 @@ func (ss *sqlStore) decryptToken(ctx context.Context, cm *cloudmigration.CloudMi return nil } + +// TODO move this function dashboards/databases/databases.go +func (ss *sqlStore) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) { + //ctx, span := tracer.Start(ctx, "dashboards.database.GetAllDashboardsByOrgId") + //defer span.End() + + var dashs = make([]*dashboards.Dashboard, 0) + err := ss.db.WithDbSession(ctx, func(session *db.Session) error { + // "deleted IS NULL" is to avoid deleted dashboards + return session.Where("org_id = ? AND deleted IS NULL", orgID).Find(&dashs) + }) + if err != nil { + return nil, err + } + return dashs, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go index 58b98f6c1ad..c9ae1163eba 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go @@ -25,7 +25,7 @@ func Test_GetAllCloudMigrationSessions(t *testing.T) { ctx := context.Background() t.Run("get all cloud_migration_session entries", func(t *testing.T) { - value, err := s.GetCloudMigrationSessionList(ctx) + value, err := s.GetCloudMigrationSessionList(ctx, 1) require.NoError(t, err) require.Equal(t, 3, len(value)) for _, m := range value { @@ -54,6 +54,7 @@ func Test_CreateMigrationSession(t *testing.T) { cm := cloudmigration.CloudMigrationSession{ AuthToken: encodeToken("token"), Slug: "fake_stack", + OrgID: 3, StackID: 1234, RegionSlug: "fake_slug", ClusterSlug: "fake_cluster_slug", @@ -63,7 +64,7 @@ func Test_CreateMigrationSession(t *testing.T) { require.NotEmpty(t, sess.ID) require.NotEmpty(t, sess.UID) - getRes, err := s.GetMigrationSessionByUID(ctx, sess.UID) + getRes, err := s.GetMigrationSessionByUID(ctx, 3, sess.UID) require.NoError(t, err) require.Equal(t, sess.ID, getRes.ID) require.Equal(t, sess.UID, getRes.UID) @@ -80,13 +81,15 @@ func Test_GetMigrationSessionByUID(t *testing.T) { ctx := context.Background() t.Run("find session by uid", func(t *testing.T) { uid := "qwerty" - mig, err := s.GetMigrationSessionByUID(ctx, uid) + orgId := int64(1) + mig, err := s.GetMigrationSessionByUID(ctx, orgId, uid) require.NoError(t, err) require.Equal(t, uid, mig.UID) + require.Equal(t, orgId, mig.OrgID) }) t.Run("returns error if session is not found by uid", func(t *testing.T) { - _, err := s.GetMigrationSessionByUID(ctx, "fake_uid_1234") + _, err := s.GetMigrationSessionByUID(ctx, 1, "fake_uid_1234") require.ErrorIs(t, cloudmigration.ErrMigrationNotFound, err) }) } @@ -115,7 +118,10 @@ func Test_SnapshotManagement(t *testing.T) { ctx := context.Background() t.Run("tests the snapshot lifecycle", func(t *testing.T) { - session, err := s.CreateMigrationSession(ctx, cloudmigration.CloudMigrationSession{}) + session, err := s.CreateMigrationSession(ctx, cloudmigration.CloudMigrationSession{ + OrgID: 1, + AuthToken: encodeToken("token"), + }) require.NoError(t, err) // create a snapshot @@ -129,7 +135,7 @@ func Test_SnapshotManagement(t *testing.T) { require.NotEmpty(t, snapshotUid) //retrieve it from the db - snapshot, err := s.GetSnapshotByUID(ctx, session.UID, snapshotUid, 0, 0) + snapshot, err := s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, 0, 0) require.NoError(t, err) require.Equal(t, cloudmigration.SnapshotStatusCreating, snapshot.Status) @@ -138,22 +144,22 @@ func Test_SnapshotManagement(t *testing.T) { require.NoError(t, err) //retrieve it again - snapshot, err = s.GetSnapshotByUID(ctx, session.UID, snapshotUid, 0, 0) + snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, 0, 0) require.NoError(t, err) require.Equal(t, cloudmigration.SnapshotStatusCreating, snapshot.Status) // lists snapshots and ensures it's in there - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: session.UID, Page: 1, Limit: 100}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: session.UID, OrgID: 1, Page: 1, Limit: 100}) require.NoError(t, err) require.Len(t, snapshots, 1) require.Equal(t, *snapshot, snapshots[0]) // delete snapshot - err = s.DeleteSnapshot(ctx, snapshotUid) + err = s.deleteSnapshot(ctx, 1, snapshotUid) require.NoError(t, err) // now we expect not to find the snapshot - snapshot, err = s.GetSnapshotByUID(ctx, session.UID, snapshotUid, 0, 0) + snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, 0, 0) require.ErrorIs(t, err, cloudmigration.ErrSnapshotNotFound) require.Nil(t, snapshot) }) @@ -165,12 +171,12 @@ func Test_SnapshotResources(t *testing.T) { t.Run("tests CRUD of snapshot resources", func(t *testing.T) { // Get the default rows from the test - resources, err := s.GetSnapshotResources(ctx, "poiuy", 0, 100) + resources, err := s.getSnapshotResources(ctx, "poiuy", 0, 100) assert.NoError(t, err) assert.Len(t, resources, 3) // create a new resource and update an existing resource - err = s.CreateUpdateSnapshotResources(ctx, "poiuy", []cloudmigration.CloudMigrationResource{ + err = s.createUpdateSnapshotResources(ctx, "poiuy", []cloudmigration.CloudMigrationResource{ { Type: cloudmigration.DatasourceDataType, RefID: "mi39fj", @@ -184,7 +190,7 @@ func Test_SnapshotResources(t *testing.T) { assert.NoError(t, err) // Get resources again - resources, err = s.GetSnapshotResources(ctx, "poiuy", 0, 100) + resources, err = s.getSnapshotResources(ctx, "poiuy", 0, 100) assert.NoError(t, err) assert.Len(t, resources, 4) // ensure existing resource was updated @@ -203,7 +209,7 @@ func Test_SnapshotResources(t *testing.T) { } // check stats - stats, err := s.GetSnapshotResourceStats(ctx, "poiuy") + stats, err := s.getSnapshotResourceStats(ctx, "poiuy") assert.NoError(t, err) assert.Equal(t, map[cloudmigration.MigrateDataType]int{ cloudmigration.DatasourceDataType: 2, @@ -217,10 +223,10 @@ func Test_SnapshotResources(t *testing.T) { assert.Equal(t, 4, stats.Total) // delete snapshot resources - err = s.DeleteSnapshotResources(ctx, "poiuy") + err = s.deleteSnapshotResources(ctx, "poiuy") assert.NoError(t, err) // make sure they're gone - resources, err = s.GetSnapshotResources(ctx, "poiuy", 0, 100) + resources, err = s.getSnapshotResources(ctx, "poiuy", 0, 100) assert.NoError(t, err) assert.Len(t, resources, 0) }) @@ -233,7 +239,7 @@ func TestGetSnapshotList(t *testing.T) { ctx := context.Background() t.Run("returns list of snapshots that belong to a session", func(t *testing.T) { - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 100}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 1, Limit: 100}) require.NoError(t, err) ids := make([]string, 0) @@ -246,7 +252,7 @@ func TestGetSnapshotList(t *testing.T) { }) t.Run("returns only one snapshot that belongs to a session", func(t *testing.T) { - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 1}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 1, Limit: 1}) require.NoError(t, err) assert.Len(t, snapshots, 1) }) @@ -258,7 +264,7 @@ func TestGetSnapshotList(t *testing.T) { }) t.Run("returns paginated snapshot that belongs to a session", func(t *testing.T) { - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 2, Limit: 1}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 2, Limit: 1}) require.NoError(t, err) ids := make([]string, 0) @@ -271,7 +277,7 @@ func TestGetSnapshotList(t *testing.T) { }) t.Run("returns desc sorted list of snapshots that belong to a session", func(t *testing.T) { - snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 100, Sort: "latest"}) + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 1, Limit: 100, Sort: "latest"}) require.NoError(t, err) ids := make([]string, 0) @@ -291,7 +297,7 @@ func TestGetSnapshotList(t *testing.T) { t.Run("if the session is deleted, snapshots can't be retrieved anymore", func(t *testing.T) { // Delete the session. - _, _, err := s.DeleteMigrationSessionByUID(ctx, sessionUID) + _, _, err := s.DeleteMigrationSessionByUID(ctx, 1, sessionUID) require.NoError(t, err) // Fetch the snapshots that belong to the deleted session. @@ -363,15 +369,17 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { // insert cloud migration test data _, err := testDB.GetSqlxSession().Exec(ctx, ` INSERT INTO - cloud_migration_session (id, uid, auth_token, slug, stack_id, region_slug, cluster_slug, created, updated) + cloud_migration_session (id, uid, org_id, auth_token, slug, stack_id, region_slug, cluster_slug, created, updated) VALUES - (1,'qwerty', ?, '11111', 11111, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), - (2,'asdfgh', ?, '22222', 22222, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), - (3,'zxcvbn', ?, '33333', 33333, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'); + (1,'qwerty', 1, ?, '11111', 11111, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), + (2,'asdfgh', 1, ?, '22222', 22222, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), + (3,'zxcvbn', 1, ?, '33333', 33333, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), + (4,'zxcvbn_org2', 2, ?, '33333', 33333, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'); `, encodeToken("12345"), encodeToken("6789"), encodeToken("777"), + encodeToken("0987"), ) require.NoError(t, err) @@ -380,9 +388,10 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { INSERT INTO cloud_migration_snapshot (session_uid, uid, created, updated, finished, status) VALUES - ('qwerty', 'poiuy', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), + ('qwerty', 'poiuy', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), ('qwerty', 'lkjhg', '2024-03-26 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), - ('zxcvbn', 'mnbvvc', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"); + ('zxcvbn', 'mnbvvc', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), + ('zxcvbn_org2', 'mnbvvc_org2', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"); `, ) require.NoError(t, err) @@ -400,7 +409,8 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { ('mnbvde', 'poiuy', 'DATASOURCE', 'jf38gh', 'OK', ''), ('qwerty', 'poiuy', 'DASHBOARD', 'ejcx4d', 'ERROR', 'fake error'), ('zxcvbn', 'poiuy', 'FOLDER', 'fi39fj', 'PENDING', ''), - ('4fi9sd', '39fi39', 'FOLDER', 'fi39fj', 'OK', ''); + ('4fi9sd', '39fi39', 'FOLDER', 'fi39fj', 'OK', ''), + ('4fi9ee', 'mnbvvc_org2', 'DATASOURCE', 'fi39asd', 'OK', ''); `, ) require.NoError(t, err) diff --git a/pkg/services/cloudmigration/gmsclient/dtos.go b/pkg/services/cloudmigration/gmsclient/dtos.go index 75ff01e16a0..299e6a29edf 100644 --- a/pkg/services/cloudmigration/gmsclient/dtos.go +++ b/pkg/services/cloudmigration/gmsclient/dtos.go @@ -50,6 +50,7 @@ type EventRequestDTO struct { Event LocalEventType `json:"event"` Error string `json:"error"` DurationIfFinished time.Duration `json:"duration"` + UserUID string `json:"userUid"` } type LocalEventType string diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 6fb86ac9e77..635ae12572a 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -21,6 +21,7 @@ var ( // CloudMigrationSession represents a configured migration token type CloudMigrationSession struct { ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` UID string `xorm:"uid"` AuthToken string Slug string @@ -144,6 +145,8 @@ type CloudMigrationRunList struct { type CloudMigrationSessionRequest struct { AuthToken string + // OrgId in the on prem instance + OrgID int64 } type CloudMigrationSessionResponse struct { @@ -159,6 +162,7 @@ type CloudMigrationSessionListResponse struct { type GetSnapshotsQuery struct { SnapshotUID string + OrgID int64 SessionUID string ResultPage int ResultLimit int @@ -166,6 +170,7 @@ type GetSnapshotsQuery struct { type ListSnapshotsQuery struct { SessionUID string + OrgID int64 Page int Limit int Sort string @@ -189,13 +194,14 @@ type Base64EncodedTokenPayload struct { Instance Base64HGInstance } -func (p Base64EncodedTokenPayload) ToMigration() CloudMigrationSession { +func (p Base64EncodedTokenPayload) ToMigration(orgID int64) CloudMigrationSession { return CloudMigrationSession{ AuthToken: p.Token, Slug: p.Instance.Slug, StackID: p.Instance.StackID, RegionSlug: p.Instance.RegionSlug, ClusterSlug: p.Instance.ClusterSlug, + OrgID: orgID, } } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 836bed5b50b..687326cefb8 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1628,6 +1628,13 @@ var ( Stage: FeatureStageExperimental, Owner: identityAccessTeam, }, + { + Name: "crashDetection", + Description: "Enables browser crash detection reporting to Faro.", + Stage: FeatureStageExperimental, + Owner: grafanaObservabilityTracesAndProfilingSquad, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 68295315b36..cac9fde8832 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -217,3 +217,4 @@ exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,fals enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false zipkinBackendMigration,experimental,@grafana/oss-big-tent,false,false,false enableSCIM,experimental,@grafana/identity-access-team,false,false,false +crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 70c0813d282..1656aa935c3 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -878,4 +878,8 @@ const ( // FlagEnableSCIM // Enables SCIM support for user and group management FlagEnableSCIM = "enableSCIM" + + // FlagCrashDetection + // Enables browser crash detection reporting to Faro. + FlagCrashDetection = "crashDetection" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index bde889b2f00..34c1e5e5f78 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -855,6 +855,19 @@ "expression": "true" } }, + { + "metadata": { + "name": "crashDetection", + "resourceVersion": "1730381712885", + "creationTimestamp": "2024-10-31T13:35:12Z" + }, + "spec": { + "description": "Enables browser crash detection reporting to Faro.", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true + } + }, { "metadata": { "name": "dashboardNewLayouts", @@ -3470,4 +3483,4 @@ } } ] -} \ No newline at end of file +} diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 36c6e631b67..3d209aa85b9 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -150,7 +150,12 @@ func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, decryptFn Decrypt return c.Do(req.WithContext(ctx)) } senderLogger := log.New("ngalert.sender.external-alertmanager") - s, err := sender.NewExternalAlertmanagerSender(senderLogger, prometheus.NewRegistry(), sender.WithDoFunc(doFunc)) + s, err := sender.NewExternalAlertmanagerSender( + senderLogger, + prometheus.NewRegistry(), + sender.WithDoFunc(doFunc), + sender.WithUTF8Labels(), + ) if err != nil { return nil, err } diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index b8cb77ef422..7ad086dc7f2 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -785,16 +785,18 @@ func TestIntegrationRemoteAlertmanagerAlerts(t *testing.T) { require.Equal(t, 0, len(alertGroups)) // Let's create two active alerts and one expired one. - alert1 := genAlert(true, map[string]string{"test_1": "test_1", "empty": "", alertingModels.NamespaceUIDLabel: "test_1"}) - alert2 := genAlert(true, map[string]string{"test_2": "test_2", "empty": "", alertingModels.NamespaceUIDLabel: "test_2"}) - alert3 := genAlert(false, map[string]string{"test_3": "test_3", "empty": "", alertingModels.NamespaceUIDLabel: "test_3"}) + // UTF-8 label names should be preserved. + utf8LabelName := "test utf-8 label 😳" + alert1 := genAlert(true, map[string]string{utf8LabelName: "test_1", "empty": "", alertingModels.NamespaceUIDLabel: "test_1"}) + alert2 := genAlert(true, map[string]string{utf8LabelName: "test_2", "empty": "", alertingModels.NamespaceUIDLabel: "test_2"}) + alert3 := genAlert(false, map[string]string{utf8LabelName: "test_3", "empty": "", alertingModels.NamespaceUIDLabel: "test_3"}) postableAlerts := apimodels.PostableAlerts{ PostableAlerts: []amv2.PostableAlert{alert1, alert2, alert3}, } err = am.PutAlerts(context.Background(), postableAlerts) require.NoError(t, err) - // We should have two alerts and one group now. + // We should eventually have two active alerts. require.Eventually(t, func() bool { alerts, err = am.GetAlerts(context.Background(), true, true, true, []string{}, "") require.NoError(t, err) @@ -806,15 +808,15 @@ func TestIntegrationRemoteAlertmanagerAlerts(t *testing.T) { require.Equal(t, 1, len(alertGroups)) // Labels with empty values and the namespace UID label should be removed. + // UTF-8 label names should remain unchanged. for _, a := range alertGroups { - require.NotContains(t, a.Labels, "empty") - require.NotContains(t, a.Labels, alertingModels.NamespaceUIDLabel) + require.Len(t, a.Alerts, 2) + for _, a := range a.Alerts { + require.NotContains(t, a.Labels, "empty") + require.NotContains(t, a.Labels, alertingModels.NamespaceUIDLabel) + require.Contains(t, a.Labels, utf8LabelName) + } } - - // Filtering by `test_1=test_1` should return one alert. - alerts, err = am.GetAlerts(context.Background(), true, true, true, []string{"test_1=test_1"}, "") - require.NoError(t, err) - require.Equal(t, 1, len(alerts)) } func TestIntegrationRemoteAlertmanagerReceivers(t *testing.T) { diff --git a/pkg/services/ngalert/sender/sender.go b/pkg/services/ngalert/sender/sender.go index 2553e124780..8f4cd77df47 100644 --- a/pkg/services/ngalert/sender/sender.go +++ b/pkg/services/ngalert/sender/sender.go @@ -37,8 +37,9 @@ type ExternalAlertmanager struct { manager *Manager - sdCancel context.CancelFunc - sdManager *discovery.Manager + sanitizeLabelSetFn func(lbls models.LabelSet) labels.Labels + sdCancel context.CancelFunc + sdManager *discovery.Manager } type ExternalAMcfg struct { @@ -57,6 +58,20 @@ func WithDoFunc(doFunc doFunc) Option { } } +// WithUTF8Labels skips sanitizing labels and annotations before sending alerts to the external Alertmanager(s). +// It assumes UTF-8 label names are supported by the Alertmanager(s). +func WithUTF8Labels() Option { + return func(s *ExternalAlertmanager) { + s.sanitizeLabelSetFn = func(lbls models.LabelSet) labels.Labels { + ls := make(labels.Labels, 0, len(lbls)) + for k, v := range lbls { + ls = append(ls, labels.Label{Name: k, Value: v}) + } + return ls + } + } +} + func (cfg *ExternalAMcfg) SHA256() string { return asSHA256([]string{cfg.headerString(), cfg.URL}) } @@ -87,6 +102,7 @@ func NewExternalAlertmanagerSender(l log.Logger, reg prometheus.Registerer, opts sdCancel: sdCancel, } + s.sanitizeLabelSetFn = s.sanitizeLabelSet s.manager = NewManager( // Injecting a new registry here means these metrics are not exported. // Once we fix the individual Alertmanager metrics we should fix this scenario too. @@ -240,8 +256,8 @@ func buildNotifierConfig(alertmanagers []ExternalAMcfg) (*config.Config, map[str func (s *ExternalAlertmanager) alertToNotifierAlert(alert models.PostableAlert) *Alert { // Prometheus alertmanager has stricter rules for annotations/labels than grafana's internal alertmanager, so we sanitize invalid keys. return &Alert{ - Labels: s.sanitizeLabelSet(alert.Alert.Labels), - Annotations: s.sanitizeLabelSet(alert.Annotations), + Labels: s.sanitizeLabelSetFn(alert.Alert.Labels), + Annotations: s.sanitizeLabelSetFn(alert.Annotations), StartsAt: time.Time(alert.StartsAt), EndsAt: time.Time(alert.EndsAt), GeneratorURL: alert.Alert.GeneratorURL.String(), diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index a2eb863fddd..76beadd09fc 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -124,16 +124,19 @@ func (st *Manager) Run(ctx context.Context) error { } func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceReader InstanceReader) { + logger := st.log.FromContext(ctx) + if st.instanceStore == nil { - st.log.Info("Skip warming the state because instance store is not configured") + logger.Info("Skip warming the state because instance store is not configured") return } + startTime := time.Now() - st.log.Info("Warming state cache for startup") + logger.Info("Warming state cache for startup") orgIds, err := instanceReader.FetchOrgIds(ctx) if err != nil { - st.log.Error("Unable to fetch orgIds", "error", err) + logger.Error("Unable to fetch orgIds", "error", err) } statesCount := 0 @@ -144,7 +147,7 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea } alertRules, err := rulesReader.ListAlertRules(ctx, &ruleCmd) if err != nil { - st.log.Error("Unable to fetch previous state", "error", err) + logger.Error("Unable to fetch previous state", "error", err) } ruleByUID := make(map[string]*ngModels.AlertRule, len(alertRules)) @@ -158,7 +161,7 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea // We will not enforce this here, but it's convenient to emit the warning here as we load up all the rules. for name, size := range groupSizes { if st.rulesPerRuleGroupLimit > 0 && size > st.rulesPerRuleGroupLimit { - st.log.Warn( + logger.Warn( "Large rule group was loaded. Large groups are discouraged and changes to them may be disallowed in the future.", "limit", st.rulesPerRuleGroupLimit, "actual", size, @@ -173,7 +176,7 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea } alertInstances, err := instanceReader.ListAlertInstances(ctx, &cmd) if err != nil { - st.log.Error("Unable to fetch previous state", "error", err) + logger.Error("Unable to fetch previous state", "error", err) } for _, entry := range alertInstances { @@ -195,7 +198,7 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea if entry.ResultFingerprint != "" { fp, err := strconv.ParseUint(entry.ResultFingerprint, 16, 64) if err != nil { - st.log.Error("Failed to parse result fingerprint of alert instance", "error", err, "ruleUID", entry.RuleUID) + logger.Error("Failed to parse result fingerprint of alert instance", "error", err, "ruleUID", entry.RuleUID) } resultFp = data.Fingerprint(fp) } @@ -215,12 +218,12 @@ func (st *Manager) Warm(ctx context.Context, rulesReader RuleReader, instanceRea ResolvedAt: entry.ResolvedAt, LastSentAt: entry.LastSentAt, } - st.cache.getOrAdd(state, st.log) + st.cache.getOrAdd(state, logger) statesCount++ } } - st.log.Info("State cache has been initialized", "states", statesCount, "duration", time.Since(startTime)) + logger.Info("State cache has been initialized", "states", statesCount, "duration", time.Since(startTime)) } func (st *Manager) Get(orgID int64, alertRuleUID string, stateId data.Fingerprint) *State { diff --git a/pkg/services/sqlstore/migrations/cloud_migrations.go b/pkg/services/sqlstore/migrations/cloud_migrations.go index b7d5a84d6bb..18dc3590570 100644 --- a/pkg/services/sqlstore/migrations/cloud_migrations.go +++ b/pkg/services/sqlstore/migrations/cloud_migrations.go @@ -66,7 +66,7 @@ func addCloudMigrationsMigrations(mg *Migrator) { })) // --- v2 - asynchronous workflow refactor - sessionTable := Table{ + migrationSessionTable := Table{ Name: "cloud_migration_session", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, @@ -99,7 +99,7 @@ func addCloudMigrationsMigrations(mg *Migrator) { }, } - addTableReplaceMigrations(mg, migrationTable, sessionTable, 2, map[string]string{ + addTableReplaceMigrations(mg, migrationTable, migrationSessionTable, 2, map[string]string{ "id": "id", "uid": "uid", "auth_token": "auth_token", @@ -171,6 +171,11 @@ func addCloudMigrationsMigrations(mg *Migrator) { Nullable: true, })) + // -- Adds org_id column for for all elements - defaults to 1 (default org) + mg.AddMigration("add cloud_migration_session.org_id column", NewAddColumnMigration(migrationSessionTable, &Column{ + Name: "org_id", Type: DB_BigInt, Nullable: false, Default: "1", + })) + mg.AddMigration("add cloud_migration_resource.error_code column", NewAddColumnMigration(migrationResourceTable, &Column{ Name: "error_code", Type: DB_Text, diff --git a/pkg/services/ssosettings/strategies/saml_strategy.go b/pkg/services/ssosettings/strategies/saml_strategy.go index 740d86ee2c0..026fd5cef7f 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), + "entity_id": section.KeyValue("entity_id").MustString(""), "name": section.KeyValue("name").MustString("SAML"), "single_logout": section.KeyValue("single_logout").MustBool(false), "allow_sign_up": section.KeyValue("allow_sign_up").MustBool(false), diff --git a/pkg/services/ssosettings/strategies/saml_strategy_test.go b/pkg/services/ssosettings/strategies/saml_strategy_test.go index d076f708400..c04ef280318 100644 --- a/pkg/services/ssosettings/strategies/saml_strategy_test.go +++ b/pkg/services/ssosettings/strategies/saml_strategy_test.go @@ -15,6 +15,7 @@ var ( iniSAMLContent = ` [auth.saml] enabled = true + entity_id = custom-entity-id single_logout = true name = "SAML Test" allow_sign_up = true @@ -54,6 +55,7 @@ var ( expectedSAMLInfo = map[string]any{ "enabled": true, + "entity_id": "custom-entity-id", "single_logout": true, "allow_sign_up": true, "auto_login": true, diff --git a/pkg/storage/unified/apistore/prepare.go b/pkg/storage/unified/apistore/prepare.go index ed379fd73c9..9cf7739ad81 100644 --- a/pkg/storage/unified/apistore/prepare.go +++ b/pkg/storage/unified/apistore/prepare.go @@ -7,7 +7,9 @@ import ( "math" "time" + "github.com/google/uuid" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/storage" "k8s.io/klog/v2" @@ -50,6 +52,9 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime if obj.GetResourceVersion() != "" { return nil, storage.ErrResourceVersionSetOnCreate } + if obj.GetUID() == "" { + obj.SetUID(types.UID(uuid.NewString())) + } obj.SetGenerateName("") // Clear the random name field obj.SetResourceVersion("") @@ -95,7 +100,9 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti if previous.GetUID() == "" { klog.Errorf("object is missing UID: %s, %s", obj.GetGroupVersionKind().String(), obj.GetName()) } else if obj.GetUID() != previous.GetUID() { - klog.Errorf("object UID mismatch: %s, was:%s, now: %s", obj.GetGroupVersionKind().String(), previous.GetName(), obj.GetUID()) + if obj.GetUID() != "" { + klog.Errorf("object UID mismatch: %s, was:%s, now: %s", obj.GetGroupVersionKind().String(), previous.GetName(), obj.GetUID()) + } obj.SetUID(previous.GetUID()) } diff --git a/pkg/storage/unified/resource/access.go b/pkg/storage/unified/resource/access.go new file mode 100644 index 00000000000..af6f2582d33 --- /dev/null +++ b/pkg/storage/unified/resource/access.go @@ -0,0 +1,26 @@ +package resource + +import ( + "context" + + "github.com/grafana/authlib/authz" + "github.com/grafana/authlib/claims" +) + +type staticAuthzClient struct { + allowed bool +} + +// Check implements authz.AccessClient. +func (c *staticAuthzClient) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) { + return authz.CheckResponse{Allowed: c.allowed}, nil +} + +// Compile implements authz.AccessClient. +func (c *staticAuthzClient) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) { + return func(namespace string, name, folder string) bool { + return c.allowed + }, nil +} + +var _ authz.AccessClient = &staticAuthzClient{} diff --git a/pkg/storage/unified/resource/hooks.go b/pkg/storage/unified/resource/hooks.go index 605700791f9..b7160c0fc35 100644 --- a/pkg/storage/unified/resource/hooks.go +++ b/pkg/storage/unified/resource/hooks.go @@ -8,10 +8,6 @@ import ( ) type WriteAccessHooks struct { - // Check if a user has access to write folders - // When this is nil, no resources can have folders configured - Folder func(ctx context.Context, user claims.AuthInfo, uid string) bool - // When configured, this will make sure a user is allowed to save to a given origin Origin func(ctx context.Context, user claims.AuthInfo, origin string) bool } @@ -24,16 +20,6 @@ type LifecycleHooks interface { Stop(context.Context) error } -func (a *WriteAccessHooks) CanWriteFolder(ctx context.Context, user claims.AuthInfo, uid string) error { - if a.Folder == nil { - return fmt.Errorf("writing folders is not supported") - } - if !a.Folder(ctx, user, uid) { - return fmt.Errorf("not allowed to write resource to folder") - } - return nil -} - func (a *WriteAccessHooks) CanWriteOrigin(ctx context.Context, user claims.AuthInfo, uid string) error { if a.Origin == nil || uid == "UI" { return nil // default to OK diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 316bd75e471..97549b19dba 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -17,6 +17,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/grafana/authlib/authz" "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -140,7 +141,10 @@ type ResourceServerOptions struct { // Check if a user has access to write folders // When this is nil, no resources can have folders configured - WriteAccess WriteAccessHooks + WriteHooks WriteAccessHooks + + // Link RBAC + AccessClient authz.AccessClient // Callbacks for startup and shutdown Lifecycle LifecycleHooks @@ -161,6 +165,10 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { return nil, fmt.Errorf("missing Backend implementation") } + if opts.AccessClient == nil { + opts.AccessClient = &staticAuthzClient{allowed: true} // everything OK + } + if opts.Diagnostics == nil { opts.Diagnostics = &noopService{} } @@ -209,7 +217,8 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { index: opts.Index, blob: blobstore, diagnostics: opts.Diagnostics, - access: opts.WriteAccess, + access: opts.AccessClient, + writeHooks: opts.WriteHooks, lifecycle: opts.Lifecycle, now: opts.Now, ctx: ctx, @@ -228,7 +237,8 @@ type server struct { blob BlobSupport index ResourceIndexServer diagnostics DiagnosticsServer - access WriteAccessHooks + access authz.AccessClient + writeHooks WriteAccessHooks lifecycle LifecycleHooks now func() int64 mostRecentRV atomic.Int64 // The most recent resource version seen by the server @@ -303,6 +313,8 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour } if obj.GetUID() == "" { + // TODO! once https://github.com/grafana/grafana/pull/96086 is deployed everywhere + // return nil, NewBadRequestError("object is missing UID") s.log.Error("object is missing UID", "key", key) } @@ -310,6 +322,13 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour s.log.Error("object must not include a resource version", "key", key) } + check := authz.CheckRequest{ + Verb: "create", + Group: key.Group, + Resource: key.Resource, + Namespace: key.Namespace, + } + event := &WriteEvent{ Value: value, Key: key, @@ -319,6 +338,7 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour event.Type = WatchEvent_ADDED } else { event.Type = WatchEvent_MODIFIED + check.Verb = "update" temp := &unstructured.Unstructured{} err = temp.UnmarshalJSON(oldValue) @@ -362,19 +382,24 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour return nil, err } - folder := obj.GetFolder() - if folder != "" { - err = s.access.CanWriteFolder(ctx, user, folder) - if err != nil { - return nil, AsErrorResult(err) + check.Folder = obj.GetFolder() + check.Name = key.Name + a, err := s.access.Check(ctx, user, check) + if err != nil { + return nil, AsErrorResult(err) + } + if !a.Allowed { + return nil, &ErrorResult{ + Code: http.StatusForbidden, } } + origin, err := obj.GetOriginInfo() if err != nil { return nil, NewBadRequestError("invalid origin info") } if origin != nil { - err = s.access.CanWriteOrigin(ctx, user, origin.Name) + err = s.writeHooks.CanWriteOrigin(ctx, user, origin.Name) if err != nil { return nil, AsErrorResult(err) } @@ -404,7 +429,7 @@ func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateRespons if found != nil && len(found.Value) > 0 { rsp.Error = &ErrorResult{ Code: http.StatusConflict, - Message: "key already exists", + Message: "key already exists", // TODO?? soft delete replace? } return rsp, nil } @@ -414,6 +439,7 @@ func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateRespons rsp.Error = e return rsp, nil } + var err error rsp.ResourceVersion, err = s.backend.WriteEvent(ctx, *event) if err != nil { @@ -489,6 +515,14 @@ func (s *server) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRespons if req.ResourceVersion < 0 { return nil, apierrors.NewBadRequest("update must include the previous version") } + user, ok := claims.From(ctx) + if !ok || user == nil { + rsp.Error = &ErrorResult{ + Message: "no user found in context", + Code: http.StatusUnauthorized, + } + return rsp, nil + } latest := s.backend.ReadResource(ctx, &ReadRequest{ Key: req.Key, @@ -502,6 +536,25 @@ func (s *server) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRespons return rsp, nil } + access, err := s.access.Check(ctx, user, authz.CheckRequest{ + Verb: "delete", + Group: req.Key.Group, + Resource: req.Key.Resource, + Namespace: req.Key.Namespace, + Name: req.Key.Name, + Folder: latest.Folder, + }) + if err != nil { + rsp.Error = AsErrorResult(err) + return rsp, nil + } + if !access.Allowed { + rsp.Error = &ErrorResult{ + Code: http.StatusForbidden, + } + return rsp, nil + } + now := metav1.NewTime(time.UnixMilli(s.now())) event := WriteEvent{ Key: req.Key, @@ -513,7 +566,7 @@ func (s *server) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRespons return nil, apierrors.NewBadRequest("unable to get user") } marker := &DeletedMarker{} - err := json.Unmarshal(latest.Value, marker) + err = json.Unmarshal(latest.Value, marker) if err != nil { return nil, apierrors.NewBadRequest( fmt.Sprintf("unable to read previous object, %v", err)) @@ -549,6 +602,14 @@ func (s *server) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, err if err := s.Init(ctx); err != nil { return nil, err } + user, ok := claims.From(ctx) + if !ok || user == nil { + return &ReadResponse{ + Error: &ErrorResult{ + Message: "no user found in context", + Code: http.StatusUnauthorized, + }}, nil + } // if req.Key.Group == "" { // status, _ := AsErrorResult(apierrors.NewBadRequest("missing group")) @@ -559,7 +620,24 @@ func (s *server) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, err } rsp := s.backend.ReadResource(ctx, req) - // TODO, check folder permissions etc + + a, err := s.access.Check(ctx, user, authz.CheckRequest{ + Verb: "get", + Group: req.Key.Group, + Resource: req.Key.Resource, + Namespace: req.Key.Namespace, + Name: req.Key.Name, + Folder: rsp.Folder, + }) + if err != nil { + return &ReadResponse{Error: AsErrorResult(err)}, nil + } + if !a.Allowed { + return &ReadResponse{ + Error: &ErrorResult{ + Code: http.StatusForbidden, + }}, nil + } return &ReadResponse{ ResourceVersion: rsp.ResourceVersion, Value: rsp.Value, @@ -571,6 +649,15 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err ctx, span := s.tracer.Start(ctx, "storage_server.List") defer span.End() + user, ok := claims.From(ctx) + if !ok || user == nil { + return &ListResponse{ + Error: &ErrorResult{ + Message: "no user found in context", + Code: http.StatusUnauthorized, + }}, nil + } + if err := s.Init(ctx); err != nil { return nil, err } @@ -580,19 +667,37 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err maxPageBytes := 1024 * 1024 * 2 // 2mb/page pageBytes := 0 rsp := &ListResponse{} + + key := req.Options.Key + checker, err := s.access.Compile(ctx, user, authz.ListRequest{ + Group: key.Group, + Resource: key.Resource, + Namespace: key.Namespace, + }) + if err != nil { + return &ListResponse{Error: AsErrorResult(err)}, nil + } + if checker == nil { + return &ListResponse{Error: &ErrorResult{ + Code: http.StatusForbidden, + }}, nil + } + rv, err := s.backend.ListIterator(ctx, req, func(iter ListIterator) error { for iter.Next() { if err := iter.Error(); err != nil { return err } - // TODO: add authz filters - item := &ResourceWrapper{ ResourceVersion: iter.ResourceVersion(), Value: iter.Value(), } + if !checker(iter.Namespace(), iter.Name(), iter.Folder()) { + continue + } + pageBytes += len(item.Value) rsp.Items = append(rsp.Items, item) if len(rsp.Items) >= int(req.Limit) || pageBytes >= maxPageBytes { @@ -642,6 +747,7 @@ func (s *server) initWatcher() error { return err } +//nolint:gocyclo func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { ctx := srv.Context() @@ -649,6 +755,24 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { return err } + user, ok := claims.From(ctx) + if !ok || user == nil { + return apierrors.NewUnauthorized("no user found in context") + } + + key := req.Options.Key + checker, err := s.access.Compile(ctx, user, authz.ListRequest{ + Group: key.Group, + Resource: key.Resource, + Namespace: key.Namespace, + }) + if err != nil { + return err + } + if checker == nil { + return apierrors.NewUnauthorized("not allowed to list anything") // ?? or a single error? + } + // Start listening -- this will buffer any changes that happen while we backfill. // If events are generated faster than we can process them, then some events will be dropped. // TODO: Think of a way to allow the client to catch up. @@ -722,6 +846,10 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { } s.log.Debug("Server Broadcasting", "type", event.Type, "rv", event.ResourceVersion, "previousRV", event.PreviousRV, "group", event.Key.Group, "namespace", event.Key.Namespace, "resource", event.Key.Resource, "name", event.Key.Name) if event.ResourceVersion > since && matchesQueryKey(req.Options.Key, event.Key) { + if !checker(event.Key.Namespace, event.Key.Name, event.Folder) { + continue + } + value := event.Value // remove the delete marker stored in the value for deleted objects if event.Type == WatchEvent_DELETED { diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 4d93e18bc1b..cc54f4ab6cc 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -8,13 +8,14 @@ import ( "testing" "time" - "github.com/grafana/authlib/claims" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/stretchr/testify/require" "gocloud.dev/blob/fileblob" "gocloud.dev/blob/memblob" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" ) func TestSimpleServer(t *testing.T) { @@ -56,6 +57,7 @@ func TestSimpleServer(t *testing.T) { "kind": "Playlist", "metadata": { "name": "fdgsv37qslr0ga", + "uid": "xyz", "namespace": "default", "annotations": { "grafana.app/originName": "elsewhere", @@ -172,6 +174,7 @@ func TestSimpleServer(t *testing.T) { "metadata": { "name": "fdgsv37qslr0ga", "namespace": "default", + "uid": "xyz", "annotations": { "grafana.app/originName": "elsewhere", "grafana.app/originPath": "path/to/item", diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index 7e89816cda7..e1b730d4744 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -5,14 +5,16 @@ import ( "os" "strings" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" infraDB "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" - "github.com/prometheus/client_golang/prometheus" ) // Creates a new ResourceServer @@ -48,17 +50,6 @@ func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, fea opts.Diagnostics = store opts.Lifecycle = store - if features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) { - opts.WriteAccess = resource.WriteAccessHooks{ - Folder: func(ctx context.Context, user claims.AuthInfo, uid string) bool { - // #TODO build on the logic here - // #TODO only enable write access when the resource being written in the folder - // is another folder - return true - }, - } - } - if features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) { opts.Index = resource.NewResourceIndexServer(cfg, tracer) } @@ -70,6 +61,22 @@ func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, fea // Initialize the indexer if one is configured if opts.Index != nil { + // TODO: Create a proper identity for the indexer + orgId := int64(1) + ctx = identity.WithRequester(ctx, &identity.StaticRequester{ + Type: claims.TypeServiceAccount, // system:apiserver + UserID: 1, + OrgID: int64(1), + Name: "admin", + Login: "admin", + OrgRole: identity.RoleAdmin, + IsGrafanaAdmin: true, + Permissions: map[int64]map[string][]string{ + orgId: { + "*": {"*"}, // all resources, all scopes + }, + }, + }) _, err = rs.(resource.ResourceIndexer).Index(ctx) if err != nil { return nil, err diff --git a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go index c52aa3a1670..454f6830172 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go @@ -1,10 +1,12 @@ package azuremonitor import ( + "encoding/json" "fmt" "io" "net/http" "net/url" + "regexp" "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -27,16 +29,46 @@ type httpServiceProxy struct { logger log.Logger } +func (s *httpServiceProxy) writeErrorResponse(rw http.ResponseWriter, statusCode int, message string) error { + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(statusCode) + + // Attempt to locate JSON portion in error message + re := regexp.MustCompile(`\{.*?\}`) + jsonPart := re.FindString(message) + + var jsonData map[string]interface{} + if unmarshalErr := json.Unmarshal([]byte(jsonPart), &jsonData); unmarshalErr != nil { + errorMsg, _ := json.Marshal(map[string]string{"error": "Invalid JSON format in error message"}) + _, err := rw.Write(errorMsg) + if err != nil { + return fmt.Errorf("unable to write HTTP response: %v", err) + } + return unmarshalErr + } + + // Extract relevant fields for a formatted error message + errorType, _ := jsonData["error"].(string) + errorDescription, _ := jsonData["error_description"].(string) + if errorType == "" { + errorType = "UnknownError" + } + formattedError := fmt.Sprintf("%s: %s", errorType, errorDescription) + + errorMsg, _ := json.Marshal(map[string]string{"error": formattedError}) + _, err := rw.Write(errorMsg) + if err != nil { + return fmt.Errorf("unable to write HTTP response: %v", err) + } + return nil +} + func (s *httpServiceProxy) Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error) { res, err := cli.Do(req) if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - _, err = rw.Write([]byte(fmt.Sprintf("unexpected error %v", err))) - if err != nil { - return nil, fmt.Errorf("unable to write HTTP response: %v", err) - } - return nil, err + return nil, s.writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error: %v", err)) } + defer func() { if err := res.Body.Close(); err != nil { s.logger.Warn("Failed to close response body", "err", err) @@ -52,6 +84,7 @@ func (s *httpServiceProxy) Do(rw http.ResponseWriter, req *http.Request, cli *ht } return nil, err } + rw.WriteHeader(res.StatusCode) _, err = rw.Write(body) if err != nil { @@ -64,7 +97,8 @@ func (s *httpServiceProxy) Do(rw http.ResponseWriter, req *http.Request, cli *ht rw.Header().Add(k, v) } } - // Returning the response write for testing purposes + + // Return the ResponseWriter for testing purposes return rw, nil } diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index a290c73ce1b..72ca44fe673 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "regexp" + "strings" "sync" "time" @@ -131,13 +132,30 @@ func callResource(ctx context.Context, req *backend.CallResourceRequest, sender api := newLokiAPI(dsInfo.HTTPClient, dsInfo.URL, plog, tracer, false) - rawLokiResponse, err := api.RawQuery(ctx, lokiURL) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - plog.Error("Failed resource call from loki", "err", err, "url", lokiURL) - return err + var rawLokiResponse RawLokiResponse + var err error + + // suggestions is a resource endpoint that will return label and label value suggestions based + // on queries and the existing scope. By moving this to the backend we can use the logql parser to + // rewrite queries safely. + if req.Method == http.MethodPost && strings.EqualFold(req.Path, "suggestions") { + rawLokiResponse, err = GetSuggestions(ctx, api, req) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + plog.FromContext(ctx).Error("Failed to get suggestions from loki", "err", err) + return err + } + } else { + rawLokiResponse, err = api.RawQuery(ctx, lokiURL) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + plog.Error("Failed resource call from loki", "err", err, "url", lokiURL) + return err + } } + respHeaders := map[string][]string{ "content-type": {"application/json"}, } diff --git a/pkg/tsdb/loki/scopes.go b/pkg/tsdb/loki/scopes.go index c946cbb14fc..c9f502a780b 100644 --- a/pkg/tsdb/loki/scopes.go +++ b/pkg/tsdb/loki/scopes.go @@ -1,12 +1,83 @@ package loki import ( + "context" + "encoding/json" "fmt" + "net/url" + "time" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/promlib/models" + "github.com/grafana/grafana/pkg/tsdb/loki/kinds/dataquery" "github.com/grafana/loki/v3/pkg/logql/syntax" + "github.com/prometheus/prometheus/promql/parser" ) +// SuggestionRequest is the request body for the GetSuggestions resource. +type SuggestionRequest struct { + // LabelName, if provided, will result in label values being returned for the given label name. + LabelName string `json:"labelName"` + + Query string `json:"query"` + + Scopes []models.ScopeFilter `json:"scopes"` + AdhocFilters []models.ScopeFilter `json:"adhocFilters"` + + // Start and End are proxied directly to the prometheus endpoint (which is rfc3339 | unix_timestamp) + Start string `json:"start"` + End string `json:"end"` +} + +// GetSuggestions returns label names or label values for the given queries and scopes. +func GetSuggestions(ctx context.Context, lokiAPI *LokiAPI, req *backend.CallResourceRequest) (RawLokiResponse, error) { + sugReq := SuggestionRequest{} + err := json.Unmarshal(req.Body, &sugReq) + if err != nil { + return RawLokiResponse{}, fmt.Errorf("error unmarshalling suggestion request: %v", err) + } + + values := url.Values{} + if sugReq.Query != "" { + // the query is used to find label/labelvalues the duration and interval does not matter. + // If the user want to filter values based on time it should used the `start` and `end` fields + interpolatedQuery := interpolateVariables(sugReq.Query, time.Minute, time.Minute, dataquery.LokiQueryTypeRange, time.Minute) + + if len(sugReq.Scopes) > 0 { + rewrittenQuery, err := ApplyScopes(interpolatedQuery, sugReq.Scopes) + if err == nil { + values.Add("query", rewrittenQuery) + } else { + values.Add("query", interpolatedQuery) + } + } + } else if len(sugReq.Scopes) > 0 { + matchers, err := models.FiltersToMatchers(sugReq.Scopes, sugReq.AdhocFilters) + if err != nil { + return RawLokiResponse{}, fmt.Errorf("error converting filters to matchers: %v", err) + } + vs := parser.VectorSelector{LabelMatchers: matchers} + values.Add("query", vs.String()) + } + + if sugReq.Start != "" { + values.Add("start", sugReq.Start) + } + + if sugReq.End != "" { + values.Add("end", sugReq.End) + } + + var path string + if sugReq.LabelName != "" { + path = "/loki/api/v1/label/" + url.QueryEscape(sugReq.LabelName) + "/values?" + values.Encode() + } else { + path = "/loki/api/v1/labels?" + values.Encode() + } + + return lokiAPI.RawQuery(ctx, path) +} + // ApplyScopes applies the given scope filters to the given raw expression. func ApplyScopes(rawExpr string, scopeFilters []models.ScopeFilter) (string, error) { if len(scopeFilters) == 0 { diff --git a/public/app/app.ts b/public/app/app.ts index 052a3382a58..dad577603d8 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -56,6 +56,7 @@ import { AppChromeService } from './core/components/AppChrome/AppChromeService'; import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry'; import { PluginPage } from './core/components/Page/PluginPage'; import { GrafanaContextType, useChromeHeaderHeight, useReturnToPreviousInternal } from './core/context/GrafanaContext'; +import { initializeCrashDetection } from './core/crash'; import { initIconCache } from './core/icons/iconBundle'; import { initializeI18n } from './core/internationalization'; import { setMonacoEnv } from './core/monacoEnv'; @@ -267,6 +268,10 @@ export class GrafanaApp { initializeScopes(); + if (config.featureToggles.crashDetection) { + initializeCrashDetection(); + } + const root = createRoot(document.getElementById('reactRoot')!); root.render( createElement(AppWrapper, { diff --git a/public/app/core/components/RolePicker/RolePickerMenu.tsx b/public/app/core/components/RolePicker/RolePickerMenu.tsx index ed68be79f07..193a5062a8f 100644 --- a/public/app/core/components/RolePicker/RolePickerMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerMenu.tsx @@ -1,8 +1,9 @@ import { css, cx } from '@emotion/css'; import { useEffect, useRef, useState } from 'react'; -import { Button, CustomScrollbar, Stack, TextLink, useStyles2, useTheme2 } from '@grafana/ui'; +import { Button, Stack, TextLink, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { OrgRole, Role } from 'app/types'; import { BuiltinRoleSelector } from './BuiltinRoleSelector'; @@ -217,14 +218,11 @@ export const RolePickerMenu = ({ )} >
- {showBasicRole && (
@@ -254,7 +252,7 @@ export const RolePickerMenu = ({ showOnLeftSubMenu={menuLeft} /> ))} - +
); } @@ -128,8 +129,5 @@ function getStyles(theme: GrafanaTheme2) { flexShrink: 0, paddingLeft: theme.spacing(2), }), - scroll: css({ - background: theme.colors.background.primary, - }), }; } diff --git a/public/app/features/dashboard/components/AddWidgetModal/AddWidgetModal.tsx b/public/app/features/dashboard/components/AddWidgetModal/AddWidgetModal.tsx deleted file mode 100644 index 85572494816..00000000000 --- a/public/app/features/dashboard/components/AddWidgetModal/AddWidgetModal.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { css } from '@emotion/css'; -import { useMemo, useState } from 'react'; - -import { GrafanaTheme2, PanelPluginMeta } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; -import { CustomScrollbar, Icon, Input, Modal, useStyles2 } from '@grafana/ui'; -import { onCreateNewWidgetPanel } from 'app/features/dashboard/utils/dashboard'; -import { VizTypePickerPlugin } from 'app/features/panel/components/VizTypePicker/VizTypePickerPlugin'; -import { filterPluginList, getWidgetPluginMeta } from 'app/features/panel/state/util'; -import { useSelector } from 'app/types'; - -export const AddWidgetModal = () => { - const styles = useStyles2(getStyles); - const [searchQuery, setSearchQuery] = useState(''); - const dashboard = useSelector((state) => state.dashboard.getModel()); - - const widgetsList: PanelPluginMeta[] = useMemo(() => { - return getWidgetPluginMeta(); - }, []); - - const filteredWidgetsTypes = useMemo((): PanelPluginMeta[] => { - return filterPluginList(widgetsList, searchQuery); - }, [widgetsList, searchQuery]); - - const onDismiss = () => { - locationService.partial({ addWidget: null }); - }; - - return ( - - } - placeholder="Search widget" - onChange={(e) => { - setSearchQuery(e.currentTarget.value); - }} - /> - -
- {filteredWidgetsTypes.map((plugin, index) => ( - { - const id = onCreateNewWidgetPanel(dashboard!, plugin.id); - locationService.partial({ editPanel: id, addWidget: null }); - }} - /> - ))} -
-
-
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - modal: css({ - width: '65%', - maxWidth: '960px', - [theme.breakpoints.down('md')]: { - width: '100%', - }, - }), - searchInput: css({ - marginBottom: theme.spacing(2), - }), - grid: css({ - display: 'grid', - gridGap: theme.spacing(1), - }), -}); diff --git a/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx b/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx index 76946da2697..8de5f5c24cf 100644 --- a/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx +++ b/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx @@ -6,8 +6,9 @@ import { GrafanaTheme2, ScopedVars } from '@grafana/data'; import { sanitize, sanitizeUrl } from '@grafana/data/src/text/sanitize'; import { selectors } from '@grafana/e2e-selectors'; import { DashboardLink } from '@grafana/schema'; -import { CustomScrollbar, Dropdown, Icon, Button, Menu, useStyles2 } from '@grafana/ui'; +import { Dropdown, Icon, Button, Menu, useStyles2 } from '@grafana/ui'; import { ButtonLinkProps, LinkButton } from '@grafana/ui/src/components/Button'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { DashboardSearchItem } from 'app/features/search/types'; @@ -36,7 +37,7 @@ function DashboardLinksMenu({ dashboardUID, link }: DashboardLinksMenuProps) { return (
- + {resolvedLinks.map((resolvedLink, index) => { return ( ); })} - +
); @@ -157,7 +158,6 @@ function getStyles(theme: GrafanaTheme2) { dropdown: css({ maxWidth: 'max(30vw, 300px)', maxHeight: '70vh', - overflowY: 'auto', }), button: css({ color: theme.colors.text.primary, diff --git a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx index 6bdcb492fc5..e0fc9a77e3c 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx @@ -118,7 +118,7 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E if (pageId === PluginTabIds.USAGE && pluginConfig) { return ( -
+
); @@ -140,6 +140,10 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E } export const getStyles = (theme: GrafanaTheme2) => ({ + wrap: css({ + width: '100%', + height: '50vh', + }), readme: css({ '& img': { maxWidth: '100%', diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx index 8aca36f54fa..e11f83c2c89 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx @@ -115,6 +115,7 @@ export const getStyles = (theme: GrafanaTheme2) => { // Needed due to block formatting context tabContent: css({ paddingLeft: '5px', + width: '100%', }), }; }; diff --git a/public/app/features/plugins/admin/components/PluginUsage.tsx b/public/app/features/plugins/admin/components/PluginUsage.tsx index 5bee5f99d45..162986160fe 100644 --- a/public/app/features/plugins/admin/components/PluginUsage.tsx +++ b/public/app/features/plugins/admin/components/PluginUsage.tsx @@ -88,7 +88,7 @@ export const getStyles = (theme: GrafanaTheme2) => { return { wrap: css({ width: '100%', - height: '100%', + height: '90%', }), info: css({ paddingBottom: '30px', diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx index e052b0aa128..38511a2dfea 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { Provider } from 'react-redux'; +import { config } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; import { Version } from '../types'; @@ -8,6 +9,13 @@ import { Version } from '../types'; import { VersionInstallButton } from './VersionInstallButton'; describe('VersionInstallButton', () => { + const originalConfig = { ...config }; + afterEach(() => { + config.featureToggles = { + ...originalConfig.featureToggles, + }; + config.pluginCatalogPreinstalledPlugins = originalConfig.pluginCatalogPreinstalledPlugins; + }); it('should show install when no version is installed', () => { const version: Version = { version: '', @@ -103,6 +111,72 @@ describe('VersionInstallButton', () => { const el = screen.getByText('Installed'); expect(el).toBeVisible(); }); + + it('should hide the upgrade button if preinstalled and pinned', () => { + const version: Version = { + version: '1.0.1', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.0'; + config.featureToggles.preinstallAutoUpdate = true; + config.pluginCatalogPreinstalledPlugins = [{ id: 'test', version: '1.0.0' }]; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Upgrade')).not.toBeVisible(); + }); + + it('should hide the downgrade button if preinstalled and pinned', () => { + const version: Version = { + version: '1.0.0', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.1'; + config.featureToggles.preinstallAutoUpdate = true; + config.pluginCatalogPreinstalledPlugins = [{ id: 'test', version: '1.0.1' }]; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Downgrade')).not.toBeVisible(); + }); + + it('should hide the downgrade button if preinstalled', () => { + const version: Version = { + version: '1.0.0', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.1'; + config.featureToggles.preinstallAutoUpdate = true; + config.pluginCatalogPreinstalledPlugins = [{ id: 'test', version: '' }]; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Downgrade')).not.toBeVisible(); + }); }); function renderWithStore(component: JSX.Element) { diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.tsx index 46117ca6d96..a26973b704c 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.tsx @@ -3,10 +3,11 @@ import { useEffect, useState } from 'react'; import { gt } from 'semver'; import { GrafanaTheme2 } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { Badge, Button, ConfirmModal, Icon, Spinner, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; +import { isPreinstalledPlugin } from '../helpers'; import { useInstall } from '../state/hooks'; import { Version } from '../types'; @@ -89,11 +90,22 @@ export const VersionInstallButton = ({ }; let label = 'Downgrade'; + let hidden = false; + const isPreinstalled = isPreinstalledPlugin(pluginId); if (!installedVersion) { label = 'Install'; } else if (gt(version.version, installedVersion)) { label = 'Upgrade'; + if (isPreinstalled.withVersion) { + // Hide button if the plugin is preinstalled with a specific version + hidden = true; + } + } else { + if (isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate)) { + // Hide the downgrade button if the plugin is preinstalled since it will be auto-updated + hidden = true; + } } return ( @@ -106,6 +118,7 @@ export const VersionInstallButton = ({ variant={latestCompatibleVersion === version.version ? 'primary' : 'secondary'} onClick={onInstallClick} className={styles.button} + hidden={hidden} > {label} {isInstalling ? : getIcon(label)} diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts index 9ff282baef8..a0c0121ee0f 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts @@ -7,6 +7,7 @@ import { shouldLoadPluginInFrontendSandbox, setSandboxEnabledCheck, isPluginFrontendSandboxEnabled, + isPluginFrontendSandboxEligible, } from './sandbox_plugin_loader_registry'; jest.mock('@grafana/runtime', () => ({ @@ -89,4 +90,11 @@ describe('Sandbox eligibility checks', () => { expect(customCheck).toHaveBeenCalledWith({ pluginId: 'test-plugin' }); expect(result).toBe(false); }); + + test('isPluginFrontendSandboxEligible returns false for plugins with internal signature', async () => { + //@ts-expect-error We don't publicly export the internal signature + getPluginSettingsMock.mockResolvedValue({ ...fakePlugin, signature: 'internal' }); + const result = await isPluginFrontendSandboxEligible({ pluginId: 'test-plugin' }); + expect(result).toBe(false); + }); }); diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts index 42848ce7863..9d94a3556e0 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts @@ -64,7 +64,7 @@ export async function isPluginFrontendSandboxEligible({ try { // don't run grafana-signed plugins in sandbox const pluginMeta = await getPluginSettings(pluginId, { showErrorAlert: false }); - if (pluginMeta.signatureType === PluginSignatureType.grafana) { + if (pluginMeta.signatureType === PluginSignatureType.grafana || pluginMeta.signature === 'internal') { return false; } } catch (e) { diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index affb0328aaf..e37977e3a45 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -16,7 +16,8 @@ import { import { selectors } from '@grafana/e2e-selectors'; import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; -import { Button, CustomScrollbar, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui'; +import { Button, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import config from 'app/core/config'; import { backendSrv } from 'app/core/services/backend_srv'; @@ -331,7 +332,7 @@ export class QueryGroup extends PureComponent { const styles = getStyles(); return ( - +
{this.renderTopSection(styles)} {dsSettings && ( @@ -346,7 +347,7 @@ export class QueryGroup extends PureComponent { )}
-
+ ); } } diff --git a/public/app/features/scopes/internal/ScopesTreeItem.tsx b/public/app/features/scopes/internal/ScopesTreeItem.tsx index 6d310688e74..54dbfdd2cdb 100644 --- a/public/app/features/scopes/internal/ScopesTreeItem.tsx +++ b/public/app/features/scopes/internal/ScopesTreeItem.tsx @@ -2,7 +2,8 @@ import { css, cx } from '@emotion/css'; import { Dictionary } from 'lodash'; import { GrafanaTheme2 } from '@grafana/data'; -import { Checkbox, CustomScrollbar, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui'; +import { Checkbox, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import { t } from 'app/core/internationalization'; import { ScopesTree } from './ScopesTree'; @@ -129,12 +130,12 @@ export function ScopesTreeItem({ if (isLastExpandedNode) { return ( - {children} - + ); } diff --git a/public/app/features/trails/Breakdown/SortByScene.tsx b/public/app/features/trails/Breakdown/SortByScene.tsx index cd9498ff2ed..6282c758f5b 100644 --- a/public/app/features/trails/Breakdown/SortByScene.tsx +++ b/public/app/features/trails/Breakdown/SortByScene.tsx @@ -31,7 +31,7 @@ export class SortByScene extends SceneObjectBase { options: [ { value: 'outliers', - label: 'Outlying Values', + label: 'Outlying series', description: 'Prioritizes values that show distinct behavior from others within the same label', }, { diff --git a/public/app/features/trails/Integrations/logsIntegration.ts b/public/app/features/trails/Integrations/logsIntegration.ts index f86e685bbb1..59f19e22b1c 100644 --- a/public/app/features/trails/Integrations/logsIntegration.ts +++ b/public/app/features/trails/Integrations/logsIntegration.ts @@ -33,7 +33,7 @@ export type ExtractedRecordingRules = { */ async function fetchRecordingRuleGroups(datasourceSettings: DataSourceInstanceSettings) { const recordingRuleUrl = `api/prometheus/${datasourceSettings.uid}/api/v1/rules`; - const recordingRules: BackendSrvRequest = { url: recordingRuleUrl }; + const recordingRules: BackendSrvRequest = { url: recordingRuleUrl, showErrorAlert: false, showSuccessAlert: false }; const { data } = await lastValueFrom< FetchResponse<{ data: { groups: RecordingRuleGroup[] }; diff --git a/public/app/features/transformers/suggestionsInput/SuggestionsInput.tsx b/public/app/features/transformers/suggestionsInput/SuggestionsInput.tsx index 09c9066e9cb..055f58b4731 100644 --- a/public/app/features/transformers/suggestionsInput/SuggestionsInput.tsx +++ b/public/app/features/transformers/suggestionsInput/SuggestionsInput.tsx @@ -4,9 +4,10 @@ import { FormEvent, useCallback, useEffect, useRef, useState } from 'react'; import * as React from 'react'; import { GrafanaTheme2, VariableSuggestion } from '@grafana/data'; -import { CustomScrollbar, FieldValidationMessage, Portal, TextArea, useTheme2 } from '@grafana/ui'; +import { FieldValidationMessage, Portal, TextArea, useTheme2 } from '@grafana/ui'; import { DataLinkSuggestions } from '@grafana/ui/src/components/DataLinks/DataLinkSuggestions'; import { Input } from '@grafana/ui/src/components/Input/Input'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; const modulo = (a: number, n: number) => a - n * Math.floor(a / n); const ERROR_TOOLTIP_OFFSET = 8; @@ -61,6 +62,7 @@ export const SuggestionsInput = ({ const [showingSuggestions, setShowingSuggestions] = useState(false); const [suggestionsIndex, setSuggestionsIndex] = useState(0); const [variableValue, setVariableValue] = useState(value.toString()); + const scrollRef = useRef(null); const [scrollTop, setScrollTop] = useState(0); const [inputHeight, setInputHeight] = useState(0); const [startPos, setStartPos] = useState(0); @@ -70,6 +72,10 @@ export const SuggestionsInput = ({ const inputRef = useRef(); + useEffect(() => { + scrollRef.current?.scrollTo(0, scrollTop); + }, [scrollTop]); + // the order of middleware is important! const middleware = [ flip({ @@ -189,10 +195,10 @@ export const SuggestionsInput = ({ {showingSuggestions && (
- setScrollTop(scrollTop)} + setScrollTop(event.currentTarget.scrollTop ?? 0)} + ref={scrollRef} > {/* This suggestion component has a specialized name, but is rather generalistic in implementation, @@ -205,7 +211,7 @@ export const SuggestionsInput = ({ onClose={() => setShowingSuggestions(false)} activeIndex={suggestionsIndex} /> - +
)} diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx index f415eb7fa56..da536d30376 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx @@ -3,17 +3,8 @@ import { useEffect, useMemo, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { - Button, - Card, - Collapse, - CustomScrollbar, - Field, - Input, - LoadingPlaceholder, - Select, - useStyles2, -} from '@grafana/ui'; +import { Button, Card, Collapse, Field, Input, LoadingPlaceholder, Select, useStyles2 } from '@grafana/ui'; +import { ScrollContainer } from '@grafana/ui/src/unstable'; import AzureLogAnalyticsDatasource from '../../azure_log_analytics/azure_log_analytics_datasource'; import { @@ -171,7 +162,7 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => { return totalQueries; }, 0)}
- + {Object.keys(visibleQueries).map((category: string) => { if (visibleQueries[category]!.length) { return ( @@ -186,14 +177,14 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => { return ( {query.displayName} - + - +
) : ( diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index acb1e3ad106..638e8ab640f 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -19,7 +19,7 @@ "prismjs": "1.29.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0" diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 38d7ca2d9bd..05cc3443b98 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -20,7 +20,7 @@ "prismjs": "1.29.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0" diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 7acdcad9f4c..fe4d3308359 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@grafana/plugin-configs": "11.4.0-pre", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 1362fc24caa..59a588ad818 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -15,7 +15,7 @@ "micro-memoize": "^4.1.2", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index 2ad68c2bf4f..16d3fefee70 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -15,7 +15,7 @@ "logfmt": "^1.3.2", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-window": "1.8.10", "rxjs": "7.8.1", "stream-browserify": "3.0.0", @@ -25,7 +25,7 @@ "devDependencies": { "@grafana/plugin-configs": "workspace:*", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 4f9f6ca3372..1e5158630aa 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -442,9 +442,74 @@ describe('LokiDatasource', () => { expect(res.length).toBe(2); expect(res[0].text).toBe('hello'); expect(res[0].tags).toEqual(['value']); + expect(res[0].time).toEqual(1); expect(res[1].text).toBe('hello 2'); expect(res[1].tags).toEqual(['value2']); + expect(res[1].time).toEqual(2); + }); + + it('should transform the loki dataplane data to annotation response', async () => { + const originalDataplaneState = config.featureToggles.lokiLogsDataplane; + config.featureToggles.lokiLogsDataplane = true; + const testFrame: DataFrame = { + refId: 'A', + fields: [ + { + name: 'timestamp', + type: FieldType.time, + config: {}, + values: [1, 2], + }, + { + name: 'body', + type: FieldType.string, + config: {}, + values: ['hello', 'hello 2'], + }, + { + name: 'labels', + type: FieldType.other, + config: {}, + values: [ + { + label: 'value', + label2: 'value ', + }, + { + label: '', + label2: 'value2', + label3: ' ', + }, + ], + }, + { + name: 'tsNs', + type: FieldType.string, + config: {}, + values: ['1000000', '2000000'], + }, + { + name: 'id', + type: FieldType.string, + config: {}, + values: ['id1', 'id2'], + }, + ], + length: 2, + }; + const res = await getTestContext(testFrame, { stepInterval: '15s' }); + + expect(res.length).toBe(2); + expect(res[0].text).toBe('hello'); + expect(res[0].tags).toEqual(['value']); + expect(res[0].time).toEqual(1); + + expect(res[1].text).toBe('hello 2'); + expect(res[1].tags).toEqual(['value2']); + expect(res[1].time).toEqual(2); + + config.featureToggles.lokiLogsDataplane = originalDataplaneState; }); describe('Formatting', () => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index f661d8d5f39..e146973ccf7 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -1042,8 +1042,12 @@ export class LokiDatasource const annotations: AnnotationEvent[] = []; const splitKeys: string[] = tagKeys.split(',').filter((v: string) => v !== ''); + const isDataplaneLog = config.featureToggles.lokiLogsDataplane; + for (const frame of data) { - const view = new DataFrameView<{ Time: string; Line: string; labels: Labels }>(frame); + const view = new DataFrameView<{ timestamp: string; Time: string; body: string; Line: string; labels: Labels }>( + frame + ); view.forEach((row) => { const { labels } = row; @@ -1063,15 +1067,17 @@ export class LokiDatasource return true; }) - .map(([key, val]) => val); // keep only the label-value + .map(([_, val]) => val); // keep only the label-value // remove duplicates const tags = Array.from(new Set(maybeDuplicatedTags)); + const logLine = isDataplaneLog ? row.body : row.Line; + annotations.push({ - time: new Date(row.Time).valueOf(), + time: isDataplaneLog ? new Date(row.timestamp).valueOf() : new Date(row.Time).valueOf(), title: renderLegendFormat(titleFormat, labels), - text: renderLegendFormat(textFormat, labels) || row.Line, + text: renderLegendFormat(textFormat, labels) || logLine, tags, }); }); diff --git a/public/app/plugins/datasource/loki/querySplitting.test.ts b/public/app/plugins/datasource/loki/querySplitting.test.ts index 9c8326eb767..57f9859ea6c 100644 --- a/public/app/plugins/datasource/loki/querySplitting.test.ts +++ b/public/app/plugins/datasource/loki/querySplitting.test.ts @@ -18,6 +18,10 @@ jest.mock('uuid', () => ({ })); const originalShardingFlagState = config.featureToggles.lokiShardSplitting; +const originalErr = console.error; +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); beforeAll(() => { // @ts-expect-error jest.spyOn(global, 'setTimeout').mockImplementation((callback) => { @@ -28,6 +32,7 @@ beforeAll(() => { afterAll(() => { jest.mocked(global.setTimeout).mockReset(); config.featureToggles.lokiShardSplitting = originalShardingFlagState; + console.error = originalErr; }); describe('runSplitQuery()', () => { diff --git a/public/app/plugins/datasource/loki/responseUtils.ts b/public/app/plugins/datasource/loki/responseUtils.ts index 3701ad0cb35..dcfafdc670a 100644 --- a/public/app/plugins/datasource/loki/responseUtils.ts +++ b/public/app/plugins/datasource/loki/responseUtils.ts @@ -138,9 +138,9 @@ export function isRetriableError(errorResponse: DataQueryResponse) { : (errorResponse.error?.message ?? ''); if (message.includes('timeout')) { return true; - } else if (message.includes('parse error')) { - // If the error is a parse error, we want to signal to stop querying. - throw new Error(message); + } else if (errorResponse.data.length > 0 && errorResponse.data[0].fields.length > 0) { + // Error response but we're receiving data, continue querying. + return false; } - return false; + throw new Error(message); } diff --git a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts index 39dd75ca4f1..06637829cf6 100644 --- a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts +++ b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts @@ -14,6 +14,7 @@ jest.mock('uuid', () => ({ const originalLog = console.log; const originalWarn = console.warn; +const originalErr = console.error; beforeEach(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); jest.spyOn(console, 'warn').mockImplementation(() => {}); @@ -22,6 +23,7 @@ beforeEach(() => { afterAll(() => { console.log = originalLog; console.warn = originalWarn; + console.error = originalErr; }); describe('runShardSplitQuery()', () => { @@ -203,7 +205,7 @@ describe('runShardSplitQuery()', () => { callback(); }); await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => { - expect(datasource.runQuery).toHaveBeenCalledTimes(2); + expect(datasource.runQuery).toHaveBeenCalledTimes(1); }); }); diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 1bb877f9529..691fd413983 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -29,7 +29,7 @@ "prismjs": "1.29.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "semver": "7.6.3", @@ -41,7 +41,7 @@ "devDependencies": { "@grafana/plugin-configs": "11.4.0-pre", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 84bf1925c04..07b1b616b38 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -14,7 +14,7 @@ "lodash": "4.17.21", "react": "18.2.0", "react-dom": "18.2.0", - "react-select": "5.8.2", + "react-select": "5.8.3", "react-use": "17.5.1", "rxjs": "7.8.1", "tslib": "2.7.0" @@ -22,7 +22,7 @@ "devDependencies": { "@grafana/plugin-configs": "workspace:*", "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.1", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index df3769fd029..d6cf592062e 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -419,6 +419,7 @@ export const prepConfig = ({ series, totalSeries, color, orientation, options, t direction: vizOrientation.yDir, distribution: customConfig.scaleDistribution?.type, log: customConfig.scaleDistribution?.log, + decimals: field.config.decimals, }); if (customConfig.axisPlacement !== AxisPlacement.Hidden) { @@ -445,6 +446,7 @@ export const prepConfig = ({ series, totalSeries, color, orientation, options, t tickLabelRotation: vizOrientation.xOri === 1 ? xTickLabelRotation * -1 : 0, theme, grid: { show: customConfig.axisGridShow }, + decimals: field.config.decimals, }; if (customConfig.axisBorderShow) { diff --git a/scripts/drone/services/services.star b/scripts/drone/services/services.star index 6d3044e932a..629f6016157 100644 --- a/scripts/drone/services/services.star +++ b/scripts/drone/services/services.star @@ -57,7 +57,7 @@ def integration_test_services(): "name": "mimir_backend", "image": images["mimir"], "environment": {}, - "commands": ["/bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled"], + "commands": ["/bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled -alertmanager.utf8-strict-mode-enabled"], }, { "name": "redis", diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index 74d4a51ec94..1c87a607abb 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -22,7 +22,7 @@ images = { "plugins_slack": "plugins/slack", "python": "python:3.8", "postgres_alpine": "postgres:12.3-alpine", - "mimir": "grafana/mimir-alpine:r304-3872ccb", + "mimir": "grafana/mimir-alpine:r316-55f47f8", "mysql5": "mysql:5.7.39", "mysql8": "mysql:8.0.32", "redis_alpine": "redis:6.2.11-alpine", diff --git a/yarn.lock b/yarn.lock index 458faf9ef15..7f27e129d50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,55 +82,57 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/code-frame@npm:7.25.9" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0": + version: 7.26.2 + resolution: "@babel/code-frame@npm:7.26.2" dependencies: - "@babel/highlight": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + js-tokens: "npm:^4.0.0" picocolors: "npm:^1.0.0" - checksum: 10/96d69a570d0df82daedeb3d26ca508970bb31de83580c36c9605e7e7c0aae307ae17bc42609363016f0bdab12e991cebca3c02bf10765036b136bfe7281aee9a + checksum: 10/db2c2122af79d31ca916755331bb4bac96feb2b334cdaca5097a6b467fdd41963b89b14b6836a14f083de7ff887fc78fa1b3c10b14e743d33e12dbfe5ee3d223 languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/compat-data@npm:7.25.9" - checksum: 10/76d06c56e1d1ab661dc90870d70d950c7df5514d2abfb115387ea0790ceeb1924ee3a88c959345f235aad219cfb13ff03c4458081ac350d47fc135a7ba2d49d3 +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.25.9, @babel/compat-data@npm:^7.26.0": + version: 7.26.2 + resolution: "@babel/compat-data@npm:7.26.2" + checksum: 10/ed9eed6b62ce803ef4a320b1dac76b0302abbb29c49dddf96f3e3207d9717eb34e299a8651bb1582e9c3346ead74b6d595ffced5b3dae718afa08b18741f8402 languageName: node linkType: hard -"@babel/core@npm:7.25.9, @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.9 - resolution: "@babel/core@npm:7.25.9" +"@babel/core@npm:7.26.0, @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.26.0 + resolution: "@babel/core@npm:7.26.0" dependencies: "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.25.9" - "@babel/generator": "npm:^7.25.9" + "@babel/code-frame": "npm:^7.26.0" + "@babel/generator": "npm:^7.26.0" "@babel/helper-compilation-targets": "npm:^7.25.9" - "@babel/helper-module-transforms": "npm:^7.25.9" - "@babel/helpers": "npm:^7.25.9" - "@babel/parser": "npm:^7.25.9" + "@babel/helper-module-transforms": "npm:^7.26.0" + "@babel/helpers": "npm:^7.26.0" + "@babel/parser": "npm:^7.26.0" "@babel/template": "npm:^7.25.9" "@babel/traverse": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" + "@babel/types": "npm:^7.26.0" 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/92cc69d9d59a5eb057527e69c41db46f05d0a8eeeb5ebab3f34e5ad040b74f34f20a4d97c3f3ede6476537cac93d2b46e3915b572269d2a039301dab068fd2e8 + checksum: 10/65767bfdb1f02e80d3af4f138066670ef8fdd12293de85ef151758a901c191c797e86d2e99b11c4cdfca33c72385ecaf38bbd7fa692791ec44c77763496b9b93 languageName: node linkType: hard -"@babel/generator@npm:^7.22.9, @babel/generator@npm:^7.24.4, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.7.2": - version: 7.25.9 - resolution: "@babel/generator@npm:7.25.9" +"@babel/generator@npm:^7.22.9, @babel/generator@npm:^7.24.4, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.26.0, @babel/generator@npm:^7.7.2": + version: 7.26.2 + resolution: "@babel/generator@npm:7.26.2" dependencies: - "@babel/types": "npm:^7.25.9" + "@babel/parser": "npm:^7.26.2" + "@babel/types": "npm:^7.26.0" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^3.0.2" - checksum: 10/eb36706c62ea77a09604077b84fae4e25d103cce58a15926d9d8b62d90c5fa69e35962515c05e78b5a975848ef772406dd79e2d4e83851bf9f7517b197a1b19d + checksum: 10/71ace82b5b07a554846a003624bfab93275ccf73cdb9f1a37a4c1094bf9dc94bb677c67e8b8c939dbd6c5f0eda2e8f268aa2b0d9c3b9511072565660e717e045 languageName: node linkType: hard @@ -241,17 +243,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-module-transforms@npm:7.25.9" +"@babel/helper-module-transforms@npm:^7.25.9, @babel/helper-module-transforms@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helper-module-transforms@npm:7.26.0" dependencies: "@babel/helper-module-imports": "npm:^7.25.9" - "@babel/helper-simple-access": "npm:^7.25.9" "@babel/helper-validator-identifier": "npm:^7.25.9" "@babel/traverse": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/6a9dc7da67f901a511ef26b99fd1b395946d466495159cbf80c092345ef3238306296ee76b204aea5f2675713130c58760c46b1ff3b6f6b19f7f8afcaa19d8ca + checksum: 10/9841d2a62f61ad52b66a72d08264f23052d533afc4ce07aec2a6202adac0bfe43014c312f94feacb3291f4c5aafe681955610041ece2c276271adce3f570f2f5 languageName: node linkType: hard @@ -349,36 +350,24 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helpers@npm:7.25.9" +"@babel/helpers@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helpers@npm:7.26.0" dependencies: "@babel/template": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" - checksum: 10/83c0df8f45850c5621be660b69c33d93c02832162a9109bb9a03de32a2b6477fbbd1e2c8c5c19fadb0d48f07066ff20d2d2da32de62dfda5c0a6a1036cebeb00 + "@babel/types": "npm:^7.26.0" + checksum: 10/fd4757f65d10b64cfdbf4b3adb7ea6ffff9497c53e0786452f495d1f7794da7e0898261b4db65e1c62bbb9a360d7d78a1085635c23dfc3af2ab6dcba06585f86 languageName: node linkType: hard -"@babel/highlight@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/highlight@npm:7.25.9" +"@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.9, @babel/parser@npm:^7.26.0, @babel/parser@npm:^7.26.2": + version: 7.26.2 + resolution: "@babel/parser@npm:7.26.2" dependencies: - "@babel/helper-validator-identifier": "npm:^7.25.9" - chalk: "npm:^2.4.2" - js-tokens: "npm:^4.0.0" - picocolors: "npm:^1.0.0" - checksum: 10/0d165283dd4eb312292cea8fec3ae0d376874b1885f476014f0136784ed5b564b2c2ba2d270587ed546ee92505056dab56493f7960c01c4e6394d71d1b2e7db6 - 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.9": - version: 7.25.9 - resolution: "@babel/parser@npm:7.25.9" - dependencies: - "@babel/types": "npm:^7.25.9" + "@babel/types": "npm:^7.26.0" bin: parser: ./bin/babel-parser.js - checksum: 10/702af8c40bb1236e3e3e6187b99e1290bd4bc1500aa53593ea63df8fe99f07ff1efef147b1d58886b264aff0972c4b9440ace442c8db9a6e079f318d46773421 + checksum: 10/8baee43752a3678ad9f9e360ec845065eeee806f1fdc8e0f348a8a0e13eef0959dabed4a197c978896c493ea205c804d0a1187cc52e4a1ba017c7935bab4983d languageName: node linkType: hard @@ -528,25 +517,25 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.25.9" +"@babel/plugin-syntax-import-assertions@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.26.0" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/2f0c70bb379135ee205402caa42c0dda4d5d8fb64ff4ad163cab94bd8291c1a63b9dc9cf293758cecee223080d2d61e83f92c6d2a264621e24a07258c48968db + checksum: 10/b58f2306df4a690ca90b763d832ec05202c50af787158ff8b50cdf3354359710bce2e1eb2b5135fcabf284756ac8eadf09ca74764aa7e76d12a5cac5f6b21e67 languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.25.9" +"@babel/plugin-syntax-import-attributes@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.26.0" dependencies: "@babel/helper-plugin-utils": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0ea00b9100d383680c7142e61e3aa7101e3657ec5e1bfa990871eee4ae17e2c4a0da084e8f611d349bb9612908e911e1400418eb59caa5184226b08f513c1a0a + checksum: 10/c122aa577166c80ee67f75aebebeef4150a132c4d3109d25d7fc058bf802946f883e330f20b78c1d3e3a5ada631c8780c263d2d01b5dbaecc69efefeedd42916 languageName: node linkType: hard @@ -754,15 +743,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-class-static-block@npm:7.25.9" +"@babel/plugin-transform-class-static-block@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-transform-class-static-block@npm:7.26.0" dependencies: "@babel/helper-create-class-features-plugin": "npm:^7.25.9" "@babel/helper-plugin-utils": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.12.0 - checksum: 10/01dbb32e443086cf98e986a0d3532f74916ed85ef4823139d155d69c6fbf4ae80ae862f83abba57d815bb6aebd349b18cdec93ef08be42ead4248f3a294c8a08 + checksum: 10/60cba3f125a7bc4f90706af0a011697c7ffd2eddfba336ed6f84c5f358c44c3161af18b0202475241a96dee7964d96dd3a342f46dbf85b75b38bb789326e1766 languageName: node linkType: hard @@ -1207,6 +1196,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-regexp-modifiers@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.26.0" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/726deca486bbd4b176f8a966eb0f4aabc19d9def3b8dabb8b3a656778eca0df1fda3f3c92b213aa5a184232fdafd5b7bd73b4e24ca4345c498ef6baff2bda4e1 + languageName: node + linkType: hard + "@babel/plugin-transform-reserved-words@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-transform-reserved-words@npm:7.25.9" @@ -1345,11 +1346,11 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.25.9, @babel/preset-env@npm:^7.24.4": - version: 7.25.9 - resolution: "@babel/preset-env@npm:7.25.9" +"@babel/preset-env@npm:7.26.0, @babel/preset-env@npm:^7.24.4": + version: 7.26.0 + resolution: "@babel/preset-env@npm:7.26.0" dependencies: - "@babel/compat-data": "npm:^7.25.9" + "@babel/compat-data": "npm:^7.26.0" "@babel/helper-compilation-targets": "npm:^7.25.9" "@babel/helper-plugin-utils": "npm:^7.25.9" "@babel/helper-validator-option": "npm:^7.25.9" @@ -1359,8 +1360,8 @@ __metadata: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.25.9" "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.25.9" "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions": "npm:^7.25.9" - "@babel/plugin-syntax-import-attributes": "npm:^7.25.9" + "@babel/plugin-syntax-import-assertions": "npm:^7.26.0" + "@babel/plugin-syntax-import-attributes": "npm:^7.26.0" "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" "@babel/plugin-transform-arrow-functions": "npm:^7.25.9" "@babel/plugin-transform-async-generator-functions": "npm:^7.25.9" @@ -1368,7 +1369,7 @@ __metadata: "@babel/plugin-transform-block-scoped-functions": "npm:^7.25.9" "@babel/plugin-transform-block-scoping": "npm:^7.25.9" "@babel/plugin-transform-class-properties": "npm:^7.25.9" - "@babel/plugin-transform-class-static-block": "npm:^7.25.9" + "@babel/plugin-transform-class-static-block": "npm:^7.26.0" "@babel/plugin-transform-classes": "npm:^7.25.9" "@babel/plugin-transform-computed-properties": "npm:^7.25.9" "@babel/plugin-transform-destructuring": "npm:^7.25.9" @@ -1401,6 +1402,7 @@ __metadata: "@babel/plugin-transform-private-property-in-object": "npm:^7.25.9" "@babel/plugin-transform-property-literals": "npm:^7.25.9" "@babel/plugin-transform-regenerator": "npm:^7.25.9" + "@babel/plugin-transform-regexp-modifiers": "npm:^7.26.0" "@babel/plugin-transform-reserved-words": "npm:^7.25.9" "@babel/plugin-transform-shorthand-properties": "npm:^7.25.9" "@babel/plugin-transform-spread": "npm:^7.25.9" @@ -1419,7 +1421,7 @@ __metadata: semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/7a9ca1a19949426498fdffc37eed919f9581226ea45b18be764d29ce81bbf8c8f2f37152300c3524b7a3861831d33f10d481810f3011dff702f780d3f79aa789 + checksum: 10/a7a80314f845deea713985a6316361c476621c76cfe5c6c28e8b9558f01634b49bbfdd3581ef94b5d6cff5c2b8830468aa53a73f5b5c1224db2dfea5db7e676f languageName: node linkType: hard @@ -1505,12 +1507,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.25.9, @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.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": - version: 7.25.9 - resolution: "@babel/runtime@npm:7.25.9" +"@babel/runtime@npm:7.26.0, @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.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 10/8d904cfcb433374b3bb90369452751c94ae69547cdd3679950de4527ac5d04195b9c4a1840482a6f3a84694cb22a6403a7f98b826d60cd945918223a4a6b479c + checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 languageName: node linkType: hard @@ -1540,13 +1542,13 @@ __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.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": - version: 7.25.9 - resolution: "@babel/types@npm:7.25.9" +"@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.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" dependencies: "@babel/helper-string-parser": "npm:^7.25.9" "@babel/helper-validator-identifier": "npm:^7.25.9" - checksum: 10/dd0f2874b10048aa230a5633ab440bbee8c3905f254ef26223b5321ddb824b057b9404d24a87556c6a9f7430198fa6311473778d147ed8ed7845428aee2ebc34 + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e languageName: node linkType: hard @@ -2763,10 +2765,10 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.11.0": - version: 4.11.1 - resolution: "@eslint-community/regexpp@npm:4.11.1" - checksum: 10/934b6d3588c7f16b18d41efec4fdb89616c440b7e3256b8cb92cfd31ae12908600f2b986d6c1e61a84cbc10256b1dd3448cd1eec79904bd67ac365d0f1aba2e2 +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1": + version: 4.12.1 + resolution: "@eslint-community/regexpp@npm:4.12.1" + checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc languageName: node linkType: hard @@ -2793,10 +2795,10 @@ __metadata: languageName: node linkType: hard -"@eslint/core@npm:^0.6.0": - version: 0.6.0 - resolution: "@eslint/core@npm:0.6.0" - checksum: 10/ec5cce168c8773fbd60c5a505563c6cf24398b3e1fa352929878d63129e0dd5b134d3232be2f2c49e8124a965d03359b38962aa0dcf7dfaf50746059d2a2f798 +"@eslint/core@npm:^0.7.0": + version: 0.7.0 + resolution: "@eslint/core@npm:0.7.0" + checksum: 10/69227f33fddd9b402b7b0830732a6e84cae77d202cb5b56f0dbcc462882e07d00e80216b796cf2f243f5b775af3ef27545a0c439d78e66122eab71da4773b81c languageName: node linkType: hard @@ -2817,10 +2819,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.12.0": - version: 9.12.0 - resolution: "@eslint/js@npm:9.12.0" - checksum: 10/c4ec9f7ff664f778324002bccdfd63e4a563018e4d7efc838d8149898f9df8649fbc51a379c3d7deea40da4fba9e8e62f39f2df3ff2b9616e2241bbfc10456b0 +"@eslint/js@npm:9.14.0": + version: 9.14.0 + resolution: "@eslint/js@npm:9.14.0" + checksum: 10/897e26bd68f898e56e96f85c92a1d823ef3d9f34e17d88d0ff40e88882ddae28d2f35915150c21cf640e0c64cb23703d0fbe6f7c9b9d6328aabe58ca30d9b4fe languageName: node linkType: hard @@ -2848,9 +2850,9 @@ __metadata: linkType: hard "@faker-js/faker@npm:^9.0.0": - version: 9.0.3 - resolution: "@faker-js/faker@npm:9.0.3" - checksum: 10/60ea71c31d6bd6317184736de17daee415e229cab5e10a4146a19622f3eb22f49f42345ff339dc9261f0214ca1f9b019c63fd9aac842a2d359f135102788ec90 + version: 9.2.0 + resolution: "@faker-js/faker@npm:9.2.0" + checksum: 10/c233aaa41ebc4f4bed36c1304c399c424b9e5abca9cb8fe54ce73d4162ec7acd66f294b90c85ed7a5df774400e44f0a10364bdd41f49e72cb519a48b5948939f languageName: node linkType: hard @@ -2946,14 +2948,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.2.0": - version: 2.2.0 - resolution: "@formatjs/ecma402-abstract@npm:2.2.0" +"@formatjs/ecma402-abstract@npm:2.2.3": + version: 2.2.3 + resolution: "@formatjs/ecma402-abstract@npm:2.2.3" dependencies: - "@formatjs/fast-memoize": "npm:2.2.1" - "@formatjs/intl-localematcher": "npm:0.5.5" - tslib: "npm:^2.7.0" - checksum: 10/f836dfa8787c8d0d2022f7c77fd15025a402ee519f9cb6e68dfce2b937956059e4f96b6059c40506c7260d93bdfd0517001b568cdbea8db08b439ce5d21f437e + "@formatjs/fast-memoize": "npm:2.2.3" + "@formatjs/intl-localematcher": "npm:0.5.7" + tslib: "npm:2" + checksum: 10/d39e9f0d36c296a635f52aa35e07a67b6aa90383a30a046a0508e5d730676399fd0e67188eff463fe2a4d5febc9f567af45788fdf881e070910be7eb9294dd8c languageName: node linkType: hard @@ -2966,12 +2968,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/fast-memoize@npm:2.2.1": - version: 2.2.1 - resolution: "@formatjs/fast-memoize@npm:2.2.1" +"@formatjs/fast-memoize@npm:2.2.3": + version: 2.2.3 + resolution: "@formatjs/fast-memoize@npm:2.2.3" dependencies: - tslib: "npm:^2.7.0" - checksum: 10/7bb12904d4c93cfae17006388b26d97cc0e32fef5af510f80974437382cd13a88b439b4407d96b6646af1806977cf34113f3dc8f1c96b28f73856700ee7857fd + tslib: "npm:2" + checksum: 10/a9634acb5e03d051e09881eea5484ab02271f7d6b5f96ae9485674ab3c359aa881bc45fc07a1181ae4b2d6e288dadc169f578d142d698913ebbefa373014cac2 languageName: node linkType: hard @@ -2996,14 +2998,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-durationformat@npm:^0.5.0": - version: 0.5.0 - resolution: "@formatjs/intl-durationformat@npm:0.5.0" +"@formatjs/intl-durationformat@npm:^0.6.0": + version: 0.6.3 + resolution: "@formatjs/intl-durationformat@npm:0.6.3" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.0" - "@formatjs/intl-localematcher": "npm:0.5.5" - tslib: "npm:^2.7.0" - checksum: 10/3fe55f729cf76df13e5b5bf085cf06f7f46bb69b1eeddfbfa8a5223d87c65438a5e91199e39ff8a24fa7c79d5e83754ee406b3012adb9ae3ff370239cb067377 + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/intl-localematcher": "npm:0.5.7" + tslib: "npm:2" + checksum: 10/1adc77b06e2f5791aef526cc3041de339cf99bc7e68cce81490066e0b1b0124d5cbe03d9f28b6a46bdd9ba22a0363315ccb20b9d1859ee0bcea7f79e9365844a languageName: node linkType: hard @@ -3016,12 +3018,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.5.5": - version: 0.5.5 - resolution: "@formatjs/intl-localematcher@npm:0.5.5" +"@formatjs/intl-localematcher@npm:0.5.7": + version: 0.5.7 + resolution: "@formatjs/intl-localematcher@npm:0.5.7" dependencies: - tslib: "npm:^2.7.0" - checksum: 10/179069eb3a23510e17f118efa3b6a9873f18683977cb813e57ef08cba09bbac73fce13f42c557deb62d3da4f32a8c7ad7d522aae4f1b41625835ce564e59e750 + tslib: "npm:2" + checksum: 10/52201f12212e7e9cba1a4f99020da587b13e44e06e03c4ccd4e5ac0829b411e73dfe0904a9039ef81eeabeea04ed8cfae9e727e6791acd0230745b7bd3ad059e languageName: node linkType: hard @@ -3079,7 +3081,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" @@ -3134,7 +3136,7 @@ __metadata: "@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/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" @@ -3190,7 +3192,7 @@ __metadata: micro-memoize: "npm:^4.1.2" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" @@ -3216,7 +3218,7 @@ __metadata: "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" @@ -3231,7 +3233,7 @@ __metadata: logfmt: "npm:^1.3.2" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-window: "npm:1.8.10" rxjs: "npm:7.8.1" stream-browserify: "npm:3.0.0" @@ -3372,7 +3374,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-test-renderer: "npm:18.2.0" react-use: "npm:17.5.1" @@ -3408,7 +3410,7 @@ __metadata: "@opentelemetry/exporter-collector": "npm:0.25.0" "@opentelemetry/semantic-conventions": "npm:1.27.0" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" @@ -3429,7 +3431,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" @@ -3459,7 +3461,7 @@ __metadata: "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.13" @@ -3469,7 +3471,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" @@ -3612,7 +3614,7 @@ __metadata: dependencies: "@typescript-eslint/types": "npm:^8.9.0" "@typescript-eslint/utils": "npm:^8.9.0" - eslint: "npm:9.12.0" + eslint: "npm:9.14.0" tslib: "npm:2.7.0" languageName: unknown linkType: soft @@ -3679,7 +3681,7 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:1.11.0, @grafana/faro-web-sdk@npm:^1.10.2, @grafana/faro-web-sdk@npm:^1.3.6": +"@grafana/faro-web-sdk@npm:1.11.0, @grafana/faro-web-sdk@npm:^1.11.0, @grafana/faro-web-sdk@npm:^1.3.6": version: 1.11.0 resolution: "@grafana/faro-web-sdk@npm:1.11.0" dependencies: @@ -3691,10 +3693,10 @@ __metadata: linkType: hard "@grafana/faro-web-tracing@npm:^1.8.2": - version: 1.10.2 - resolution: "@grafana/faro-web-tracing@npm:1.10.2" + version: 1.11.0 + resolution: "@grafana/faro-web-tracing@npm:1.11.0" dependencies: - "@grafana/faro-web-sdk": "npm:^1.10.2" + "@grafana/faro-web-sdk": "npm:^1.11.0" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/context-zone": "npm:1.26.0" "@opentelemetry/core": "npm:^1.26.0" @@ -3706,7 +3708,7 @@ __metadata: "@opentelemetry/resources": "npm:^1.26.0" "@opentelemetry/sdk-trace-web": "npm:^1.26.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" - checksum: 10/c477d2373fc51d432f1492d012e828310c5044766a7a4d9317546d2e9ba074d5532a5d4642e9fc4bfa9a70bf12969a593bc3151df186a0865f408976ccd73a26 + checksum: 10/d9e46b42d574f8e012bb113a5f3bd03699433cab01f57c548a3461102dd1a5882b7e53c340b4a8374a340cfff69d30af49049b7c6f580c9d78ed7377819e1ecd languageName: node linkType: hard @@ -3714,8 +3716,8 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/flamegraph@workspace:packages/grafana-flamegraph" dependencies: - "@babel/core": "npm:7.25.9" - "@babel/preset-env": "npm:7.25.9" + "@babel/core": "npm:7.26.0" + "@babel/preset-env": "npm:7.26.0" "@babel/preset-react": "npm:7.25.9" "@emotion/css": "npm:11.13.4" "@grafana/data": "npm:11.4.0-pre" @@ -3819,7 +3821,7 @@ __metadata: "@types/systemjs": "npm:6.15.1" jest: "npm:^29.6.4" react: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-use: "npm:17.5.1" rxjs: "npm:7.8.1" ts-jest: "npm:29.2.5" @@ -3840,7 +3842,7 @@ __metadata: "@swc/core": "npm:1.4.2" "@types/eslint": "npm:9.6.1" copy-webpack-plugin: "npm:12.0.2" - eslint: "npm:9.12.0" + eslint: "npm:9.14.0" eslint-webpack-plugin: "npm:4.2.0" fork-ts-checker-webpack-plugin: "npm:9.0.2" glob: "npm:11.0.0" @@ -3853,16 +3855,16 @@ __metadata: linkType: soft "@grafana/plugin-e2e@npm:^1.11.0": - version: 1.11.3 - resolution: "@grafana/plugin-e2e@npm:1.11.3" + version: 1.12.0 + resolution: "@grafana/plugin-e2e@npm:1.12.0" dependencies: "@grafana/e2e-selectors": "npm:^11.4.0-205692" semver: "npm:^7.5.4" - uuid: "npm:^11.0.0" + uuid: "npm:^11.0.2" yaml: "npm:^2.3.4" peerDependencies: "@playwright/test": ^1.41.2 - checksum: 10/a0e127bae12507c8c0a4e93d5f08d7c08ca369c0b8da3737d580c0f0ce843d7a50c1f1e6e7fa6b36546ca6f5acecf2472598f2aa44666fbab40d65712472b4f1 + checksum: 10/7e378bed8ad7a6f07a834fc9a02ca9fbcc63e5d842545e9fa443b0f24fcb62001f61a3592a45b0893af87a31f022b2646d388d152325f92df94213d94ecfcaad languageName: node linkType: hard @@ -3887,13 +3889,13 @@ __metadata: "@lezer/highlight": "npm:1.2.1" "@lezer/lr": "npm:1.4.2" "@prometheus-io/lezer-promql": "npm:0.55.1" - "@reduxjs/toolkit": "npm:2.2.8" + "@reduxjs/toolkit": "npm:2.3.0" "@rollup/plugin-image": "npm:3.0.3" "@rollup/plugin-node-resolve": "npm:15.3.0" "@swc/core": "npm:1.4.2" "@swc/helpers": "npm:0.5.15" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:7.4.3" @@ -3919,11 +3921,11 @@ __metadata: date-fns: "npm:3.6.0" debounce-promise: "npm:3.1.2" esbuild: "npm:0.24.0" - eslint: "npm:9.12.0" + eslint: "npm:9.14.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" - eslint-plugin-jest: "npm:28.8.3" - eslint-plugin-jsdoc: "npm:50.4.3" + eslint-plugin-jest: "npm:28.9.0" + eslint-plugin-jsdoc: "npm:50.5.0" eslint-plugin-jsx-a11y: "npm:6.10.2" eslint-plugin-lodash: "npm:8.0.0" eslint-plugin-react: "npm:7.37.2" @@ -3948,7 +3950,7 @@ __metadata: react: "npm:18.2.0" react-dom: "npm:18.2.0" react-highlight-words: "npm:0.20.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-test-renderer: "npm:18.2.0" react-use: "npm:17.5.1" @@ -3959,7 +3961,7 @@ __metadata: rollup-plugin-node-externals: "npm:^7.1.3" rxjs: "npm:7.8.1" sass: "npm:1.79.5" - sass-loader: "npm:16.0.2" + sass-loader: "npm:16.0.3" semver: "npm:7.6.3" style-loader: "npm:4.0.0" testing-library-selector: "npm:0.3.1" @@ -4023,7 +4025,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/saga-icons@workspace:packages/grafana-icons" dependencies: - "@babel/core": "npm:7.25.9" + "@babel/core": "npm:7.26.0" "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:^15.3.0" "@rollup/plugin-typescript": "npm:^12.1.0" @@ -4143,7 +4145,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-use: "npm:17.5.1" react-virtualized-auto-sizer: "npm:1.0.24" rxjs: "npm:7.8.1" @@ -4169,7 +4171,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: - "@babel/core": "npm:7.25.9" + "@babel/core": "npm:7.26.0" "@emotion/css": "npm:11.13.4" "@emotion/react": "npm:11.13.3" "@emotion/serialize": "npm:1.3.2" @@ -4207,7 +4209,7 @@ __metadata: "@storybook/theming": "npm:^8.1.6" "@tanstack/react-virtual": "npm:^3.5.1" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/user-event": "npm:14.5.2" "@types/chance": "npm:1.1.6" @@ -4238,7 +4240,7 @@ __metadata: chance: "npm:1.1.12" classnames: "npm:2.5.1" common-tags: "npm:1.8.2" - core-js: "npm:3.38.1" + core-js: "npm:3.39.0" css-loader: "npm:7.1.2" csstype: "npm:3.1.3" d3: "npm:7.9.0" @@ -4277,7 +4279,7 @@ __metadata: react-inlinesvg: "npm:3.0.2" react-loading-skeleton: "npm:3.5.0" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:^5.1.0" react-table: "npm:7.8.0" react-test-renderer: "npm:18.2.0" @@ -4292,7 +4294,7 @@ __metadata: rollup-plugin-node-externals: "npm:^7.1.3" rollup-plugin-svg-import: "npm:3.0.0" rxjs: "npm:7.8.1" - sass-loader: "npm:16.0.2" + sass-loader: "npm:16.0.3" slate: "npm:0.47.9" slate-plain-serializer: "npm:0.7.13" slate-react: "npm:0.22.10" @@ -4356,20 +4358,20 @@ __metadata: languageName: node linkType: hard -"@humanfs/core@npm:^0.19.0": - version: 0.19.0 - resolution: "@humanfs/core@npm:0.19.0" - checksum: 10/9c4f96b9e934b7d2f69c5ee8b9414dcaf5c5a03225eb08f8ace3b80429c0fc796e11c4e2ef182172790e7b4560b1137ef984da4dc9662cdd5e3e92baceb02821 +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 10/270d936be483ab5921702623bc74ce394bf12abbf57d9145a69e8a0d1c87eb1c768bd2d93af16c5705041e257e6d9cc7529311f63a1349f3678abc776fc28523 languageName: node linkType: hard -"@humanfs/node@npm:^0.16.5": - version: 0.16.5 - resolution: "@humanfs/node@npm:0.16.5" +"@humanfs/node@npm:^0.16.6": + version: 0.16.6 + resolution: "@humanfs/node@npm:0.16.6" dependencies: - "@humanfs/core": "npm:^0.19.0" + "@humanfs/core": "npm:^0.19.1" "@humanwhocodes/retry": "npm:^0.3.0" - checksum: 10/16e49b5f9d4a3cf8205af18f0909b8c6e00faa70a0e01bc606b413423ee20123e53028b6ca22c57725595341d62e148cd1908c297a761ee495087cc674f7b0a6 + checksum: 10/6d43c6727463772d05610aa05c83dab2bfbe78291022ee7a92cb50999910b8c720c76cc312822e2dea2b497aa1b3fef5fe9f68803fc45c9d4ed105874a65e339 languageName: node linkType: hard @@ -4380,13 +4382,20 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/retry@npm:^0.3.0, @humanwhocodes/retry@npm:^0.3.1": +"@humanwhocodes/retry@npm:^0.3.0": version: 0.3.1 resolution: "@humanwhocodes/retry@npm:0.3.1" checksum: 10/eb457f699529de7f07649679ec9e0353055eebe443c2efe71c6dd950258892475a038e13c6a8c5e13ed1fb538cdd0a8794faa96b24b6ffc4c87fb1fc9f70ad7f languageName: node linkType: hard +"@humanwhocodes/retry@npm:^0.4.0": + version: 0.4.1 + resolution: "@humanwhocodes/retry@npm:0.4.1" + checksum: 10/39fafc7319e88f61befebd5e1b4f0136534ea6a9bd10d74366698187bd63544210ec5d79a87ed4d91297f1cc64c4c53d45fb0077a2abfdce212cf0d3862d5f04 + languageName: node + linkType: hard + "@hutson/parse-repository-url@npm:^3.0.0": version: 3.0.2 resolution: "@hutson/parse-repository-url@npm:3.0.2" @@ -6993,9 +7002,9 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:2.2.8": - version: 2.2.8 - resolution: "@reduxjs/toolkit@npm:2.2.8" +"@reduxjs/toolkit@npm:2.3.0": + version: 2.3.0 + resolution: "@reduxjs/toolkit@npm:2.3.0" dependencies: immer: "npm:^10.0.3" redux: "npm:^5.0.1" @@ -7009,7 +7018,7 @@ __metadata: optional: true react-redux: optional: true - checksum: 10/d89bf5fd3e763669d34169fdc68da2a673cebc07a20bfc835277112dcdc2e7276258646fce9aa9792e3680caef719813bdc2dd654117632018ec027266026873 + checksum: 10/3aa0bf29ce4dfe4ae75416cb5338a91ccc89f06c5a2fcdae5dc137dca02b3990c46306062696e158869ac399b50cae6476e5536902df9bd1ad1a998195221af6 languageName: node linkType: hard @@ -8307,15 +8316,15 @@ __metadata: linkType: hard "@stylistic/eslint-plugin-ts@npm:^2.9.0": - version: 2.9.0 - resolution: "@stylistic/eslint-plugin-ts@npm:2.9.0" + version: 2.10.1 + resolution: "@stylistic/eslint-plugin-ts@npm:2.10.1" dependencies: - "@typescript-eslint/utils": "npm:^8.8.0" - eslint-visitor-keys: "npm:^4.1.0" - espree: "npm:^10.2.0" + "@typescript-eslint/utils": "npm:^8.12.2" + eslint-visitor-keys: "npm:^4.2.0" + espree: "npm:^10.3.0" peerDependencies: eslint: ">=8.40.0" - checksum: 10/f689bdb8047abbe80fad506dae884cf840254a60f943c60fbfa4d338fbcc2146458d5acd97f427293649a0b14dd1ccb30510cce30fa503f050e18001571352aa + checksum: 10/e3a966e30e4986f795592a4a574bea4133d42d3ae0489799bfd8498d6c7d1c5b4b0c0016e825fbb521156602041bccbed60f4909e0056a3c225d63cb8bffe324 languageName: node linkType: hard @@ -9339,9 +9348,9 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.5.0": - version: 6.5.0 - resolution: "@testing-library/jest-dom@npm:6.5.0" +"@testing-library/jest-dom@npm:6.6.3, @testing-library/jest-dom@npm:^6.1.2": + version: 6.6.3 + resolution: "@testing-library/jest-dom@npm:6.6.3" dependencies: "@adobe/css-tools": "npm:^4.4.0" aria-query: "npm:^5.0.0" @@ -9350,22 +9359,7 @@ __metadata: dom-accessibility-api: "npm:^0.6.3" lodash: "npm:^4.17.21" redent: "npm:^3.0.0" - checksum: 10/3d2080888af5fd7306f57448beb5a23f55d965e265b5e53394fffc112dfb0678d616a5274ff0200c46c7618f293520f86fc8562eecd8bdbc0dbb3294d63ec431 - languageName: node - linkType: hard - -"@testing-library/jest-dom@npm:^6.1.2": - version: 6.6.1 - resolution: "@testing-library/jest-dom@npm:6.6.1" - dependencies: - "@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.21" - redent: "npm:^3.0.0" - checksum: 10/006357d7547cc50624564a1b0e9986d0f0252365a6e5ac1c7c989032159c47226adfbd9a9ac17eef236738e9597541d344176176b5c3ed0e06e8f4b33387e135 + checksum: 10/1f3427e45870eab9dcc59d6504b780d4a595062fe1687762ae6e67d06a70bf439b40ab64cf58cbace6293a99e3764d4647fdc8300a633b721764f5ce39dade18 languageName: node linkType: hard @@ -10293,7 +10287,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -10759,7 +10753,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.8, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": +"@types/semver@npm:7.5.8, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 @@ -11066,13 +11060,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/scope-manager@npm:6.21.0" +"@typescript-eslint/scope-manager@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/scope-manager@npm:8.14.0" dependencies: - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/visitor-keys": "npm:6.21.0" - checksum: 10/fe91ac52ca8e09356a71dc1a2f2c326480f3cccfec6b2b6d9154c1a90651ab8ea270b07c67df5678956c3bbf0bbe7113ab68f68f21b20912ea528b1214197395 + "@typescript-eslint/types": "npm:8.14.0" + "@typescript-eslint/visitor-keys": "npm:8.14.0" + checksum: 10/48ff44a790254b5a98c17bf15176fbdc1408b58eb3ccd8eda9c5707811786de25e1bccc5c490dcc05cbd34b685e162ee4e92b28f57b071c522274fa97f23c98c languageName: node linkType: hard @@ -11108,14 +11102,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/types@npm:6.21.0" - checksum: 10/e26da86d6f36ca5b6ef6322619f8ec55aabcd7d43c840c977ae13ae2c964c3091fc92eb33730d8be08927c9de38466c5323e78bfb270a9ff1d3611fe821046c5 +"@typescript-eslint/types@npm:8.14.0, @typescript-eslint/types@npm:^8.9.0": + version: 8.14.0 + resolution: "@typescript-eslint/types@npm:8.14.0" + checksum: 10/1924aef8efdf5399d6cc9ef3a5307fda39b1a2be129ab8cb24a46dc0a37156230e77f2809ab709d5d0a43891b6ffd67ce45292724e8f8164ac19e1786c5f4644 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.9.0, @typescript-eslint/types@npm:^8.9.0": +"@typescript-eslint/types@npm:8.9.0": version: 8.9.0 resolution: "@typescript-eslint/types@npm:8.9.0" checksum: 10/4d087153605ec23c980f9bc807b122edefff828e0c3b52ef531f4b8e1d30078c39f95e84019370a395bf97eed0d7886cc50b8cd545c287f8a2a21b301272377a @@ -11140,22 +11134,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/typescript-estree@npm:6.21.0" +"@typescript-eslint/typescript-estree@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.14.0" dependencies: - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/visitor-keys": "npm:6.21.0" + "@typescript-eslint/types": "npm:8.14.0" + "@typescript-eslint/visitor-keys": "npm:8.14.0" debug: "npm:^4.3.4" - globby: "npm:^11.1.0" + fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" - minimatch: "npm:9.0.3" - semver: "npm:^7.5.4" - ts-api-utils: "npm:^1.0.1" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" peerDependenciesMeta: typescript: optional: true - checksum: 10/b32fa35fca2a229e0f5f06793e5359ff9269f63e9705e858df95d55ca2cd7fdb5b3e75b284095a992c48c5fc46a1431a1a4b6747ede2dd08929dc1cbacc589b8 + checksum: 10/b0b9f228071b6338dbf5e2ac52848fa6af630e8d84d4102e1cccaae67114f2bff82bd027af2818e3ad778668e3c3d4a2fb31b7f4c8a9796295e5aa87903fb313 languageName: node linkType: hard @@ -11178,7 +11172,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.9.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.8.0, @typescript-eslint/utils@npm:^8.9.0": +"@typescript-eslint/utils@npm:8.9.0": version: 8.9.0 resolution: "@typescript-eslint/utils@npm:8.9.0" dependencies: @@ -11210,20 +11204,17 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^6.4.1": - version: 6.21.0 - resolution: "@typescript-eslint/utils@npm:6.21.0" +"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.12.2, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.9.0": + version: 8.14.0 + resolution: "@typescript-eslint/utils@npm:8.14.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@types/json-schema": "npm:^7.0.12" - "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:6.21.0" - "@typescript-eslint/types": "npm:6.21.0" - "@typescript-eslint/typescript-estree": "npm:6.21.0" - semver: "npm:^7.5.4" + "@typescript-eslint/scope-manager": "npm:8.14.0" + "@typescript-eslint/types": "npm:8.14.0" + "@typescript-eslint/typescript-estree": "npm:8.14.0" peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - checksum: 10/b404a2c55a425a79d054346ae123087d30c7ecf7ed7abcf680c47bf70c1de4fabadc63434f3f460b2fa63df76bc9e4a0b9fa2383bb8a9fcd62733fb5c4e4f3e3 + eslint: ^8.57.0 || ^9.0.0 + checksum: 10/6d3b2583c473b452dd8f978524802aabd275055f98d461cc71ee6a9424291f4481d2a3416a3f77b2458939dd38a39c0fd8e0c9b47915141c8409e63528a1216b languageName: node linkType: hard @@ -11237,13 +11228,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/visitor-keys@npm:6.21.0" +"@typescript-eslint/visitor-keys@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.14.0" dependencies: - "@typescript-eslint/types": "npm:6.21.0" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10/30422cdc1e2ffad203df40351a031254b272f9c6f2b7e02e9bfa39e3fc2c7b1c6130333b0057412968deda17a3a68a578a78929a8139c6acef44d9d841dc72e1 + "@typescript-eslint/types": "npm:8.14.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10/735cc9c2ce3235e543d03afe0de740022888e69ed9f5027564e1c959a3a087106bcf21b5b8d3ac872171c0a585744f0442b76fe6ba68341a735a4b4a15f52a46 languageName: node linkType: hard @@ -11780,12 +11771,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.12.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": - version: 8.12.1 - resolution: "acorn@npm:8.12.1" +"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.14.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": + version: 8.14.0 + resolution: "acorn@npm:8.14.0" bin: acorn: bin/acorn - checksum: 10/d08c2d122bba32d0861e0aa840b2ee25946c286d5dc5990abca991baf8cdbfbe199b05aacb221b979411a2fea36f83e26b5ac4f6b4e0ce49038c62316c1848f0 + checksum: 10/6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2 languageName: node linkType: hard @@ -12018,15 +12009,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^3.2.1": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: "npm:^1.9.0" - checksum: 10/d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 - languageName: node - linkType: hard - "ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" @@ -13307,17 +13289,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.4.2": - version: 2.4.2 - resolution: "chalk@npm:2.4.2" - dependencies: - ansi-styles: "npm:^3.2.1" - escape-string-regexp: "npm:^1.0.5" - supports-color: "npm:^5.3.0" - checksum: 10/3d1d103433166f6bfe82ac75724951b33769675252d8417317363ef9d54699b7c3b2d46671b772b893a8e50c3ece70c4b933c73c01e81bc60ea4df9b55afa303 - languageName: node - linkType: hard - "chalk@npm:^3.0.0": version: 3.0.0 resolution: "chalk@npm:3.0.0" @@ -13737,15 +13708,6 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:^1.9.0": - version: 1.9.3 - resolution: "color-convert@npm:1.9.3" - dependencies: - color-name: "npm:1.1.3" - checksum: 10/ffa319025045f2973919d155f25e7c00d08836b6b33ea2d205418c59bd63a665d713c52d9737a9e0fe467fb194b40fbef1d849bae80d674568ee220a31ef3d10 - languageName: node - linkType: hard - "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -13762,13 +13724,6 @@ __metadata: languageName: node linkType: hard -"color-name@npm:1.1.3": - version: 1.1.3 - resolution: "color-name@npm:1.1.3" - checksum: 10/09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d - languageName: node - linkType: hard - "color-name@npm:^1.1.4, color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" @@ -14242,10 +14197,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.38.1, core-js@npm:^3.6.0, core-js@npm:^3.8.3": - version: 3.38.1 - resolution: "core-js@npm:3.38.1" - checksum: 10/3c25fdf0b2595ed37ceb305213a61e2cf26185f628455e99d1c736dda5f69e2de4de7126e6a1da136f54260c4fcc982c4215e37b5a618790a597930f854c0a37 +"core-js@npm:3.39.0, core-js@npm:^3.6.0, core-js@npm:^3.8.3": + version: 3.39.0 + resolution: "core-js@npm:3.39.0" + checksum: 10/a3d34e669783dfc878e545f1983f60d9ff48a3867cd1d7ff8839b849e053002a208c7c14a5ca354b8e0b54982901e2f83dc87c3d9b95de0a94b4071d1c74e5f6 languageName: node linkType: hard @@ -14324,6 +14279,13 @@ __metadata: languageName: node linkType: hard +"crashme@npm:0.0.15": + version: 0.0.15 + resolution: "crashme@npm:0.0.15" + checksum: 10/576110b3d61f996869b993c2f6b8dca33ac82d07ea708b39217b6349653e38a9894c1af838136bc60ba61f6c69bd68f1c60e3da772cb2fa1fe0b64c2fbc53b79 + languageName: node + linkType: hard + "create-jest@npm:^29.7.0": version: 29.7.0 resolution: "create-jest@npm:29.7.0" @@ -15846,9 +15808,9 @@ __metadata: linkType: hard "dompurify@npm:^3.0.0": - version: 3.1.7 - resolution: "dompurify@npm:3.1.7" - checksum: 10/dc637a064306f83cf911caa267ffe1f973552047602020e3b6723c90f67962813edf8a65a0b62e8c9bc13fcd173a2691212a3719bc116226967f46bcd6181277 + version: 3.2.0 + resolution: "dompurify@npm:3.2.0" + checksum: 10/d9139fdc69e95efe43d154362216a9a6768c3a238b02b988a7ac0ddd044a992422d6c5fae5b319ab47aef8465824a815d104b843800b294352657f70d5168bfa languageName: node linkType: hard @@ -16879,9 +16841,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:28.8.3": - version: 28.8.3 - resolution: "eslint-plugin-jest@npm:28.8.3" +"eslint-plugin-jest@npm:28.9.0": + version: 28.9.0 + resolution: "eslint-plugin-jest@npm:28.9.0" dependencies: "@typescript-eslint/utils": "npm:^6.0.0 || ^7.0.0 || ^8.0.0" peerDependencies: @@ -16893,13 +16855,13 @@ __metadata: optional: true jest: optional: true - checksum: 10/3f1798c61e143981eefcfb2fbc4b2e5b329378ebaafdec6485f443c79ee0d3304e9409e8ea8ce089ac15abb4e700d8d838e0a1da29feb528d77a2b3cce6989ec + checksum: 10/399dd882205da996c2d46b768b744093346ffe4f730972825c72a3f85cc2e26bc08ccd73a7b487f8da4c39fa0178c58b571d5d611a22ced5518b5dfccb48d906 languageName: node linkType: hard -"eslint-plugin-jsdoc@npm:50.4.3": - version: 50.4.3 - resolution: "eslint-plugin-jsdoc@npm:50.4.3" +"eslint-plugin-jsdoc@npm:50.5.0": + version: 50.5.0 + resolution: "eslint-plugin-jsdoc@npm:50.5.0" dependencies: "@es-joy/jsdoccomment": "npm:~0.49.0" are-docs-informative: "npm:^0.0.2" @@ -16914,7 +16876,7 @@ __metadata: synckit: "npm:^0.9.1" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10/1950e907ba1237e0261da79f41c98ac51a4ea45e5163bb928671828868b93c2fc688448ff67d123b56f9248238ffb0d09d371a0b988320d743366826135861d4 + checksum: 10/4abe3ac34ad0d3bf34238968b6961c4ea9ec8eb058046f554cf60a702a91ac294eda8e475cadeaa1951e1a5317dd055543407787b04256fbd0728454af7e2ad0 languageName: node linkType: hard @@ -16955,11 +16917,11 @@ __metadata: linkType: hard "eslint-plugin-no-barrel-files@npm:^1.1.1": - version: 1.1.1 - resolution: "eslint-plugin-no-barrel-files@npm:1.1.1" + version: 1.2.0 + resolution: "eslint-plugin-no-barrel-files@npm:1.2.0" dependencies: - "@typescript-eslint/utils": "npm:^6.4.1" - checksum: 10/26d7cee608455bf31ccf691b7fff68adf2dbdba90a3e6fd06af84e022ad214a6993af0ad74e8854dfe3c8aaa5a6994e7c3e98af0c63397cc1f972d7592fb61f9 + "@typescript-eslint/utils": "npm:^8.13.0" + checksum: 10/da54a4930ddfd7d14bd82766e857af9eea5315125a2599d5ba03326530214e0d416f78915335d6c239b19df21b989196ffc4146919eff05477cef7e34261b946 languageName: node linkType: hard @@ -17047,27 +17009,27 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^8.1.0": - version: 8.1.0 - resolution: "eslint-scope@npm:8.1.0" +"eslint-scope@npm:^8.1.0, eslint-scope@npm:^8.2.0": + version: 8.2.0 + resolution: "eslint-scope@npm:8.2.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10/4c34a12fbeb0677822a9e93e81f2027e39e6f27557c17bc1e5ff76debbd41e748c3673517561792bda9e276245f89fbfd9b0b24fcec3b33a04ee2196729b3489 + checksum: 10/cd9ab60d5a68f3a0fcac04d1cff5a7383d0f331964d5f1c446259123caec5b3ccc542284d07846e4f4d1389da77750821cc9a6e1ce18558c674977351666f9a6 languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.1.0": - version: 4.1.0 - resolution: "eslint-visitor-keys@npm:4.1.0" - checksum: 10/3fb5bd1b2f36db89d0ac57ddd66d36ccd3b1e3cddb2a55a0f9f6f1c85268cfcc1cc32e7eda4990e3423107a120dd254fb6cb52d6154cf81d344d8c3fa671f7c2 +"eslint-visitor-keys@npm:^4.2.0": + version: 4.2.0 + resolution: "eslint-visitor-keys@npm:4.2.0" + checksum: 10/9651b3356b01760e586b4c631c5268c0e1a85236e3292bf754f0472f465bf9a856c0ddc261fceace155334118c0151778effafbab981413dbf9288349343fa25 languageName: node linkType: hard @@ -17087,20 +17049,20 @@ __metadata: languageName: node linkType: hard -"eslint@npm:9.12.0": - version: 9.12.0 - resolution: "eslint@npm:9.12.0" +"eslint@npm:9.14.0": + version: 9.14.0 + resolution: "eslint@npm:9.14.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" - "@eslint-community/regexpp": "npm:^4.11.0" + "@eslint-community/regexpp": "npm:^4.12.1" "@eslint/config-array": "npm:^0.18.0" - "@eslint/core": "npm:^0.6.0" + "@eslint/core": "npm:^0.7.0" "@eslint/eslintrc": "npm:^3.1.0" - "@eslint/js": "npm:9.12.0" + "@eslint/js": "npm:9.14.0" "@eslint/plugin-kit": "npm:^0.2.0" - "@humanfs/node": "npm:^0.16.5" + "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" - "@humanwhocodes/retry": "npm:^0.3.1" + "@humanwhocodes/retry": "npm:^0.4.0" "@types/estree": "npm:^1.0.6" "@types/json-schema": "npm:^7.0.15" ajv: "npm:^6.12.4" @@ -17108,9 +17070,9 @@ __metadata: cross-spawn: "npm:^7.0.2" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^8.1.0" - eslint-visitor-keys: "npm:^4.1.0" - espree: "npm:^10.2.0" + eslint-scope: "npm:^8.2.0" + eslint-visitor-keys: "npm:^4.2.0" + espree: "npm:^10.3.0" esquery: "npm:^1.5.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" @@ -17133,18 +17095,18 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10/c3f10d1ca3798bf1d0f71e43846e254d4bf0ea9ffbb0e61f9686a98e412aa762a454c5e5ef4e74fd71956b1500c04817c9f08dbf7a0cec47317160e28f585e4f + checksum: 10/6ce59dda56ecff9483c6e8cd28e91138d2c43cbf08c923f731f507fd9b4aba9d72761c99882dc313a72ea915a5e380ab0b4f01e208a7a37d71490ddfd29ee063 languageName: node linkType: hard -"espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.2.0": - version: 10.2.0 - resolution: "espree@npm:10.2.0" +"espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.3.0": + version: 10.3.0 + resolution: "espree@npm:10.3.0" dependencies: - acorn: "npm:^8.12.0" + acorn: "npm:^8.14.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^4.1.0" - checksum: 10/365076a963ca84244c1e2d36e4f812362d21cfa7e7df10d67f7b82b759467796df81184721d153c4e235d9ef5eb5b4d044167dd66be3be00f53a21a515b1bfb1 + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10/3412d44d4204c9e29d6b5dd0277400cfa0cd68495dc09eae1b9ce79d0c8985c1c5cc09cb9ba32a1cd963f48a49b0c46bdb7736afe395a300aa6bb1c0d86837e8 languageName: node linkType: hard @@ -18714,9 +18676,9 @@ __metadata: version: 0.0.0-use.local resolution: "grafana@workspace:." dependencies: - "@babel/core": "npm:7.25.9" - "@babel/preset-env": "npm:7.25.9" - "@babel/runtime": "npm:7.25.9" + "@babel/core": "npm:7.26.0" + "@babel/preset-env": "npm:7.26.0" + "@babel/runtime": "npm:7.26.0" "@betterer/betterer": "npm:5.4.0" "@betterer/cli": "npm:5.4.0" "@betterer/eslint": "npm:5.4.0" @@ -18728,7 +18690,7 @@ __metadata: "@eslint/compat": "npm:^1.2.0" "@fingerprintjs/fingerprintjs": "npm:^3.4.2" "@floating-ui/react": "npm:0.26.27" - "@formatjs/intl-durationformat": "npm:^0.5.0" + "@formatjs/intl-durationformat": "npm:^0.6.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/aws-sdk": "npm:0.5.0" "@grafana/azure-sdk": "npm:0.0.5" @@ -18782,14 +18744,14 @@ __metadata: "@react-types/menu": "npm:3.9.12" "@react-types/overlays": "npm:3.8.10" "@react-types/shared": "npm:3.25.0" - "@reduxjs/toolkit": "npm:2.2.8" + "@reduxjs/toolkit": "npm:2.3.0" "@rtk-query/codegen-openapi": "npm:^1.2.0" "@rtsao/plugin-proposal-class-properties": "npm:7.0.1-patch.1" "@stylistic/eslint-plugin-ts": "npm:^2.9.0" "@swc/core": "npm:1.4.2" "@swc/helpers": "npm:0.5.15" "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.0.1" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/user-event": "npm:14.5.2" @@ -18877,7 +18839,8 @@ __metadata: comlink: "npm:4.4.2" common-tags: "npm:1.8.2" copy-webpack-plugin: "npm:12.0.2" - core-js: "npm:3.38.1" + core-js: "npm:3.39.0" + crashme: "npm:0.0.15" css-loader: "npm:7.1.2" css-minimizer-webpack-plugin: "npm:6.0.0" cypress: "npm:13.10.0" @@ -18893,12 +18856,12 @@ __metadata: esbuild: "npm:0.24.0" esbuild-loader: "npm:4.2.2" esbuild-plugin-browserslist: "npm:^0.15.0" - eslint: "npm:9.12.0" + eslint: "npm:9.14.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" - eslint-plugin-jest: "npm:28.8.3" + eslint-plugin-jest: "npm:28.9.0" eslint-plugin-jest-dom: "npm:^5.4.0" - eslint-plugin-jsdoc: "npm:50.4.3" + eslint-plugin-jsdoc: "npm:50.5.0" eslint-plugin-jsx-a11y: "npm:6.10.2" eslint-plugin-lodash: "npm:8.0.0" eslint-plugin-no-barrel-files: "npm:^1.1.1" @@ -18996,7 +18959,7 @@ __metadata: react-router: "npm:5.3.3" react-router-dom: "npm:5.3.3" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.8.2" + react-select: "npm:5.8.3" react-select-event: "npm:5.5.1" react-split-pane: "npm:0.1.92" react-table: "npm:7.8.0" @@ -19015,10 +18978,10 @@ __metadata: regenerator-runtime: "npm:0.14.1" reselect: "npm:5.1.1" rimraf: "npm:6.0.1" - rudder-sdk-js: "npm:2.48.23" + rudder-sdk-js: "npm:2.48.26" rxjs: "npm:7.8.1" sass: "npm:1.79.5" - sass-loader: "npm:16.0.2" + sass-loader: "npm:16.0.3" selecto: "npm:1.26.3" semver: "npm:7.6.3" slate: "npm:0.47.9" @@ -19165,13 +19128,6 @@ __metadata: languageName: node linkType: hard -"has-flag@npm:^3.0.0": - version: 3.0.0 - resolution: "has-flag@npm:3.0.0" - checksum: 10/4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b - languageName: node - linkType: hard - "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -19323,7 +19279,7 @@ __metadata: languageName: node linkType: hard -"history@npm:4.10.1": +"history@npm:4.10.1, history@npm:^4.9.0": version: 4.10.1 resolution: "history@npm:4.10.1" dependencies: @@ -19346,20 +19302,6 @@ __metadata: languageName: node linkType: hard -"history@patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch::locator=grafana%40workspace%3A.": - version: 4.10.1 - resolution: "history@patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch::version=4.10.1&hash=b8466f&locator=grafana%40workspace%3A." - dependencies: - "@babel/runtime": "npm:^7.1.2" - loose-envify: "npm:^1.2.0" - resolve-pathname: "npm:^3.0.0" - tiny-invariant: "npm:^1.0.2" - tiny-warning: "npm:^1.0.0" - value-equal: "npm:^1.0.1" - checksum: 10/15ef2925dc665d7671308625a03e43b0c2e5b7d7a28c41b04c3ca68c805b6c4b6c6e0c217168ebf356c753ccdce6236ad717b58e6dae92be0501a0bfd44c0423 - languageName: node - linkType: hard - "hoist-non-react-statics@npm:3.3.2, hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -27278,11 +27220,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.49.2": - version: 7.53.0 - resolution: "react-hook-form@npm:7.53.0" + version: 7.53.2 + resolution: "react-hook-form@npm:7.53.2" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10/b7d73696b7c10e042f6ea6fcec01f951091146bfbc89d1378327a970bcd724b968e93fae1657bddada75caf648cfaf8693c5ba03c25e96816b755079d29f65da + checksum: 10/172589ac5871bd41717e8c6bc4c825d149db88eb02e356a8fef4115615a56b21bf66ad16954575b1d6302ed265b8780f1975a2730d5aa10a53e412b56d9f72ca languageName: node linkType: hard @@ -27662,9 +27604,9 @@ __metadata: languageName: node linkType: hard -"react-select@npm:5.8.2": - version: 5.8.2 - resolution: "react-select@npm:5.8.2" +"react-select@npm:5.8.3": + version: 5.8.3 + resolution: "react-select@npm:5.8.3" dependencies: "@babel/runtime": "npm:^7.12.0" "@emotion/cache": "npm:^11.4.0" @@ -27678,7 +27620,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10/143f3974829d6abc79a80ab79e1daaa054ad5120064c8f840d2d224cc60a853896a68ef3451f90b9997751ba17e7d558bd8875bed7be19ea3166a491d10783c7 + checksum: 10/16a4543b1d9ffbf54dd2f749cc5ff765254ad96186fbdac164c1e20e3c40a7c64b726e020050775b3d54d10b9ac6e60ff9eb7fd443010982496dad814958e5fa languageName: node linkType: hard @@ -28907,10 +28849,10 @@ __metadata: languageName: node linkType: hard -"rudder-sdk-js@npm:2.48.23": - version: 2.48.23 - resolution: "rudder-sdk-js@npm:2.48.23" - checksum: 10/520d92172f6aa870a75d7caa0a8e763c7aa20b73b82decc9e565aa17028f834891816091080e5db12442c18a25bfd6a744d88bca4fbc685c3dd7f2a6068f5205 +"rudder-sdk-js@npm:2.48.26": + version: 2.48.26 + resolution: "rudder-sdk-js@npm:2.48.26" + checksum: 10/1f0bb65fce1b764bd40ac224be7a6b6ed8e16b6bbcc35e92337b850e20771de94e3f8e6142147a6b247afc3b704243e1aecaeeec873e2aad64578d14549e579d languageName: node linkType: hard @@ -28997,9 +28939,9 @@ __metadata: languageName: node linkType: hard -"sass-loader@npm:16.0.2": - version: 16.0.2 - resolution: "sass-loader@npm:16.0.2" +"sass-loader@npm:16.0.3": + version: 16.0.3 + resolution: "sass-loader@npm:16.0.3" dependencies: neo-async: "npm:^2.6.2" peerDependencies: @@ -29019,7 +28961,7 @@ __metadata: optional: true webpack: optional: true - checksum: 10/a24ae7f6b7ef2159274b47c0a7e35cb3bfb56367a192d362ff7674f2b85b2c7bfd021e83e9351d3af36b66524efd85ebf70e242971c0b807af6622ba4ee8e298 + checksum: 10/cd49635013efae80b3a24b7e655a6f2334078006f270d50ac9e6ed19a44a80d689b402b5584b64ca737c3f948366a47e2896d07cead19015de7b2bd03f778d2b languageName: node linkType: hard @@ -30618,15 +30560,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^5.3.0": - version: 5.5.0 - resolution: "supports-color@npm:5.5.0" - dependencies: - has-flag: "npm:^3.0.0" - checksum: 10/5f505c6fa3c6e05873b43af096ddeb22159831597649881aeb8572d6fe3b81e798cc10840d0c9735e0026b250368851b7f77b65e84f4e4daa820a4f69947f55b - languageName: node - linkType: hard - "supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -31343,7 +31276,7 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^1.0.1, ts-api-utils@npm:^1.3.0": +"ts-api-utils@npm:^1.3.0": version: 1.3.0 resolution: "ts-api-utils@npm:1.3.0" peerDependencies: @@ -31487,6 +31420,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.6.2, tslib@npm:^2.8.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 + languageName: node + linkType: hard + "tslib@npm:2.4.0": version: 2.4.0 resolution: "tslib@npm:2.4.0" @@ -31515,13 +31455,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.6.2, tslib@npm:^2.7.0, tslib@npm:^2.8.0": - version: 2.8.1 - resolution: "tslib@npm:2.8.1" - checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 - languageName: node - linkType: hard - "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -32224,7 +32157,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^11.0.0": +"uuid@npm:^11.0.2": version: 11.0.3 resolution: "uuid@npm:11.0.3" bin: